# GuruWatcher Setup — first hour

Self-hosted **claim ledger + Discord buzzer**. You bring the feeds and the webhook.

**Domain:** [guruwatcher.com](https://guruwatcher.com) · **Alert-only forever — no orders.**

## This is / isn’t

| This is | This isn’t |
|---------|------------|
| Claim ledger + Discord buzzer | A trading bot / broker |
| BYO Discord + BYO sources you subscribe to | A Macro Charts reseller |
| Multi-ingest into one inbox → guards → arms | Auto-arming from charts without numbers |
| Most-recent-wins mind | A generic TradingView ping clone |

## Claim fence

- Use only newsletter / export / notes **you have the right to use**.
- Do **not** redistribute paid third-party newsletter text via docs, git, or shared packs.
- Levels arm only when a **number appears in that issue’s text**.
- Soft lists / chart-only / no number → do not arm.
- Locus of judgment stays with **you**. Alerts are not trade instructions.

## Release / install

```bash
git clone --branch v0.1.1 --depth 1 https://github.com/cemini23/GuruWatcher
cd GuruWatcher
```

- Pin a **release tag**: `v0.1.1` (see the repo’s [Releases page](https://github.com/cemini23/GuruWatcher/releases)).
- Prebuilt image: `ghcr.io/cemini23/guruwatcher:v0.1.1` (GHCR also publishes `latest` and `sha-<short>` on main).
- Platform matrix (honest OS support): [docs/PLATFORM.md](https://github.com/cemini23/GuruWatcher/blob/main/docs/PLATFORM.md).

---

## First-hour checklist

### 1. Install

**macOS / Linux:**

```bash
cd GuruWatcher   # this repo
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# Packaged alternative (also installs the `guru-watcher` console script):
#   pip install -e .
#   pip install "git+https://github.com/cemini23/GuruWatcher@<tag>"
```

**Windows (PowerShell):**

```powershell
cd GuruWatcher
py -3 -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
# If Activate.ps1 is blocked by the execution policy:
#   Set-ExecutionPolicy -Scope Process Bypass
```

**Docker Desktop (optional path):** on macOS / Windows install [Docker Desktop](https://www.docker.com/products/docker-desktop/); on Linux use native Docker. You can run everything via Compose (step 7) instead of a venv — including the full pipeline in one command.

### 2. Init portable data dir

```bash
# Interactive first-run (prompts: data dir, source id, optional webhook, optional band):
python3 -m guru_watcher init --wizard --data-dir ./gw-data
# Non-interactive (CI / headless): --wizard --yes, or GURU_WATCHER_WIZARD_YES=1
export GURU_WATCHER_DATA="$(pwd)/gw-data"
python3 -m guru_watcher status
# Alternative without env: pass --config "$(pwd)/gw-data/config.yaml" on each command
# (state resolves under gw-data/, not the git repo).
```

Windows PowerShell equivalent:

```powershell
python -m guru_watcher init --wizard --data-dir .\gw-data
$env:GURU_WATCHER_DATA = "$(Get-Location)\gw-data"
python -m guru_watcher status
```

Creates: `config.yaml`, `.env.example`, `sources/`, `inbox/`, `state/`, `README-DATA.md`.
`status` prints the data dir + config path, webhook **yes/no only** (never the URL),
watch counts, ingest-manifest age, and a `dry-run-safe` / `live-ready` mode line.

### 3. Set webhook (or stay dry-run)

```bash
cp "$GURU_WATCHER_DATA/.env.example" "$GURU_WATCHER_DATA/.env"
# Edit .env — set ONLY when you want live Discord:
# GURU_WATCHER_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
```

The CLI reads the webhook **directly from `$GURU_WATCHER_DATA/.env`** — no export needed.

Prefer a one-liner instead? The helper writes the same `.env` (portable path, not `~/.cemini`):

```bash
bash scripts/setup_discord_webhook.sh 'https://discord.com/api/webhooks/...'
```

Omit the webhook and always use `--dry-run` until ready. Never commit `.env`.

### 4. Add / edit a source

- Edit `$GURU_WATCHER_DATA/config.yaml` → `sources:` list, or  
- Edit `$GURU_WATCHER_DATA/sources/example.yaml`  
- Use **your** symbols, bands, and TTL — not a bundled paid corpus.

### 5. Ingest into inbox

Content you own rights to:

```bash
mkdir -p "$GURU_WATCHER_DATA/drop"
# copy your .md/.txt into drop/
python3 -m guru_watcher ingest-run \
  --adapter file_drop \
  --data-dir "$GURU_WATCHER_DATA" \
  --path drop \
  --once
```

Other adapters (credentials via env only — see `.env.example`):

| Adapter | Notes |
|---------|--------|
| `rss_url` | `--url 'https://…/feed.xml'` public feeds you may store |
| `notebooklm_export` | folder of `.md`/`.txt` exports |
| `imap_email` | `GURU_WATCHER_IMAP_*` (UNSEEN ∪ UID cursor; dedupe via seen set) |
| `telegram_bot` | token + **`GURU_WATCHER_TELEGRAM_CHAT_IDS`** allowlist (or `ALLOW_ALL=1`) |
| `discord_inbound` | bot token + channel IDs — **inbound only**, never posts |

### 6. Extract + dry-run check

```bash
python3 -m guru_watcher ingest-issue \
  --inbox "$GURU_WATCHER_DATA/inbox" \
  --guru example \
  --dry-run

python3 -m guru_watcher check --dry-run
```

No LIVE Discord posts unless you intentionally run without `--dry-run` with a real webhook.

**First-hour host smoke (no webhook, no Docker, zero network)** — exercises
`init → file_drop ingest → ingest-issue --dry-run → reconcile --dry-run →
check --dry-run → status` against a temp data dir and exits nonzero on any
failure. Windows: `scripts/smoke_host_oneshot.ps1`.

```bash
bash scripts/smoke_host_oneshot.sh
```

**Cold-user demo pack (synthetic, proves ≥1 armed watch)** — a self-contained
first-hour proof with an invented SPY/QQQ sample (not paid-newsletter text).
`init → ingest --extract → reconcile → list --mind → check --dry-run → status`:

```bash
bash demos/first-hour/run.sh            # Windows: powershell -ExecutionPolicy Bypass -File demos\first-hour\run.ps1
```

### 7. Docker Compose — one-command pipeline

```bash
# From repo root — mount the same portable data dir
export GURU_WATCHER_HOST_DATA="$(pwd)/gw-data"
export GURU_WATCHER_MODE=dry-run          # default; never posts
# export GURU_WATCHER_INTERVAL_SECONDS=900

docker compose up --build
```

The default `loop-pipeline` runs the full spine on an interval:
**ingest** (`file_drop` + configured adapters) → **reconcile** → **check**.

| Action | What it does |
|--------|--------------|
| `loop-pipeline` (default) | full spine every interval |
| `pipeline` | full spine **once**, then exit |
| `loop` | check-only loop (legacy) |
| `oneshot` | single dry-run check then exit |
| `ingest` | file_drop ingest once (no extract) |

Example one-shot spine: `docker compose run --rm guruwatcher pipeline`.

- Default mode **dry-run**: `check --dry-run` — never posts Discord.
- **Live:** set `GURU_WATCHER_MODE=live` **and** `GURU_WATCHER_DISCORD_WEBHOOK_URL` (fail closed without webhook).
- **Pipeline knobs:** `GURU_WATCHER_INGEST_ADAPTERS=file_drop,notebooklm_export`
  (comma allowlist; secret-gated adapters auto-skip when their env is unset),
  `GURU_WATCHER_GURU=example`, `GURU_WATCHER_RSS_URL=…`, `GURU_WATCHER_RECONCILE_NOTIFY=1`.
- **Prebuilt image instead of a local build** — swap `image` in `docker-compose.yml`:

  ```yaml
  # image: guruwatcher:local            # local build (default)
  image: ghcr.io/cemini23/guruwatcher:latest
  ```

  then `docker compose up` (no `--build`). Images are published to GHCR on
  `main` and `v*` tags: `latest`, `sha-<short>`, and semver tags.

- **Optional ingest profile (legacy):**  
  `docker compose --profile ingest run --rm ingest`  
  (file_drop from `/data/drop` → inbox — runs once).

Entrypoint: `scripts/docker_entrypoint.sh` — init if missing config; load `$DATA/.env` without printing secrets.

Validate compose file (no daemon required for some installs):

```bash
docker compose config   # if Docker CLI available
```

Smoke test — builds the image and runs a oneshot dry-run **and** a one-pass
pipeline against a temp data dir (skips with a message when Docker isn't
installed; fails if either run fails):

```bash
bash scripts/smoke_docker_oneshot.sh
```

---

## Scheduling (ingest is `--once`)

`ingest-run` always runs a **single pass** (`--once` is the default) — it does not
loop. To keep a news source flowing you schedule it yourself. The **check** loop is
what alerts; run it after ingest so new claims are armed.

### Non-Docker loop — `serve`

`python3 -m guru_watcher serve` runs the full spine (ingest → reconcile → check)
on an interval **without Docker**. Dry-run by default; `--mode live` requires a
webhook and fails closed. `serve --once` runs a single pass (CI-friendly).

```bash
python3 -m guru_watcher serve --data-dir "$GURU_WATCHER_DATA" --interval 900
python3 -m guru_watcher serve --once --data-dir "$GURU_WATCHER_DATA"   # single pass
```

### Linux / macOS cron

`crontab -e`, then a line per job. Adjust the cadence to your feeds (the example
polls every 15 minutes, matching the compose default interval):

```cron
# 1. Move drops into the inbox
*/15 * * * * cd /path/to/GuruWatcher && GURU_WATCHER_DATA="$HOME/gw-data" .venv/bin/python -m guru_watcher ingest-run --adapter file_drop --data-dir "$HOME/gw-data" --path drop --once >> "$HOME/gw-ingest.log" 2>&1

# 2. Extract claims from inbox files (writes the ledger)
*/15 * * * * cd /path/to/GuruWatcher && GURU_WATCHER_DATA="$HOME/gw-data" .venv/bin/python -m guru_watcher ingest-issue --inbox "$HOME/gw-data/inbox" --guru example >> "$HOME/gw-ingest.log" 2>&1

# 3. Project claims → armed watches
*/15 * * * * cd /path/to/GuruWatcher && GURU_WATCHER_DATA="$HOME/gw-data" .venv/bin/python -m guru_watcher reconcile --guru example >> "$HOME/gw-reconcile.log" 2>&1

# 4. Poll prices. Add --dry-run unless you set a webhook and intend live alerts.
*/5  * * * * cd /path/to/GuruWatcher && GURU_WATCHER_DATA="$HOME/gw-data" .venv/bin/python -m guru_watcher check --dry-run >> "$HOME/gw-check.log" 2>&1
```

Notes:
- The webhook is read from `$GURU_WATCHER_DATA/.env`, so cron does **not** need it exported.
- Credential’d adapters (`imap_email`, `telegram_bot`, …) read secrets from env only —
  in a cron line export them (`GURU_WATCHER_IMAP_PASSWORD=…`) or source the file first
  (`set -a; . "$GURU_WATCHER_DATA/.env"; set +a`).
- Switch the last job to live (`check` without `--dry-run`) only when a webhook is set
  **and** you intend Discord posts.

### Windows Task Scheduler

Register two tasks (ingest pipeline + price check). PowerShell one-liners:

```powershell
$py = "$env:USERPROFILE\GuruWatcher\.venv\Scripts\python.exe"
$data = "$env:USERPROFILE\gw-data"
$cmd = "& '$py' -m guru_watcher ingest-run --adapter file_drop --data-dir '$data' --path drop --once; & '$py' -m guru_watcher ingest-issue --inbox '$data\inbox' --guru example; & '$py' -m guru_watcher reconcile --guru example"

$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -Command $cmd"
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 15)
Register-ScheduledTask -TaskName "GuruWatcherIngest" -Action $action -Trigger $trigger
```

`schtasks` equivalent (ingest, then check — add `--dry-run` until you go live):

```bat
schtasks /Create /TN "GuruWatcherIngest" /SC MINUTE /MO 15 /TR "powershell -NoProfile -ExecutionPolicy Bypass -Command \"& '$env:USERPROFILE\GuruWatcher\.venv\Scripts\python.exe' -m guru_watcher ingest-run --adapter file_drop --data-dir '%USERPROFILE%\gw-data' --path drop --once\""
schtasks /Create /TN "GuruWatcherCheck" /SC MINUTE /MO 5 /TR "powershell -NoProfile -ExecutionPolicy Bypass -Command \"& '$env:USERPROFILE\GuruWatcher\.venv\Scripts\python.exe' -m guru_watcher check --dry-run\""
```

### Docker

`docker compose up` schedules the **full pipeline** loop inside the container
(`GURU_WATCHER_MODE=dry-run` default, `GURU_WATCHER_INTERVAL_SECONDS=900`) —
ingest → reconcile → check — so no separate host cron is needed. The legacy
**ingest** profile is still one-shot if you prefer to schedule it yourself:

```bash
# cron: docker compose --profile ingest run --rm ingest
*/15 * * * * cd /path/to/GuruWatcher && docker compose --profile ingest run --rm ingest >> "$HOME/gw-ingest.log" 2>&1
```

### Linux systemd (no Docker)

Generic templates live in `systemd/toolkit/` — a `guruwatcher-pipeline` timer +
service that run the same pipeline via a venv, using `GURU_WATCHER_DATA`
(portable root). See `systemd/toolkit/README.md`.

---

## Product spine (keep these four)

1. Inbox forward → durable markdown  
2. Claim ledger + most-recent-wins  
3. Verbatim guards (no invented numbers)  
4. Discord + poll — **no orders**

## Safety

- Alert-only — never places trades  
- Do not commit webhook URLs or API keys  
- Not investment advice · not a broker · not redistributing paid newsletters  

## Support

- Primary: [GitHub Issues](https://github.com/cemini23/GuruWatcher/issues)  
- Secondary: `support@guruwatcher.com`  
- No Discord invite is published until a real community space exists.

## Platform matrix

See [docs/PLATFORM.md](https://github.com/cemini23/GuruWatcher/blob/main/docs/PLATFORM.md):
pip/venv CLI on Linux/macOS/Windows; Docker pipeline on Linux + Docker Desktop;
CI smoke on Linux (host+docker), macOS (pytest), Windows (host ps1). No native GUI.

## Next

- Getting started: [getting-started/](./getting-started/) · Setup (HTML): [getting-started/setup.html](./getting-started/setup.html)
- Landing: [index.html](./index.html)  
- Deploy site notes: [README.md](./README.md) (Cloudflare Pages → guruwatcher.com)  
- Package data README after init: `$GURU_WATCHER_DATA/README-DATA.md`
