Files
buzz/crates/buzz-cli
Blake McAnally 303157f53d feat(cli): send a thread message to the channel (#8778)
> Replaces #8769, which was opened from a fork.

## Summary

This change adds `buzz messages send --from-thread <root-event-id>`.
The flag is the CLI form of desktop "Send to channel".
It publishes a new top-level message in the channel.
The message carries the tag `["buzz:sent-from-thread", <root-id>,
<excerpt>]`.
The message has no NIP-10 reply tags.

## Design

Desktop puts the thread root id in the tag. It does not use the selected
message id.
Desktop publishes kind 9. The CLI does the same.
The CLI checks the root event before it publishes anything.
The root must exist. The root must have the same `h` tag as `--channel`.
The root must be a thread root. If the id belongs to a reply, the CLI
fails and prints the real root id.
A missing root exits with a not-found error. Other failures exit with a
usage error.
No event is published after a failed check.
The excerpt comes from the root content.
It is a Rust port of `summarizeThreadRoot` in `sentFromThread.ts`.
The port clips by code point, as desktop does. It breaks words by UTF-16
unit, as desktop does.
`--from-thread` conflicts with `--reply-to` and `--broadcast`. It works
only with kind 9.
The relay does not validate this tag. Desktop's Tauri code validates it
(`message_tags.rs`).
The CLI output follows those rules: 64 hex characters, an excerpt of at
most 64 characters, and no control characters.
The change adds no dependency and no SDK change. The CLI appends the tag
to the SDK `build_message` builder.
The README describes the flag. This PR does not change `desktop/`
(deprecated; see #8840).

## Testing

What ran:
- `cargo test -p buzz-cli`: 508 passed.
- `cargo clippy -p buzz-cli --all-targets -- -D warnings`: clean.
- `cargo fmt --all --check`: clean.
- `just file-size-check`: passed.

Test coverage:
- One test calls `cmd_send_message` against a fake relay. It covers
success, a reply id, another channel, a missing root, an invalid id, and
a non-kind-9 send. The failure cases publish nothing. The success case
publishes one event with the tag and without `e` or `broadcast` tags.
- One test checks the clap conflicts.
- Unit tests cover the excerpt boundaries (63, 64, 65 characters, the
word-break threshold, and multi-byte and emoji text).
- One test compares the Rust excerpt with 639 vectors. Desktop's own
`summarizeThreadRoot` generated the vectors. The first run found an
ordering bug (desktop replaces each pattern over the whole text in
turn). The code now does the same.

Interop evidence:
- A node script runs the real `buzz` binary against a local fake relay.
It is not a live relay.
- The script passes the published tags to desktop's
`getSentFromThreadReference` and `getSentFromThreadRootId`.
- Desktop accepts the tag, returns the same root id, and returns the
same excerpt as `summarizeThreadRoot`.
- Output: `["buzz:sent-from-thread","abab…ab","Launch plan for the doc 😀
word word word word word word word…"]` then `INTEROP OK`.

Mutation checks. Each guard was broken once. A test failed each time.
Then the guard was restored.
- Channel check removed: success test fails.
- Reply-root check removed: success test fails.
- Kind check removed: success test fails.
- `--broadcast` conflict removed: clap test fails.
- Word-break threshold `>` changed to `>=`: three excerpt tests fail.
- UTF-16 break changed to code-point break: two excerpt tests fail.
- Tag not attached: success test fails.

## Manual test steps

Use a test channel. You need `BUZZ_RELAY_URL` and `BUZZ_PRIVATE_KEY`
set. Build with `cargo build -p buzz-cli`.

1. Post a root message. Copy its event id as `ROOT`.
`buzz messages send --channel <CHANNEL> --content "Launch plan:
[doc](https://example.com)"`
2. Send it to the channel.
`buzz messages send --channel <CHANNEL> --content "Decision: ship it"
--from-thread $ROOT`
Success: the output has `"accepted":true`. A new top-level message
appears in the channel.
3. Read it back.
   `buzz --format json messages get --channel <CHANNEL> --limit 1`
Success: the tags include `["buzz:sent-from-thread","<ROOT>","Launch
plan: doc"]`. There are no `e` tags.
4. Open the channel in desktop.
   Success: the message shows the "sent from thread" line for that root.
5. Try a reply id.
`buzz messages send --channel <CHANNEL> --content x --from-thread
<REPLY_ID>`
Success: exit code 1. The error names the root id. Nothing is published.
6. Try a root from another channel, and try `--reply-to` together with
`--from-thread`.
   Success: both fail and publish nothing.

🤖 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>
2026-11-05 17:28:19 -06:00
..

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 send --channel <uuid> --content "Decision: ship" --from-thread <root-event-id>   # desktop "Send to channel"
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 unread                      # unread counts from your synced read state (read-only)
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
unread Unread counts per channel (read-only)
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.