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

259 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).