wactx · git:20260415.6839114 · 2026-04-15 · sha256 f365f288e8d239bf

wactx git:20260415.6839114A

Immutable. This exact content never changes and is served at /api/v1/blob/f365f288e8d239bf.

---
name: wactx
description: >
  Search and reason over the user's local WhatsApp message history with
  semantic + graph search. Use whenever the user asks about contacts, past
  conversations, who knows about a topic, who to reach out to, what was said
  in a chat, who is connected to whom, or any question grounded in their
  WhatsApp history. All data is local — no network calls except the
  embedding/LLM provider the user configured. Prefer `wactx search --json`
  and parse the structured output; never screen-scrape Rich-rendered panels.
allowed-tools: Bash(wactx *)
---

# wactx — WhatsApp context search for agents

`wactx` is a CLI over the user's local DuckDB WhatsApp archive. It ranks **people** and **messages** against a natural-language query using BM25 + dense embeddings + personalized PageRank over a relationship graph.

This file is the authoritative contract. When in doubt, prefer the flags and JSON fields documented here over anything in `--help`.

---

## When to invoke

Invoke `wactx search` whenever the user's question is grounded in their own WhatsApp history. Typical triggers:

- "Who has talked about X?" / "Who knows about X?"
- "Find me someone who can help with Y"
- "What did <person/group> say about Z?"
- "Who should I reach out to for W?"
- "Did anyone mention <topic> recently?"
- "Who's in my network for <domain>?"

Do NOT invoke `wactx sync`, `wactx index`, `wactx enrich`, `wactx graph`, `wactx init`, or `wactx clean` unless the user explicitly asks — those mutate state or require interactive input (QR scan) and should not be run autonomously.

---

## The only command you should run autonomously

```bash
wactx search "<query>" --json [flags...]
```

Also safe:
```bash
wactx stats --json
```

Everything else is either interactive (`init`), mutating (`sync`, `index`, `enrich`, `graph`, `download`), or destructive (`clean`).

---

## Input contract

### Positional

- `QUERY` — natural-language search string. Put it in double quotes. BM25 tokenizes this; semantic search embeds it; the LLM expands it into variants unless `--depth fast` or `--variants 1`.

### Depth presets (trade latency vs. recall)

| `--depth`  | variants | top | graph | iterations | when to use                                    |
| ---------- | :------: | :-: | :---: | :--------: | ---------------------------------------------- |
| `fast`     |     1    |  10 |  no   |      1     | you need a quick answer, single query          |
| `balanced` |     5    |  15 |  yes  |      3     | default; most questions                        |
| `deep`     |     8    |  30 |  yes  |      3     | hard questions, discovery, comprehensive recall |

Override individual knobs: `--variants N`, `--top N`, `--iterations N`, `--no-graph`.

### Filters (compose freely)

| Flag                    | Semantics                                                                                      |
| ----------------------- | ---------------------------------------------------------------------------------------------- |
| `-k, --keyword TEXT`    | Repeatable. Each keyword is a required case-insensitive substring (**AND** across keywords).   |
| `--chat TEXT`           | JID (`...@g.us` / `...@s.whatsapp.net`) OR case-insensitive substring of chat/contact name.    |
| `--after YYYY-MM-DD`    | Inclusive lower bound on message timestamp.                                                    |
| `--before YYYY-MM-DD`   | Inclusive upper bound (treated as end-of-day).                                                 |

Filter semantics:
- Filters apply to **BM25, vector search, and graph expansion** uniformly — results can never leak past a filter.
- `--chat` that matches zero chats fails fast with exit code 1 and an error message on stderr. Retry with a broader substring or list chats via `wactx stats --json` first.
- Dates are validated at the CLI layer; `--after 2026/03/01` is rejected with a `click.BadParameter`.

### Flags summary

```
wactx search QUERY
  --depth {fast|balanced|deep}   # preset
  --variants N                   # override query expansion count
  --top N                        # override top-k
  --iterations N                 # graph expansion passes
  --no-graph                     # skip PPR/PathRAG
  -k, --keyword TEXT             # required literal substring (repeat for AND)
  --chat TEXT                    # scope to one chat (JID or name substring)
  --after  YYYY-MM-DD            # time lower bound (inclusive)
  --before YYYY-MM-DD            # time upper bound (inclusive, end of day)
  --json                         # structured output — ALWAYS use this
```

---

## Output contract (`--json`)

Top-level shape:

```json
{
  "query": "kubernetes expert",
  "depth": "balanced",
  "elapsed_s": 0.37,
  "queries_used": ["kubernetes expert", "k8s expert", "..."],
  "progress": [{"label": "Query → 5 variants"}, "..."],
  "filters": {
    "keywords": ["Kubernetes"],
    "chat": "Founders",
    "chat_jids": ["120363abcdef@g.us"],
    "after": "2026-01-01",
    "before": null
  },
  "people":   [ { ...PersonResult   }, ... ],
  "messages": [ { ...MessageResult  }, ... ]
}
```

### `PersonResult`

| Field            | Type               | Meaning                                                                 |
| ---------------- | ------------------ | ----------------------------------------------------------------------- |
| `name`           | string             | Display name (full_name > push_name > JID).                             |
| `phone`          | string             | E.164-ish `+<digits>` derived from JID, or `""` for group members.      |
| `score`          | number 0..1        | Final ranking score (retrieval + PPR + graph + conversational boost).   |
| `similarity`     | number 0..1        | Max cosine similarity of their messages to the query.                   |
| `ppr_score`      | number             | Personalized PageRank score (higher = more central w.r.t. seeds).       |
| `dm_volume`      | integer            | DM exchange count with the owner (if `owner_name` is configured).       |
| `shared_groups`  | string[]           | Groups this person shares with the owner.                               |
| `entities`       | `{type,value,count}[]` | Top entities this person talks about (tech / org / event / url / person). |
| `message_count`  | integer            | How many of this person's messages made it into the top results.        |
| `top_message`    | string (≤200 chars) | Their single most relevant message, truncated.                         |

### `MessageResult`

| Field        | Type    | Meaning                                   |
| ------------ | ------- | ----------------------------------------- |
| `similarity` | number  | Cosine similarity to the query embedding. |
| `sender`     | string  | Display name of the sender.               |
| `phone`      | string  | E.164-ish phone of the sender.            |
| `text`       | string  | Message text, ≤200 chars.                 |
| `group`      | string  | Chat label (group name or `"DM"`).        |
| `time`       | string  | Timestamp (string-cast).                  |

### People are the answer to "who"; messages are the evidence

The usual recipe:

1. Run `wactx search "..." --json` with the user's question.
2. Surface the top 3–5 `people[]` with name, phone, and 1–2 quoted messages each (from that person's `top_message` or the matching entries in `messages[]`).
3. Pull graph signals (`dm_volume`, `shared_groups`, `entities`) as context so the user can decide *who to actually contact*.
4. Always include the phone number — that's the primary action the user will take.

---

## Exit codes and errors

| Exit | Meaning                                                                       |
| ---: | ----------------------------------------------------------------------------- |
|  `0` | Success (including zero results — check `len(people)` / `len(messages)`).     |
|  `1` | Error: invalid date, unknown chat filter, missing database, CLI usage error.  |

Errors go to **stderr**; `--json` output is on **stdout**. Parse only stdout. On non-zero exit, check stderr for a `click`-style message.

Known error shapes:

- `No chats match: 'foo'` → your `--chat` substring didn't match. Try a broader substring, or run `wactx stats --json` and look at the tables to pick a JID directly.
- `--after must be YYYY-MM-DD, got '...'` → reformat the date. Only `YYYY-MM-DD` is accepted.
- `No database found. Run 'wactx init' first.` → the user hasn't set up wactx. Tell them, don't try to run `init` yourself (it's interactive).

---

## Recipes

### "Who knows about Kubernetes?"

```bash
wactx search "kubernetes expert" --json
```

Parse `people[:5]`, name + phone + top_message.

### "Who's talked about fundraising recently?"

```bash
wactx search "fundraising advice" --after 2026-01-01 --json
```

### "What did anyone say about GPT-5 in the Founders group?"

```bash
wactx search "GPT-5 impressions" --chat "Founders" --json
```

### "Show me any message that literally mentions Claude Code"

```bash
wactx search "anything" -k "Claude Code" --depth fast --json
```

The `-k` filter ensures literal substring match; `"anything"` is a weak query that lets BM25/vector ranking fall back to the keyword filter.

### "Who do I know at OpenAI?"

```bash
wactx search "works at OpenAI" -k OpenAI --json
```

### "Fundraising advice in ML Engineers group, March 2026"

```bash
wactx search "seed round" \
  --chat "ML Engineers" \
  --after 2026-03-01 --before 2026-03-31 \
  --json
```

### Reducing output size for large responses

Agents should pipe through `jq` to trim tokens:

```bash
wactx search "kubernetes" --json | jq '{
  people: [.people[] | {name, phone, score, top_message}][:5],
  messages: [.messages[] | {sender, text, time}][:5]
}'
```

Prefer this over dumping the full JSON into the context window.

### Discovering what's in the database

```bash
wactx stats --json
# {
#   "db_path": "/home/.../messages.duckdb",
#   "tables": { "messages": 42817, "contacts": 531, ... }
# }
```

---

## Agent-specific rules

**DO:**
- Always pass `--json` and parse the result.
- Always quote the `QUERY` argument.
- Compose filters instead of post-filtering the JSON — filters are applied server-side across BM25, vector, and graph passes.
- Prefer narrow filters (`--chat`, date range) on follow-up questions so the context window stays small.
- Trim output with `jq` before presenting to the user or re-feeding into reasoning.
- Report `people[].phone` whenever you tell the user "contact X" — that's the actionable identifier.

**DO NOT:**
- Do NOT run `wactx init`, `wactx sync`, `wactx clean`, `wactx index`, `wactx enrich`, `wactx graph`, or `wactx download` without an explicit user request. These mutate state, require a QR scan, or make API calls that cost the user money.
- Do NOT screen-scrape the Rich-rendered (non-`--json`) output. It is not stable.
- Do NOT use `--keyword` for loose thematic matches — BM25 + semantic already handle that. Use `-k` only when you need *literal* substring containment (for example, a company name, a URL fragment, an unusual acronym, or when the user says "messages that mention X").
- Do NOT invent JIDs. Either pass a substring of a chat name to `--chat`, or pull a real JID from a prior `wactx search --json` response / `wactx stats --json`.
- Do NOT pass dates in any format other than `YYYY-MM-DD`. No relative dates (`"last week"`), no slashes, no timezones.

---

## Failure-mode cheatsheet

| Symptom                                                   | Cause                                                     | Fix                                                                  |
| --------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------- |
| Empty `people[]` and `messages[]`                         | Filters too narrow, or DB not yet indexed                 | Drop a filter; run `wactx stats --json` to confirm the DB has rows.  |
| Exit 1, stderr says "No chats match"                      | `--chat` substring hit zero rows                          | Try a shorter substring, or pass a JID directly.                     |
| Exit 1, stderr says "--after must be YYYY-MM-DD"          | Wrong date format                                         | Reformat to `YYYY-MM-DD`.                                            |
| `queries_used` is length 1 on a `--depth balanced` query  | Query expansion LLM call failed (API key, network)        | Tell the user their provider might be down; try again or `--depth fast`. |
| `score` is very low on every result                       | Query is off-topic for this user's archive                | Rephrase, drop `-k`, or broaden the date range.                      |

---

## Bounds and cost

- `wactx search` **reads** the DB. It does not mutate anything. Safe to call repeatedly.
- `wactx search` **calls the user's embedding + chat provider** (OpenAI by default) to expand queries and embed them. Each call is small but non-zero cost. Prefer `--depth fast` when a fast single-query answer is enough.
- `wactx search` opens the DuckDB file in read-only mode — you can run it in parallel with other `wactx search` calls safely.