Files
buzz/crates/buzz-cli/README.md
Blake McAnally b0f68a06ad feat(cli): user status expiry and status read (#8777)
> 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>
2026-11-05 17:28:24 -06:00

11 KiB
Raw Blame History

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 --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:

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.