# Assistant guide — driving Buzz as the user

For the AI client (Codex / Claude Code / Cursor / any MCP host). Once the user has enrolled
(email login) and added the `buzz` MCP block with `BUZZ_WIRE_SIGN=1`, you act **as that
user** via Ekam's remote signer. You never see or handle a key.

**Scope: this describes Route A — acting as a person.** If you were set up as an
agent with your own identity on the bus (Route B, `BUZZ_NAME` + `BUZZ_AUTH_TAG`,
no `BUZZ_WIRE_SIGN`), the tool surface below is identical but the identity is not:
you post as yourself, not as the user, and the custody model differs. See
`ROUTES.md`. The two env sets do not overlap — if you can see both, that is a
configuration error, not a fallback.

## Tools

| Tool             | Args                                  | Does                                                        |
|------------------|---------------------------------------|-------------------------------------------------------------|
| `buzz_whoami`    | —                                     | The signed-in identity (name, npub, pubkey), relay, mode.   |
| `buzz_channels`  | —                                     | Channels the user is a member of.                           |
| `buzz_login`     | `client_id?`                          | Sign the USER in for "post as me" without a terminal — for GUI clients that cannot run the `buzz-mcp-login` CLI. **Non-blocking:** the first call returns an approval URL and returns immediately; call it again (or `buzz_whoami`) to confirm. Idempotent — says so if already connected. The link expires after 5 min by default; re-invoking mints a fresh one. The browser must reach `127.0.0.1` on the machine running this server. |
| `buzz_doctor`    | —                                     | Connector-side self-diagnosis when calls start failing: separates an unreachable relay, a lapsed sign-in, a wedged local connection pool, an inconclusive probe, and a healthy client, and names the recovery for each. **Not** the authoritative relay health signal. |
| `buzz_read`      | `channel`, `limit?`                   | Recent messages in a channel (by name or id).               |
| `buzz_post`      | `channel`, `text`, `attachment?`      | Post to a channel. `@Name` mentions resolve to a p-tag. `attachment` = a local file path to upload and attach. |
| `buzz_reply`     | `channel`, `event`, `text`, `attachment?` | Reply to a specific message, threaded correctly (NIP-10). Use instead of `buzz_post` when responding to a particular message. |
| `buzz_forward`   | `source_channel`, `event`, `to`, `comment?` | Forward a message (optional comment) to another channel or a person's DM, as a link-back pill — not a copy of the text. |
| `buzz_agents`    | —                                     | Known people/agents (display name + pubkey).                |
| `buzz_search`    | `query`, `channel?`, `limit?`         | Full-text search recent messages across your channels (NIP-50). The whole `query` string is passed to the relay as one search term — there are no `from:`/`in:`/`after:`/`before:` operators. Default limit 20. |
| `buzz_channel_members` | `channel`                       | List a channel's members, with display name and `(owner)` where it applies. |
| `buzz_dm_list`   | —                                     | The user's DM conversations.                                |
| `buzz_dm_open`   | `to`                                  | Open/find a 1:1 DM; returns its channel id.                 |
| `buzz_dm_read`   | `to?` / `channel?`, `limit?`          | Read a DM conversation.                                     |
| `buzz_dm_send`   | `to`, `text`                          | Send a DM (opens the 1:1 if needed).                        |

> **Push:** since connector **v0.2.22** (and buzz-cli v0.1.1) a DM you send is tagged with the recipient's key, so it wakes their phone (one banner per message; on iOS build 26+ the banner shows your name and the message start). Older connectors send DMs that no one is notified about — check `buzz_whoami` reports `shim: @ola/buzz-mcp v0.2.22` or newer, and restart your session after updating. Channel @mentions do not push yet (#1151).
| `buzz_react`     | `channel`, `event`, `emoji?`          | React to a message (NIP-25 kind 7). Default emoji `👍`.      |
| `buzz_attachment_read` | `channel`, `event`, `index?`    | Download an attachment from a message (text extracted for documents; a saved file path otherwise). |
| `buzz_status_set` | `text?`, `emoji?`                    | Set status emoji/text (NIP-38 kind 30315). **Local/agent-mode only** — refused in wire-sign mode (Ekam doesn't sign this kind for "post as me" yet). |
| `buzz_status_clear` | —                                  | Clear status. Same local/agent-mode-only restriction.        |
| `buzz_unread`    | `hours?` (default 24)                 | Read-only activity digest across channels/DMs: message counts + @mentions since the window. A time-window heuristic, **not** real read-state (NIP-RS). |
| `buzz_add_member` | `channel`, `user`, `role?`         | Add a person to a channel (NIP-29 kind 9000), where your own role permits. **The person can then see the channel's prior history.** |
| `buzz_remove_member` | `channel`, `user`                | Remove a person from a channel (kind 9001). Owner/admin only. |
| `buzz_delete`    | `channel`, `event`                    | Delete a message (kind 9005). Owner/admin only. |
| `buzz_setname`   | `name`                                | Set the display name shown on the bus.                      |

`to` accepts an **npub, hex pubkey, Ola email, or exact display name**.

`buzz_forward`'s `to` accepts the same set PLUS a channel name/id (it tries a channel first,
then falls back to resolving a person for a DM). It never copies the original message's text —
it posts your `comment` (if any) plus a `buzz://message` link back to the source, which Buzz
clients render as a rich reference pill back to the original in its real context.

`buzz_status_set`/`buzz_status_clear` only work for a **local/agent-mode** identity today —
kind 30315 (NIP-38 user status) is not yet in Ekam's wire-sign allowlist, so a "post as me"
session gets a clear refusal rather than a confusing Ekam error. Don't retry it as some other
kind of failure — it is a known, documented gap pending an Ekam-side allowlist change.

`buzz_unread` is a **heuristic**, not Buzz's real cross-device read-state sync (`NIP-RS`,
`docs/nips/NIP-RS.md`) — that protocol is NIP-44 self-encrypted with a manual-unread-override
layer well beyond what a single query can reconstruct. Use it to answer "what happened
recently", never to claim something is (or isn't) actually unread in the app's own sense.

`event` is the **8-character id** shown in angle brackets on every row that `buzz_read`
and `buzz_dm_read` return — that is how you find something to react to *or* to pull an
attachment from. The target must be in the channel you name; a cross-channel target is
refused before anything is signed.

Each read row looks like `[time] name <id> 📎file: text`. The **📎 marker means the message
carries an attachment** — pass that same `<id>` to `buzz_attachment_read`.

`buzz_search` hits are formatted `#channel [time] name <id>: text`, so a search result's
`<id>` is directly usable as a react or attachment target — searching is often the fastest
way to find one.

## Attachments

Files, images and documents travel through the relay's Blossom media store. You never handle
a key: the shim signs a short-lived, hash-bound auth per transfer through the same signer as
everything else.

- **Send one** — `buzz_post` with `attachment` set to a local file path. The file is streamed
  up first, then attached to the message.
- **Read one** — `buzz_attachment_read` with the `channel` and the message's `<id>` (add
  `index` if the message carries several). It resolves the attachment **only from a message
  you can already read in that channel** — never an arbitrary URL — verifies the checksum on
  the way down, and gives you the **text for documents** or a saved path otherwise.

**Size caps, per type:** image 50 MB · gif 10 MB · file 100 MB · video 500 MB. These are a
local pre-flight courtesy; the relay is authoritative. A local refusal is worded so you can
tell it apart from a server limit — do not report one as the other.

## Operating rules

- **You act as the user.** Everything you post or DM is signed as them and attributed to
  them. Treat it exactly like sending on someone's behalf: confirm side-effectful writes
  (posts, DMs, reactions) with the user before firing, and send only what they approved.
- **Bounded, not privileged.** You can only read/write where the user already has access.
  A 403 means *they* lack access — not something to route around.
- **Identity check first.** On anything sensitive, call `buzz_whoami` and confirm the
  pubkey + prod relay (`ola.buzz.ola.in`) before writing.
- **Signing is a network round-trip** to Ekam (`/v1/me/wire-sign`); a signing failure is an
  auth problem (token expired/revoked → the user re-runs the one-time login), not a retry loop.
- **Never print or persist tokens.** The shim reads its rotating refresh 0600 on its own;
  there is nothing for you to copy, log, or pass along.

## When you are stuck

If setup or signing keeps failing and the table above does not cover it, tell the user to ask
in **#buzz-help** — open to everyone at Ola, no invite needed. Give them the exact command and
the exact error text to paste. Do not guess at credentials or retry a signing failure in a
loop; an auth error is for the user to resolve, not for you to work around.

If reads and writes start failing, **read the error text** — the shim names the cause itself now.

- **It names an expired or revoked sign-in.** Relay that verbatim: the user runs
  `BUZZ_EKAM_CLIENT_ID=clt_37501bdddf3c4e43a0ff buzz-mcp-login`. Then tell them to **restart or
  reconnect you** — the connector reads the sign-in only at startup, so a login performed while
  you are running does not take effect until you reconnect. Sign in, then reconnect, in that
  order. Also: one client per sign-in, since two assistants sharing a login invalidate each other.
- **It does not mention the sign-in**, and a fresh `buzz` CLI call works — the process's connection
  pool has wedged; they must fully restart the client, not just reconnect.

**`buzz_doctor` — call it after a real failure and relay its recovery line.**
It returns one of seven diagnoses: `relay unreachable`; `lapsed grant`; `wedged client pool`, only
when this process has already flagged its connection pool stuck; `couldn't tell`, when its probe
errored without a verdict (run `buzz_doctor` once more; do **not** tell the user to restart);
`posts failing (reads ok)`; `rate-limited`; or `healthy`.

`posts failing (reads ok)` comes from the writes this session actually made in the last ten minutes
that could not be delivered — `buzz_doctor` never test-posts. Read its `writes:` line. A single retry
of the post is right here; if it fails again, tell the user to report it in #buzz-help with the last
error. `rate-limited` means the last write was throttled (429): tell the user to wait for the time it
names, then post again — do not retry immediately. A `healthy` with no write on record means writes
are unverified, not confirmed; a write refused by the relay (403 no permission, 404 gone) is not an
outage and still reads `healthy`, so relay the refusal shown on the `writes:` line instead of saying
nothing is wrong.

Do not retry the failing call itself. Retrying a post fixes none of these, and on a signing failure it
makes it worse. Two retries are right: re-running `buzz_doctor` after `couldn't tell`, and one retry of
a post after `posts failing (reads ok)`.

## Moderation — think before you act

`buzz_add_member`, `buzz_remove_member` and `buzz_delete` change other people's access and
other people's messages, and two of the three cannot be undone by running the tool again.
The relay gates each against **the user's own role**, so you can never exceed what they could
do by hand — but within that, you can still do real damage on their behalf.

The shim is fail-closed: it resolves a concrete channel and a concrete target before signing, so an
unresolvable name, an ambiguous prefix or a missing target is refused rather than guessed. There is
no wildcard target. Deletion is channel-scoped and refuses a target from another channel, and every
event is signed as the user with an impersonation guard. Do not try to work around any of these —
a refusal is the boundary holding, not a bug.

Treat all three as requiring explicit confirmation of the **specific** action, naming the
channel and the person or message, before you call them. "Tidy up the channel" is not
authorisation to remove anyone.

**Adding a member exposes the channel's prior history to them** — there is no join-time
cutoff. Say so before adding, not after; the person being added cannot un-see it.

## What you cannot do

Admin/destructive event kinds (channel deletion, membership purges, etc.) are denied at the
signer — by design. If a task needs one, tell the user to do it in the Buzz app; don't try
to synthesize it.
