# Jarvis WhatsApp Reader (read-only)

A standalone, **read-only** WhatsApp watcher for Jarvis. Ahmed links his
**secondary (client) WhatsApp number** once via QR; a background daemon then
passively captures **incoming** messages into a local SQLite DB and exposes them
to Jarvis through a read-only MCP server so Jarvis can answer *"who texted me and
what did they say"* during a voice conversation.

**It never sends messages, read-receipts, typing, or presence by default** —
`markOnlineOnConnect:false` and the code path simply never calls any write/ack
API. The number stays invisible and effectively un-bannable.

> Optional, off by default: a **gated reply** capability exists (`whatsapp_send`)
> but is physically disabled unless the daemon is started with `WA_ALLOW_SEND=1`.
> See "Enabling replies" below.

## Stack
- `@whiskeysockets/baileys` — WhatsApp multi-device, used read-only (pinned to
  the stable `6.17.16`; `latest` is a 7.x release candidate).
- `@modelcontextprotocol/sdk` — stdio MCP server.
- `better-sqlite3` — local store (WAL). Falls back to Node's built-in
  `node:sqlite` if the native build fails. **This machine uses:** see the
  `[db] backend=...` line the daemon/MCP print on startup.
- `qrcode-terminal`, `pino` (silent logger).

## Install
```bash
cd /Users/ahmed/devFolder/Ultron/claude-voice/whatsapp-reader
npm install
chmod +x run.sh          # first time only
```

## (a) One-time QR login
Run the daemon in a terminal and scan the QR **with the SECONDARY number**:
```bash
node daemon.js
```
On the phone that owns the secondary number:
**WhatsApp → Settings → Linked Devices → Link a Device**, then scan the QR
printed in the terminal. The session is saved to `./auth/` and persists across
restarts — you won't scan again unless you log out from the phone.

Once you see `[ok] connected — passively watching incoming messages`, it's live.
Leave it running, or install the always-on launch agent (below).

## Always-on (launchd)
```bash
cp com.jarvis.whatsapp-reader.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.jarvis.whatsapp-reader.plist
# stop:
launchctl unload ~/Library/LaunchAgents/com.jarvis.whatsapp-reader.plist
```
Do the QR login (above) **first** — `KeepAlive` would otherwise loop past the QR
before you can scan. Logs go to `./daemon.log`. Paths in the plist are absolute
for this machine; edit them if node or the project move.

## What gets stored
Local `./whatsapp.db` (SQLite, WAL) — this file **is** the persistent WhatsApp
memory (survives restarts, fully queryable). Per incoming message: sender number
+ name, body/caption, timestamp (the WhatsApp message time), quoted/replied text
+ its sender, @mentions, media type (image/video/audio/document/sticker/location)
with document filename or location lat/lng, and the group subject for group
chats. **Media is never downloaded — only the fact it exists + metadata.** The
daemon logs only `[+] msg from <number> (<name>)` — never the message body.

## (b) Register with Jarvis (do this later, by hand)
This reader does **not** edit `voice/llm.py`. When you're ready, add a stdio MCP
server to `_mcp_servers()` in `voice/llm.py`, guarded by an env flag so it's
easy to toggle. Slot this in next to the other stdio servers:

```python
    # Read-only WhatsApp situational awareness (secondary number).
    # MCP_WHATSAPP=0 disables. Needs `node` on PATH and the daemon running.
    if os.environ.get("MCP_WHATSAPP", "1") != "0" and shutil.which("node"):
        servers["whatsapp"] = {
            "type": "stdio",
            "command": "node",
            "args": ["/Users/ahmed/devFolder/Ultron/claude-voice/whatsapp-reader/mcp.js"],
            "env": env,
        }
```

That exposes `whatsapp_status`, `whatsapp_recent`, `whatsapp_from`,
`whatsapp_conversations` (all read-only) and the gated `whatsapp_send`. The MCP
server only reads the SQLite the daemon writes — it holds no WhatsApp socket, so
it's safe to start/stop independently.

## Enabling replies (opt-in, off by default)
By default the reader can't send. To allow Jarvis to send a **single reply when
Ahmed explicitly asks**:
1. Start the daemon with the flag: `WA_ALLOW_SEND=1 node daemon.js`
   (or uncomment `WA_ALLOW_SEND` in the plist).
2. Only then does the daemon open a **local-only Unix socket** `./control.sock`
   that `whatsapp_send` talks to. With the flag unset the socket is never
   created, so `whatsapp_send` returns *"replies are disabled by default for
   safety"* and nothing can be sent.

Keep it low-volume and never bulk — that's the guardrail against bans.

## Continuous CRM sync (`crm_sync.py`)
Hourly WhatsApp → jarvis-memory CRM sync. Every hour it finds DM threads with
NEW messages and refreshes **one dense CRM fact per lead** in jarvis-memory
(same format the backfill used); the memory pipeline supersedes the previous
fact for that lead automatically, so each lead keeps a single current fact.

- **Watermark = message TIMESTAMP per thread, never read-state.** A thread is
  due when `MAX(ts) > sync_state.last_ts` — messages are processed even if
  Ahmed already read/replied from his phone, and his own outbound replies
  (`from_me=1`) are part of the ingested arc. (The `messages` table has no
  read/unread column at all, so read-state cannot even be consulted.)
- **Own table only**: read-only on `whatsapp.db` except its private
  `sync_state(peer_number PK, last_ts, fact_hint)` watermark table.
- **Transcript-aware**: threads whose only new rows are `media_status='pending'`
  with an empty body (voice notes not yet transcribed) are left for a later run.
- **Capped**: max 30 threads/run, oldest-activity first — a burst never blows
  the hour.
- **One DeepSeek call per thread** (`deepseek-v4-flash`) composes the fact;
  POST to the memory `/remember` endpoint. On success the watermark advances to
  the thread's `MAX(ts)`; on any failure it does NOT advance (retried next hour).
- Keys read from `../.env` (`DEEPSEEK_API_KEY`, `MEMORY_API_KEY`), never printed.
- Logs to `./crm_sync.log` (self-truncates at ~1MB).

```bash
python3 crm_sync.py --dry              # preview facts for due threads, no POST
python3 crm_sync.py --dry --peer <n>   # preview a single thread
python3 crm_sync.py --once             # one real pass (what launchd runs)
# hourly launchd agent (interval-only; no run at load):
cp com.jarvis.wa-crm-sync.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.jarvis.wa-crm-sync.plist
# stop:
launchctl unload ~/Library/LaunchAgents/com.jarvis.wa-crm-sync.plist
```

## Files
- `daemon.js` — passive listener (+ gated send path).
- `mcp.js` — read-only MCP server.
- `db.js` — SQLite store + query helpers.
- `resolve.js` — pluggable identity hook (CRM lookup stubbed/disabled for v1).
- `crm_sync.py`, `com.jarvis.wa-crm-sync.plist` — hourly CRM sync → jarvis-memory.
- `run.sh`, `com.jarvis.whatsapp-reader.plist` — process management.
