mirror of
https://github.com/block/buzz.git
synced 2026-11-13 21:01:53 +00:00
## Summary `buzz messages search` returns at most the newest 100 matches after `--since` and had no end time. So an author-only search could never reach older messages. This adds two flags to page back in time: - `--before <unix ts>` — sent to the relay as the filter's `until` (inclusive). - `--before-id <event id>` — requires `--before`. Sends the relay's existing `before_id` cursor, so events from the same second are neither repeated nor skipped. This is the same `(until, before_id)` cursor that `social notes --before/--before-id` already uses. Without the new flags, the filter and output are unchanged. ### How to page 1. Run `buzz messages search --author <key> --since <ts> --before <now> --limit 100`. 2. For the next page, pass the last result's `created_at` as `--before` and its `id` as `--before-id`. 3. Stop when a page comes back empty. Results are newest first, and events from the same second are ordered by ascending id. That matches the relay's cursor order. ### `--query` rule `--before-id` with `--query` is a usage error (exit 1). Full-text results are ranked by relevance, not time, and the relay's search path does not apply `before_id`. `--before` on its own still works with `--query` as a time bound. No relay change was needed: the relay's non-search query path already honors `until` and `before_id`. ## Tests - New unit test for the filter the command builds. It covers: no flags (unchanged filter), `--before` mapping to `until`, `--before-id` lowercased, `--before` with `--query`, and the usage errors (`--before-id` with `--query`, without `--before`, malformed id). Removing the `before_id` line makes it fail. - New clap test: `--before`/`--before-id` parse, and `--before-id` alone is rejected. ## Checks run - `cargo fmt --all -- --check` - `cargo clippy -p buzz-cli --all-targets -- -D warnings` - `cargo test -p buzz-cli` (686 passed) - Ran the built CLI against a live relay. Paged an author-only search back across several full pages: no overlaps at page boundaries, and the union matched an independent time-sliced query exactly (no gaps). A manual check confirmed the relay applies `before_id` at a shared timestamp. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Tim Mellor <117199716+tmellor-block@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
360 lines
16 KiB
Markdown
360 lines
16 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
|
||
```
|
||
|
||
### Which credentials are used
|
||
|
||
The CLI uses the first of these that is configured:
|
||
|
||
1. **An explicit key** (`--private-key` or `BUZZ_PRIVATE_KEY`), with the
|
||
headless NIP-FI settings below when they are set.
|
||
2. **The Buzz desktop** (macOS and Linux), through a token it gave this
|
||
process or one saved by `buzz auth desktop pair`. The desktop signs and
|
||
supplies NIP-FI evidence itself; headless NIP-FI settings are ignored, with
|
||
a warning naming them.
|
||
|
||
With neither, commands fail with an `auth_error`.
|
||
|
||
### Federated identity (NIP-FI)
|
||
|
||
A relay that enforces federated identity also needs an identity assertion on
|
||
every protected request (`docs/nips/NIP-FI.md`). Give the CLI your NIP-FA
|
||
adapter session and it requests one; the adapter mints it
|
||
(`docs/nips/NIP-FA.md` §Relay assertion). The CLI caches the assertion for the
|
||
process, requests a new one shortly before it expires, and sends it as
|
||
`Nostr-Federated-Identity` on HTTP requests and the WebSocket upgrade.
|
||
|
||
The session is a human sign-in for your own key: the adapter issues
|
||
assertions only for the key bound to the signed-in user. It is not an agent
|
||
grant, and it cannot authorize an agent key.
|
||
|
||
```bash
|
||
export BUZZ_PRIVATE_KEY="nsec1..."
|
||
export BUZZ_FEDERATED_IDENTITY_ADAPTER="https://adapter.example"
|
||
export BUZZ_FEDERATED_IDENTITY_TOKEN="..." # your adapter session token; env only, never a flag
|
||
buzz channels list
|
||
```
|
||
|
||
The token goes only to the adapter, never through a redirect, and never into
|
||
output. Adapter outages (network errors, 429, 5xx) are retried a bounded number
|
||
of times and then fail as a retryable `adapter_error` before anything reaches
|
||
the relay; an adapter refusal is an `auth_error` and is not retried. On relays without NIP-FI, leave both unset: nothing changes. An
|
||
enforcing relay's `401` without them is an `auth_error` naming both settings.
|
||
|
||
## 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 search --author <pubkey|npub|name> --before <last created_at> --before-id <last id> --limit 100 # next older page
|
||
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"
|
||
|
||
# Sidebar sections (your own sidebar, synced with Buzz desktop)
|
||
buzz sidebar sections create --name "Engineering" --icon "🛠"
|
||
buzz sidebar sections move --section "Engineering" --channel <uuid> --channel <uuid>
|
||
buzz sidebar sections list
|
||
|
||
# Sidebar sort modes (alpha or recent per group)
|
||
buzz sidebar sort set --group dms --mode recent
|
||
buzz sidebar sort set --group "Engineering" --mode recent
|
||
buzz sidebar sort reset --group section:<section-id>
|
||
buzz sidebar sort list
|
||
|
||
# Sidebar stars, mutes, and project pins
|
||
buzz sidebar stars add --channel <uuid> --channel <uuid>
|
||
buzz sidebar mutes remove --channel <uuid>
|
||
buzz sidebar projects add --project 30621:<owner-pubkey>:<d-tag>
|
||
buzz sidebar stars list
|
||
|
||
# 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'
|
||
```
|
||
|
||
`sidebar sections` edits the signing identity's own sidebar. Each command is
|
||
one read and one publish, so a multi-channel `move` lands together. `--section`
|
||
takes a section id or a unique section name. A repeated command prints
|
||
`"message": "unchanged"` and publishes nothing. Concurrent edits from another
|
||
CLI run or another client can overwrite each other, and the last publish wins.
|
||
|
||
`sidebar stars`, `sidebar mutes`, and `sidebar projects` edit the other synced
|
||
sidebar lists the same way: one head read and one publish per command, and
|
||
`"message": "unchanged"` when nothing changes. `remove` keeps a `false` entry
|
||
(as desktop does) so the change syncs; removing an id that is not set changes
|
||
nothing. Stars and mutes keep the newest 500 entries; a command that would
|
||
evict its own ids fails. Project addresses are `30621:<owner-pubkey>:<d-tag>`.
|
||
Pass the same ids in any case; channel UUIDs and project owners are stored in
|
||
lowercase. If the relay reports a newer update (exit 5), run the command again.
|
||
|
||
`sidebar sort` sets how one sidebar group orders its channels: `alpha` (name,
|
||
the default) or `recent` (last message, newest first). `--group` takes
|
||
`starred`, `channels`, `forums`, `dms`, `section:<id>` (an exact live id), or a
|
||
section name (or bare id). A fixed group name wins over a section with the same name. `sort list`
|
||
prints `{"default": "alpha", "groups": {<key>: <mode>}}` with only the groups
|
||
that have a mode set. `sort reset` writes a `null` register that returns the
|
||
group to the default; resetting a group with no register changes nothing. A
|
||
write is one sort-head read and one publish; a section name also reads your
|
||
sections. Desktop keeps a deleted section's `section:<id>` mode, and so does the
|
||
CLI: it still shows in `sort list`, `sort set` refuses it, and `sort reset
|
||
--group section:<id>` clears it. The sidebar keeps at most 104 sort modes
|
||
(stale keys count), and a command that would pass that fails.
|
||
|
||
`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 |
|
||
| `sidebar` | `sections list` | List your sections and their channels |
|
||
| | `sections create` | Create a section at the end of your sidebar |
|
||
| | `sections update` | Rename a section or change its icon |
|
||
| | `sections delete` | Delete a section |
|
||
| | `sections reorder` | Put sections in order |
|
||
| | `sections move` | Move one or more channels into a section |
|
||
| | `sections unassign` | Return channels to the default channel list |
|
||
| | `sort list` | List the groups with a sort mode |
|
||
| | `sort set` | Sort a group by `alpha` or `recent` |
|
||
| | `sort reset` | Return a group to the default sort |
|
||
| | `stars list` / `add` / `remove` | List, star, or unstar channels |
|
||
| | `mutes list` / `add` / `remove` | List, mute, or unmute channels |
|
||
| | `projects list` / `add` / `remove` | List, pin, or unpin sidebar projects |
|
||
| `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)
|
||
│
|
||
├─ credentials.rs (identity, signer, NIP-FI evidence; every request
|
||
│ is authorized through client.rs `authorize`)
|
||
├─ federated_identity.rs (NIP-FA adapter session → NIP-FI assertion)
|
||
├─ 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).
|