mirror of
https://github.com/block/buzz.git
synced 2026-11-13 12:51:34 +00:00
> Replaces #8768, which was opened from a fork. ## Summary This change adds two things to `buzz-cli`. `buzz users set-status` now accepts `--expires`. The status then clears itself at that time. `buzz users status` is a new command. It reads the visible status of one or more users. ## Design `--expires` takes a unix timestamp or a duration. A duration is a number with a unit: `s`, `m`, `h`, `d`, or `w`. A bare number is a unix timestamp. The CLI had no duration parser to reuse. The only one is private to `buzz-workflow`. A small parser now lives in `users.rs`. The CLI rejects an expiry that is not in the future. It does this before it signs anything. The error text says that a bare number is a timestamp. `--expires` cannot combine with `--clear`. A status with no text and no emoji cannot have an expiry. The SDK rejects that shape. `buzz-sdk` has a new builder, `build_user_status_expiring`. It adds `["expiration", ts]` after the emoji tag. This is the tag order desktop uses. `build_user_status` now calls the new builder with no expiry. Its output is unchanged. Existing callers need no change. `users status` queries kind 30315 with `#d: general`. It picks the newest event for each author. A higher `created_at` wins. On a tie, the lower event id wins. Desktop uses the same rule. The CLI then drops a user when the newest event has no text and no emoji. It also drops a user when the status has expired. An expired status means `expiration <= now`. An older status never shows behind a newer cleared or expired one. The expiration tag is read like desktop's `parseInt`. A value such as `90abc` reads as 90. A value such as `soon` reads as no expiry. The output is an array: `[{pubkey, text, emoji, expires_at, updated_at}]`. `emoji` is an empty string when there is none. `expires_at` is `null` when the status does not expire. A user with no visible status has no row. `--pubkey` takes hex, npub, or an exact display name. It reuses the name resolver of `messages`. It is repeatable. The limit is 100, because each name costs one profile search. With no `--pubkey`, the command reads your own status. A relay response that is not valid JSON is an error. It is never an empty result. ## Testing What ran: - `cargo test -p buzz-cli -p buzz-sdk` passes. - `cargo clippy -p buzz-cli -p buzz-sdk --all-targets -- -D warnings` passes. - `cargo fmt --all --check` passes. - `just file-size-check` passes. The CLI tests use an axum fake relay. They call the production functions `publish_status` and `read_statuses`. These are the same functions that the commands call. The tests cover these cases: - One publish carries the right tags. - Bad expiries cause no publish. - Expired, cleared, and malformed statuses. - Newest-event choice and tie-break. - Hex, npub, and name input. - The 100-pubkey limit. - An invalid relay response. - A publish followed by a read. Interop evidence: I ran the real `buzz` binary against a local fake relay. No live relay was used. I fed the published event to desktop's own `parseUserStatusEvent` and `fetchUserStatusLookup` with node. Desktop showed the same text, emoji, and `expiresAt` that `buzz users status` printed. The check printed `INTEROP OK`. Mutation checks: I broke each guard once. Each break made a test fail. The guards were: - the expiry comparison, `<=` changed to `<` - the future-expiry check - the `d=general` filter - the newest-first order - the tie-break - the 100-pubkey limit - the author de-duplication - the invalid-response error - the hide-cleared-status check - the duration overflow check - the SDK expiration tag - the SDK empty-status check I restored each guard after the check. One note on process: I wrote the code and the tests in the same pass. I did not write the tests first. The mutation checks cover the gap. ## Manual test steps Use a test identity on a relay that you may write to. Set `BUZZ_RELAY_URL` and `BUZZ_PRIVATE_KEY` first. 1. Set a status that expires in two minutes. ```bash buzz users set-status --text "testing expiry" --emoji "🧪" --expires 2m ``` Success: the output has `"accepted":true`. 2. Read the status. ```bash buzz users status ``` Success: the output is one row. `text` is `testing expiry`. `emoji` is `🧪`. `expires_at` is about two minutes from now. 3. Open the desktop app as the same user. Success: your status shows on your profile. 4. Wait for the expiry. Read again. ```bash buzz users status ``` Success: the output is `[]`. Desktop also hides the status. 5. Try a bad expiry. ```bash buzz users set-status --text x --expires 3600 ``` Success: exit code 1. The error says that the value must be in the future. Nothing is published. 6. Try the clear conflict. ```bash buzz users set-status --clear --expires 1h ``` Success: exit code 1. The error says `--clear` cannot be used with `--expires`. 7. Read another user. ```bash buzz users status --pubkey <hex|npub|name> ``` Success: you get that user's visible status. You get `[]` if they have none. 🤖 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>
259 lines
11 KiB
Markdown
259 lines
11 KiB
Markdown
# Buzz CLI
|
||
|
||
Agent-first command-line interface for Buzz relay. JSON in, JSON out.
|
||
|
||
## Install
|
||
|
||
```bash
|
||
cargo install --path crates/buzz-cli
|
||
```
|
||
|
||
## Authentication
|
||
|
||
| Env Var | Mode | Use Case |
|
||
|---------|------|----------|
|
||
| `BUZZ_PRIVATE_KEY` | NIP-98 Schnorr signature | Agents with a keypair |
|
||
|
||
```bash
|
||
# 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.
|
||
|
||
```bash
|
||
# 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 --text "lunch" --emoji "🥪" --expires 1h # clears itself; also a unix time
|
||
buzz users set-status --clear # remove your status
|
||
buzz users status # your visible status: [{pubkey, text, emoji, expires_at, updated_at}]
|
||
buzz users status --pubkey <hex|npub|name> # repeatable, max 100; expired or cleared users are omitted
|
||
|
||
# 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; `--expires` takes a unix time or `30m`/`2h`/`1d`/`1w` |
|
||
| | `status` | Read NIP-38 statuses (default: yours); expired or cleared are omitted |
|
||
| `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:
|
||
|
||
```bash
|
||
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](../../docs/git-on-object-storage.md#default-branch-management).
|