## Summary
This change adds `buzz messages report`.
The command sends a NIP-56 report (kind 1984) to the community
moderators.
You can report a message with `--event`.
You can report a user with `--pubkey`.
The `--reason` flag is required.
It accepts one of seven values: `illegal`, `nudity`, `malware`, `spam`,
`impersonation`, `profanity`, `other`.
The `--note` flag is optional.
The `--channel` flag is optional.
When you give it, the CLI checks that the message is in that channel.
The command prints the write envelope `{event_id, accepted, message}`
and nothing else.
## Design
The relay keeps a report in the moderation queue only.
The relay does not store it as a normal event.
The relay does not send it to subscribers.
So the CLI prints no read-back.
The tag shape is the same as desktop's `submitReport`:
- Message report: `["p", <author>]` and `["e", <event id>, <reason>]`.
- User report: `["p", <pubkey>, <reason>]`. The relay accepts this
shape.
- The note is trimmed and becomes the event content.
The command adds no `h` tag.
Desktop sends none.
The relay reads the channel from the stored event.
The task text mentioned an `h` tag.
The real relay and desktop code do not use one.
For `--event`, the CLI queries the event by id to learn the author's
pubkey.
That query uses an `ids` filter with `limit` 1.
It names no kinds, because a message can have any kind.
The existing `messages edit` and `messages delete` commands use the same
lookup.
`buzz-sdk` has a new `build_report` builder and a `REPORT_TYPES` list.
The list matches the relay's `REPORT_TYPES`.
The builder rejects unknown reasons, bad hex, and a note over 64 KiB.
The builder allows self-tagging.
Without it, the signer drops the `p` tag when you report your own
message or key, and the relay rejects the report.
Desktop opts in for the same reason.
The CLI lowercases `--event` and `--pubkey` before it queries and signs.
Clap also rejects unknown reasons, so the usage error appears before any
network call.
Exit codes:
- Missing or unknown `--reason`, bad hex, channel mismatch: 1.
- Relay rejection "invalid: report target ... not found" (exact text):
1. The relay gives this answer for a missing message and for a message
you cannot read.
- Other relay rejections (HTTP 400): 2.
- HTTP 401 or 403: 3.
The CLI README describes the new command.
## Testing
What ran:
- `cargo test -p buzz-cli -p buzz-sdk`: 510 CLI tests and 327 SDK tests
pass.
- `cargo clippy -p buzz-cli -p buzz-sdk --all-targets -- -D warnings`:
clean.
- `cargo fmt --all --check`: clean.
- `just file-size-check`: passes.
This PR does not change `desktop/` (deprecated; see #8840), so I did not
run the Tauri crate tests.
The CLI tests call the production `cmd_report_message` function against
a local fake relay.
They check the exact tags, the user-report shape, self-reports (the
signer is the reported author), uppercase ids, argument parsing, the
channel check, the not-found path, and the exit code for each relay
rejection.
Interop evidence:
- I ran the real `buzz` binary against a local node HTTP server that
stands in for a relay.
- In the same node process, I ran desktop's own `submitReport` from
`desktop/src/shared/api/moderation.ts` with a mocked signer.
- Kind, tags, and content are equal for `spam` with a note and for
`other` without a note.
- The script prints `INTEROP OK`.
- No shared relay was used.
Mutation checks (break the guard, see a test fail, restore):
- SDK reason check removed: fails.
- SDK `e` tag without the reason: fails.
- SDK `p` tag changed: fails.
- SDK note trim removed: fails.
- SDK note size cap removed: fails.
- CLI channel check removed: fails.
- CLI not-found mapping removed: fails.
- CLI author read from the wrong field: fails.
- CLI event-id hex check removed: fails.
- SDK self-tagging removed: fails in the SDK test and in the CLI
self-report test.
- CLI lowercasing removed: fails.
- Not-found mapping widened to any `invalid:` 400: fails.
- Clap reason value parser removed: fails.
- Clap required-target group removed: fails.
- Clap `--channel` conflict with `--pubkey` removed: fails.
One CLI `--pubkey` hex check was redundant with the builder.
I removed it instead of keeping a guard no test can catch.
## Manual test steps
You need a local relay and a running desktop or a second account to see
the queue.
Do not use a shared relay.
1. Start a local relay with `just relay`.
2. Send a message: `buzz messages send --channel <uuid> --content
"test"`. Copy the event id.
3. Report it: `buzz messages report --channel <uuid> --event <event-id>
--reason spam --note "test report"`.
Success: the output is `{"event_id":"...","accepted":true,"message":""}`
and the exit code is 0.
4. As a moderator, run `buzz moderation reports`.
Success: the report appears with type `spam` and your note.
5. Run `buzz messages get --channel <uuid>`.
Success: the report does not appear in the channel.
6. Report a user: `buzz messages report --pubkey <hex> --reason
impersonation`.
Success: `accepted` is true and the report appears in the queue.
7. Run `buzz messages report --event <event-id> --reason bogus`.
Success: exit code 1 and a `user_error` message.
8. Run `buzz messages report --event <64 zeros> --reason spam`.
Success: exit code 1 and a not-found message.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Signed-off-by: bmcanally <bmcanally@squareup.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
10 KiB
Buzz CLI
Agent-first command-line interface for Buzz relay. JSON in, JSON out.
Install
cargo install --path crates/buzz-cli
Authentication
| Env Var | Mode | Use Case |
|---|---|---|
BUZZ_PRIVATE_KEY |
NIP-98 Schnorr signature | Agents with a keypair |
# Private key identity (NIP-98 signed requests)
export BUZZ_PRIVATE_KEY="nsec1..."
buzz channels list
Usage
All output is JSON on stdout. Errors are JSON on stderr. Exit codes: 0=ok, 1=user error, 2=network, 3=auth, 4=other, 5=write conflict.
# Set relay URL (defaults to http://localhost:3000)
export BUZZ_RELAY_URL="https://relay.example.com"
# Messages
buzz messages send --channel <uuid> --content "Hello"
buzz messages send --channel <uuid> --content "Reply" --reply-to <event-id> --broadcast
buzz messages send --channel <uuid> --content - < message.md # read body from stdin
buzz messages get --channel <uuid> --limit 20
buzz messages thread --channel <uuid> --event <event-id>
buzz messages thread --link 'buzz://message?channel=<uuid>&id=<event-id>&thread=<root-id>'
buzz messages search --query "architecture"
buzz messages search --author <pubkey|npub|name> --since <unix-ts>
buzz messages edit --event <event-id> --content "Updated text"
buzz messages delete --event <event-id>
buzz messages report --event <event-id> --reason spam --note "why" # to moderators only
buzz messages report --pubkey <pubkey> --reason impersonation
# Diffs
buzz messages send-diff --channel <uuid> --diff - --repo https://github.com/org/repo --commit abc123 < diff.patch
# Channels
buzz channels list
buzz channels create --name "my-channel" --type stream --visibility open
buzz channels join --channel <uuid>
buzz channels topic --channel <uuid> --topic "New topic"
# Reactions
buzz reactions add --event <event-id> --emoji "👍"
buzz reactions get --event <event-id>
# Huddles (read-only)
buzz huddles list --channel <uuid> --active # huddles with state and start/end times
buzz huddles participants --channel <uuid> --huddle <huddle-id>
# GIFs (requires relay to advertise buzz-gif / KLIPY)
buzz gifs search # trending GIFs
buzz gifs search --query "celebration" # search GIFs
buzz gifs share --slug <slug> # report selection to provider Recents
# Paste the `cdn_url` from a search result directly into messages send --content
# Custom emoji in messages
# buzz messages send scans outgoing content for :shortcode: patterns and
# automatically attaches NIP-30 ["emoji", shortcode, url] tags from the
# workspace palette — identical to the desktop composer behavior.
# Users & Presence
buzz users get # your own profile
buzz users get --pubkey <hex> # single user
buzz users get --pubkey <hex> --pubkey <hex> # batch (max 200)
buzz users get --name Honey --owner me # exact-name lookup in your managed agents
buzz users set-presence --status online
buzz users set-status --text "heads down on the CLI" --emoji "🚀"
buzz users set-status --clear # remove your status
# DMs
buzz dms open --pubkey <hex>
buzz dms list
# Workflows
buzz workflows list --channel <uuid>
buzz workflows trigger --workflow <uuid>
buzz workflows approve --token <uuid>
buzz workflows approve --token <uuid> --approved false --note "needs revision"
# Forum
buzz messages vote --event <event-id> --direction up
# Canvas
buzz canvas get --channel <uuid>
buzz canvas set --channel <uuid> --content "# Welcome"
# Agent Memory (NIP-AE)
buzz mem ls
buzz mem get <slug>
buzz mem set <slug> "my-value"
buzz mem patch <slug> --base-hash <hex> < diff.patch # or --no-base-hash
buzz mem rm <slug>
# Repository protection
buzz repos protect list --id my-repo
buzz repos protect set --id my-repo --ref refs/heads/main --push admin --no-force-push --no-delete
buzz repos protect remove --id my-repo --ref refs/heads/main
# Pipe to jq
buzz channels list | jq '.[].name'
protect set replaces every existing rule for the exact ref pattern. Any
constraint omitted from the command is removed. protect list reports malformed
stored rules in validation_error so an owner can remove and repair them.
Commands
buzz --help prints this whole surface as an indented tree — every group with
its subcommands and their descriptions — so there is no need to run --help
once per group to find a command. buzz -h keeps the short group-level
summary, and buzz <group> <subcommand> --help has the flags and examples.
The table below mirrors that tree for readers who are not at a terminal.
| Group | Subcommand | Description |
|---|---|---|
messages |
send |
Send a message to a channel |
send-diff |
Send a code diff with metadata | |
edit |
Edit a message you sent | |
delete |
Delete a message | |
report |
Report a message or user to community moderators (NIP-56); reasons: illegal, nudity, malware, spam, impersonation, profanity, other |
|
get |
List messages in a channel | |
thread |
Get a message thread | |
search |
Full-text search, filterable by author | |
vote |
Vote on a forum post | |
channels |
list |
List channels |
get |
Get channel details | |
create |
Create a channel | |
update |
Update channel name/description | |
topic |
Set channel topic | |
purpose |
Set channel purpose | |
join |
Join a channel | |
leave |
Leave a channel | |
archive |
Archive a channel | |
unarchive |
Unarchive a channel | |
delete |
Delete a channel | |
members |
List channel members | |
add-member |
Add a member | |
remove-member |
Remove a member | |
canvas |
get |
Get channel canvas |
set |
Set channel canvas | |
reactions |
add |
React to a message |
remove |
Remove a reaction | |
get |
List reactions | |
huddles |
list |
List a channel's huddles (read-only) |
participants |
Show who is in a huddle (read-only) | |
gifs |
search |
Search or browse trending GIFs (requires relay buzz-gif support) |
share |
Report a selected GIF to the provider's Recents | |
dms |
list |
List DM conversations |
open |
Open a DM (1–8 pubkeys) | |
add-member |
Add member to DM group | |
users |
get |
Get user profile(s) |
set-profile |
Update your profile | |
presence |
Get presence status | |
set-presence |
Set presence status | |
set-status |
Set or clear your NIP-38 profile status | |
workflows |
list |
List workflows |
get |
Get workflow definition | |
create |
Create a workflow | |
update |
Update a workflow | |
delete |
Delete a workflow | |
trigger |
Trigger a workflow | |
runs |
Get workflow run history | |
approve |
Approve/deny a workflow step | |
feed |
get |
Get your activity feed |
social |
publish |
Publish a NIP-01 note |
set-contacts |
Set NIP-02 contact list | |
event |
Get a Nostr event | |
notes |
Get notes for a user | |
contacts |
Get NIP-02 contact list | |
repos |
create |
Announce a git repository (NIP-34) |
get |
Get a repository announcement | |
list |
List repository announcements | |
default-branch get/set |
Read or select an existing default branch (requires relay support) | |
protect list |
List branch and tag protection rules | |
protect set |
Create or replace a protection rule | |
protect remove |
Remove a protection rule | |
upload |
file |
Upload a file to the Blossom store |
pack |
validate |
Validate a persona pack (local, no relay) |
inspect |
Inspect a persona pack (local, no relay) | |
mem |
ls |
List non-tombstoned memories |
get |
Print memory value to stdout | |
hash |
Print SHA-256 hex of memory value | |
set |
Write a memory value (use - for stdin) |
|
patch |
Apply unified diff to memory value | |
rm |
Publish a tombstone to delete memory |
Huddles
huddles list and huddles participants rebuild huddle state from the relay-signed lifecycle
events (kinds 48100–48103) with the same rules as the desktop app. They never join, start, or end
a huddle. Each scans at most 5000 newest events of the channel. If older events may be missing,
complete is false and truncated is true. A huddle with no end event is active, then
stale once its start is over an hour old; desktop shows stale as ended. The relay's liveness
snapshots are not served over HTTP, so liveness_checked is always false.
Rosters come from the relay's join and leave events only. Desktop's roster view also lists the
creator from the start event; the CLI lists the creator once the relay records their join.
Architecture
buzz <group> <subcommand> [flags]
│
├─ main.rs ──▶ commands/*.rs ──▶ client.rs ──▶ Buzz Relay REST API
│ (clap) (handlers) (reqwest)
│
├─ validate.rs (UUID, hex, content size, percent-encode)
└─ error.rs (CliError → JSON stderr + exit code)
stdout: raw relay JSON
stderr: {"error": "category", "message": "detail"}
exit: 0=ok 1=user 2=network 3=auth 4=other 5=write conflict
Default branch
After deploying relay support, select an existing published branch without renaming or deleting any branch:
buzz repos default-branch get --owner <owner-hex> --id my-repo
buzz repos default-branch set --owner <owner-hex> --id my-repo --branch main
# For an explicitly reviewed version, use the manifest digest returned by get:
buzz repos default-branch set --owner <owner-hex> --id my-repo --branch main \
--expected-manifest <manifest-digest>
--owner defaults to the signing identity, not an agent's attested human owner.
set without --expected-manifest reads the current version first. Success
returns branch, head, manifest and changed; get omits changed.
A stale version returns conflict (exit 5). Ambiguous write failures return
delivery_unknown with retryable:false and the original digest: read before
retrying, and do not blindly re-run against a newly fetched version.
The signer must be a current channel member and a repository manager, directly or through an unrestricted, valid NIP-OA owner attestation; permission to push is not permission to change the default. See the protocol and authorization contract.