buzz-cli: el workspace desde la terminal
buzz-cli: el workspace desde la terminal
Hasta aquí has usado Buzz con la aplicación de escritorio. Este capítulo cambia
de superficie: buzz es el cliente de línea de comandos del workspace, y su
README lo declara en la primera línea —“Agent-first command-line interface for
Buzz relay. JSON in, JSON out.” Fíjate en el orden: agent-first, no
human-first. El CLI no está pensado para que tú lo leas cómodamente, sino para
que un programa lo invoque, lea la salida como JSON y decida qué hacer. Tú
también lo vas a usar, y mucho, pero cada decisión de diseño —salida JSON
siempre, errores en stderr, códigos de salida numéricos y estables— existe
porque al otro lado hay una máquina.
Instalación
Desde la raíz del repositorio de Buzz:
cargo install --path crates/buzz-cli
El binario resultante se llama buzz. Si prefieres compilar sin instalar,
cargo build --release -p buzz-cli lo deja en ./target/release/buzz. Un
detalle útil: el mismo CLI viaja embebido dentro del binario buzz-dev-mcp
como multicall, y si ese proceso se invoca con argv[0] igual a buzz
ejecuta el CLI completo. Por eso un agente del harness tiene buzz en su
PATH sin instalarlo aparte.
Autenticación: tu clave privada es la identidad
No hay buzz login. No hay buzz auth. No hay tokens, ni sesiones, ni archivo
de configuración con credenciales. El código lo dice literalmente: “Auth:
private key is required for all relay operations. The keypair IS the identity —
no tokens, no other auth.” Las peticiones se firman con NIP-98 (firma
Schnorr sobre la petición HTTP) usando tu clave privada Nostr, la misma del
capítulo Tu identidad: claves, perfil y dispositivos.
Configuración completa
Cuatro perillas; los flags sobrescriben las variables de entorno:
| Flag | Variable de entorno | Valor | Default |
|---|---|---|---|
--relay | BUZZ_RELAY_URL | URL base del relay, http:// o https:// | http://localhost:3000 |
--private-key | BUZZ_PRIVATE_KEY | hex de 64 caracteres o nsec1… | ninguno, obligatorio |
--auth-tag | BUZZ_AUTH_TAG | tag NIP-OA en JSON | ninguno, opcional |
--format | (sin variable) | json o compact | json |
Arranque típico:
export BUZZ_RELAY_URL="https://relay.example.com"
export BUZZ_PRIVATE_KEY="nsec1..."
buzz channels list
El valor de --private-key y --auth-tag se oculta en la ayuda de clap, así
que un buzz --help no filtra tu clave en una grabación de terminal.
Si falta la clave, el CLI no adivina ni te pregunta: emite
{"error":"auth_error","message":"auth error: BUZZ_PRIVATE_KEY is required (use --private-key or set env var)","retryable":false}
y sale con código 3. Si la clave existe pero no parsea, el mensaje es
invalid BUZZ_PRIVATE_KEY: … con categoría key_error y también código 3.
La única excepción es el grupo pack, que se despacha antes de exigir la
clave: pack validate e inspect funcionan sin credenciales y sin red.
BUZZ_AUTH_TAG: atestación del dueño
BUZZ_AUTH_TAG es un tag NIP-OA que permite a un proceso firmar con su propia
clave demostrando que un dueño humano lo respalda. Su forma canónica es un array
JSON de cuatro campos, ["auth","<owner-hex>","<conditions>","<sig>"]. El CLI
acepta además la forma abreviada sin comillas [auth,<hex>,,<hex>] y la
normaliza —una concesión para archivos .env escritos a mano—, pero solo cuando
hay exactamente cuatro campos y ninguno contiene comillas. Luego verifica
criptográficamente la firma contra tu pubkey y, si no cuadra, falla con
BUZZ_AUTH_TAG verification failed for pubkey <hex>: ….
Como usuario normal no necesitas esta variable: solo la exigen agents draft-create y agents draft-update, que fallan con
agent draft requests require BUZZ_AUTH_TAG si no está. Configurar el
relay para aceptar esta vía es tarea de operador: ver el
curso de administrador.
El contrato: JSON in, JSON out
Esta es la parte que hay que memorizar; todo lo demás se consulta con --help.
flowchart TD
A["buzz grupo subcomando --flags"] --> B["clap: parseo de argumentos"]
B -->|"argumento inválido"| E1["stderr JSON, salida 1"]
B --> C["commands: handler del subcomando"]
C --> D["client.rs: HTTP firmado NIP-98"]
D --> R["Buzz Relay"]
R -->|"2xx"| OK["stdout JSON, salida 0"]
R -->|"401 o 403"| E3["stderr JSON, salida 3"]
R -->|"otro status"| E2["stderr JSON, salida 2"]
R -->|"cabeza dominada"| E5["stderr JSON, salida 5"]
Tres reglas: toda salida útil va a stdout como JSON —array para las lecturas,
objeto para las escrituras—; todo error va a stderr como una sola línea JSON;
y el código de salida clasifica el fallo.
Códigos de salida exactos
| Código | Significado | Cuándo aparece |
|---|---|---|
| 0 | ok | éxito; también --help y --version |
| 1 | error de usuario | argumento o flag inválido, UUID mal formado, recurso no encontrado |
| 2 | red o relay | fallo de transporte, o el relay devolvió un status distinto de 401/403; también entrega desconocida |
| 3 | autenticación | falta la clave, la clave no parsea, o el relay respondió 401 o 403 |
| 4 | otro | fallo inesperado |
| 5 | conflicto de escritura | tu evento quedó dominado por una cabeza NIP-33 más nueva |
No existen otros códigos: un case $? sobre esos seis valores cubre el
universo completo.
Forma del error y de la salida
El README muestra dos campos, pero el código siempre emite tres:
{"error": "<categoría>", "message": "<detalle>", "retryable": <bool>}. Las
categorías posibles son exactamente user_error, auth_error, relay_error,
network_error, key_error, conflict, not_found, delivery_unknown y
error.
El campo retryable es lo que hace que este contrato sirva para un bucle
automático. Vale true solo cuando el fallo es de transporte (conexión,
timeout, petición cortada, cuerpo o decodificación) o cuando el relay respondió
429, 502, 503 o 504. Un 400, 401, 403, 404 o 422 nunca lo es, y
delivery_unknown tampoco: la petición pudo haber llegado y ejecutado, así que
reintentar duplicaría la mutación.
Las lecturas devuelven un array JSON de eventos normalizados con los campos
id, pubkey, kind, content, created_at y tags, sin la firma. Las
escrituras devuelven {"event_id": "…", "accepted": true, "message": "…"}, y
las creaciones añaden el id de la entidad: channel_id, workflow_id…
El flag --format es global
Un tropiezo clásico: --format va antes del grupo, no después del
subcomando. buzz --format compact channels list es correcto;
buzz channels list --format compact es un error de parseo con salida 1.
compact reduce los campos para que un modelo escanee menos tokens: en
messages y feed deja {id, content, created_at}; en channels,
{channel_id, name}; en users, {pubkey, display_name}, y en la ruta de
búsqueda por nombre añade además owner_pubkey, owned_by_me y verification
cuando existen. Solo se propaga a messages, channels, users, feed y
moderation; el resto ignora el flag.
La convención del guion
Los flags que reciben contenido literal aceptan - para leer de stdin, y
los que nombran un archivo (--patch-file, --body-file) tratan - como
stdin y cualquier otro valor como ruta. Los topes son duros: 65 536 bytes
para contenido general y 61 440 bytes para diffs.
buzz messages send --channel <uuid> --content - < mensaje.md
cat notas.md | buzz canvas set --channel <uuid> --content -
Recorrido por los grupos de comandos
Hay exactamente 22 grupos, y un test del repositorio los congela para que no aparezcan ni desaparezcan por accidente:
agents, canvas, channels, dms, emoji, feed, issues, media,
mem, messages, moderation, notes, pack, patches, pr, projects,
reactions, repos, social, upload, users y workflows. Agrupados por
lo que hacen: conversación (messages, channels, reactions, emoji, dms,
canvas, feed), identidad (users, social, agents), conocimiento
(notes, mem, media, upload), git (repos, projects, patches, pr,
issues), automatización (workflows, pack) y operación (moderation).
messages — 8 subcomandos
El grupo que más vas a usar: send, send-diff, edit, delete, get,
thread, search y vote.
buzz messages send --channel <uuid> --content "Hola equipo"
buzz messages send --channel <uuid> --content "Respondo" --reply-to <event-id> --broadcast
buzz messages get --channel <uuid> --limit 20
buzz messages thread --channel <uuid> --event <event-id>
buzz messages search --author npub1... --query "arquitectura" --since 1783497600
buzz messages edit --event <event-id> --content "Texto corregido"
buzz messages delete --event <event-id>
Notas de precisión: send acepta --file repetible para adjuntar archivos (se
suben como tags imeta) y --mention repetible con hex o npub; --broadcast
publica además en la red Nostr abierta; --reply-to es el flag que crea hilo y
no existe --thread; get filtra con --limit, --before, --since y
--kinds (separados por comas, --kinds 1,1984); thread acepta --limit y
--depth-limit; search permite omitir --query si das --author, que puede
ser hex, npub o nombre visible; vote toma --direction up|down.
Para diffs, send-diff exige --diff, --repo y --commit, y acepta además
--file, --parent-commit, --source-branch, --target-branch, --pr,
--lang, --description y --reply-to:
buzz messages send-diff --channel <uuid> --diff - \
--repo https://github.com/org/repo --commit abc123 < cambio.patch
channels — 16 subcomandos
buzz channels list --visibility open --member
buzz channels search --query composer
buzz channels get --channel <uuid>
buzz channels create --name "mi-canal" --type stream --visibility open
buzz channels join --channel <uuid>
buzz channels topic --channel <uuid> --topic "Nuevo tema"
buzz channels members --channel <uuid>
El inventario completo: list, get, search, create, update, topic,
purpose, join, leave, archive, unarchive, delete, members,
add-member, remove-member y set-add-policy. Detalles que importan:
--type acepta stream o forum y --visibility acepta open o private,
ambos obligatorios salvo que uses --template, que
toma tipo, visibilidad, descripción, canvas y lista de agentes de una plantilla
local del escritorio. --ttl <SEGUNDOS> hace el canal efímero —el relay lo
archiva tras ese número de segundos sin mensajes nuevos— y update --no-ttl lo
vuelve permanente. list devuelve hasta 500 canales por defecto; search trae
hasta 1000 eventos de metadatos y acepta --exact e --include-archived.
add-member toma --role con valores owner, admin, member, guest o
bot, y set-add-policy --policy anyone|owner_only|nobody controla quién puede
añadirte a canales. No existen channels invite, rename ni pin.
reactions y emoji
buzz reactions add --event <event-id> --emoji "👍"
buzz reactions remove --event <event-id> --emoji "👍"
buzz reactions get --event <event-id> # lista las reacciones del mensaje
reactions add acepta --emoji-url para reaccionar con un emoji personalizado;
cuando lo usas, el contenido publicado pasa a ser :shortcode:. remove busca
tu propia reacción kind:7 sobre ese evento y publica el borrado.
El grupo emoji gestiona tu set personal con list, set, rm, export e
import:
buzz emoji set --shortcode party --url https://… # sin los dos puntos
buzz emoji export --scope workspace --file paleta.json
buzz emoji import --file paleta.json --dry-run
La paleta del workspace que devuelve emoji list es la unión de los sets de
todos los miembros: una vista calculada al leer, no un estado compartido.
import acepta --replace para sustituir tu set entero en vez de fusionar.
users — 5 subcomandos
buzz users get # tu propio perfil
buzz users get --pubkey <hex> --pubkey <hex> # lote, máximo 200
buzz users get --name Honey --owner me # nombre exacto entre tus agentes
buzz users set-profile --name "Ana" --about "Backend" --nip05 [email protected]
buzz users presence --pubkeys <hex>,<hex>
buzz users set-presence --status online
buzz users set-status --text "cabeza abajo en el CLI" --emoji "🚀"
buzz users set-status --clear
--owner requiere --name y acepta me, un hex o un npub: resuelve un agente
por nombre entre tus agentes gestionados. set-presence acepta online, away
u offline; en set-status, --clear es incompatible con --text y
--emoji. No existe buzz users list.
dms y canvas
buzz dms open --pubkey <hex> # 1 a 8 pubkeys, repitiendo el flag
buzz dms list --limit 20
buzz dms add-member --channel <uuid> --pubkey <hex>
buzz dms hide --channel <uuid>
buzz canvas get --channel <uuid>
buzz canvas set --channel <uuid> --content "# Bienvenida"
dms open rechaza más de 8 pubkeys y canvas set reemplaza el documento.
feed, notes y social
feed get es tu bandeja de actividad, un solo subcomando:
buzz feed get --types mentions,needs_action --since 1783497600 --limit 20.
Los tipos válidos son exactamente mentions, needs_action, activity y
agent_activity; cualquier otro da error de usuario con la lista completa en el
mensaje. El límite por defecto es 20, el tope duro 50, y los resultados llegan
ordenados por created_at descendente.
notes es la base de conocimiento del equipo con notas largas NIP-23:
buzz notes set --name decisiones-q3 --title "Decisiones Q3" --content - < doc.md
buzz notes get --name decisiones-q3 --content-only
buzz notes ls --author me --tag arquitectura
El upsert es idempotente por la pareja (tú, --name), el slug debe casar con
[a-z0-9._-] de 1 a 80 caracteres, --tag reemplaza los tags existentes,
published_at se preserva al editar y notes rm --name <slug> publica el
borrado. social cubre la superficie Nostr genérica: publish, set-contacts,
event, notes, contacts, set-list y list.
El bloque git: repos, projects, patches, pr, issues
Cinco grupos con capítulo propio, Git dentro de Buzz. Aquí solo el mapa:
| Grupo | Subcomandos |
|---|---|
repos | create, get, list, bind, protect list/set/remove |
projects | create, get, list, add-repo, remove-repo, update, delete |
patches | send, get, list, status |
pr | open, update, get, list, status |
issues | create, get, list, status |
buzz repos protect set --id my-repo --ref refs/heads/main \
--push admin --no-force-push --no-delete
Cuidado con dos cosas: protect set reemplaza todas las reglas del patrón
exacto, así que cualquier restricción omitida queda eliminada; y los estados de
issues status son open, resolved, closed, draft, distintos de los de
patches y pr, que usan merged.
media y upload
buzz upload file --file captura.png sube un archivo al almacén Blossom del
relay y devuelve un descriptor JSON legible. buzz media get, con una URL de
media del relay o un sha256[.ext], escribe los bytes crudos en stdout o en
el archivo que indiques con -o/--output.
mem — memoria de agente
Clave-valor persistente por agente, con control de concurrencia:
buzz mem ls --json
buzz mem get core
buzz mem set notas "contenido"
buzz mem patch notas --base-hash "$(buzz mem hash notas)" < cambio.patch
buzz mem rm notas
get imprime el valor crudo sin salto de línea final, para capturarlo en
una variable sin limpiarlo. hash da el SHA-256 que pasas como --base-hash a
patch: si el valor cambió desde entonces, el parche se rechaza.
--no-base-hash salta esa protección y es inseguro con ediciones concurrentes.
patch acepta --dry-run, y el slug core no se puede borrar.
pack — local, sin relay
buzz pack validate ./mi-pack escribe los diagnósticos ERROR: y WARN: en
stderr y en stdout deja Valid. o Valid (with warnings).; si hay errores
sale con código 1. buzz pack inspect ./mi-pack resume cada persona del pack:
display, descripción, modelo, proveedor, temperatura, tokens de contexto,
suscripciones, triggers, servidores MCP, skills y una vista previa del system
prompt. Ambos exigen un directorio existente y ninguno toca la red.
agents, workflows y moderation
agents tiene cinco subcomandos —draft-create, draft-update, archive,
unarchive, archived— y lo importante es qué no hacen: no crean agentes.
draft-create envía un borrador cifrado al Buzz Desktop del dueño y devuelve
"saved": false con el mensaje “Draft sent to Buzz Desktop for owner review.
Nothing changes until the owner saves it.” El alta la confirma una persona
—ver Agentes como compañeros—.
workflows tiene ocho: list, get, create, update, delete, trigger,
runs y approve. El YAML entra literal por --yaml o por stdin con -; no
existe flag de ruta. El detalle está en
Workflows en YAML, con la
advertencia de que runs hoy devuelve [] y que las compuertas de aprobación
todavía no se reanudan.
moderation tiene ocho —reports, resolve, ban, unban, timeout,
untimeout, restricted, audit— y requiere ser owner o admin de la
comunidad, que se selecciona por el host de --relay: eso ya es
curso de administrador.
Combinar con jq
Aquí es donde el contrato JSON paga. El propio README cierra con
buzz channels list | jq '.[].name'; a partir de ahí, encadenar es directo.
Captura el id de lo que creas y úsalo en el siguiente comando.
CHANNEL_ID=$(buzz channels create --name "test-cli" --type stream \
--visibility open | jq -r '.channel_id')
EVENT_ID=$(buzz messages send --channel "$CHANNEL_ID" \
--content "Propuesta de diseño" | jq -r '.event_id')
buzz messages send --channel "$CHANNEL_ID" --content "De acuerdo" \
--reply-to "$EVENT_ID"
buzz reactions add --event "$EVENT_ID" --emoji "👍"
Leer, buscar y navegar sin copiar UUIDs a mano:
# últimos cinco mensajes, una línea por mensaje
buzz --format compact messages get --channel "$CHANNEL_ID" --limit 5 \
| jq -r '.[] | "\(.created_at) \(.content)"'
buzz messages search --query "deploy" --limit 50 | jq -r '.[] | {id, pubkey, content}'
buzz channels search --query design --exact | jq -r '.[0].channel_id'
buzz feed get --types mentions,needs_action --limit 30 | jq -r '.[].content'
Un patrón de script que respeta el contrato de errores:
if out=$(buzz messages send --channel "$CHANNEL_ID" --content "informe" 2>err.json); then
echo "$out" | jq -r '.event_id'
else
code=$?; echo "fallo $code, reintentable=$(jq -r '.retryable' err.json)" >&2
exit "$code"
fi
Por qué está diseñado para llamadas de herramienta de un LLM
Un modelo que usa herramientas necesita tres cosas de una interfaz: salida
parseable sin heurísticas, fallo distinguible del éxito sin leer prosa, y
superficie estable entre versiones. buzz entrega las tres a propósito.
sequenceDiagram
participant LLM as Agente LLM
participant CLI as buzz
participant R as Relay
LLM->>CLI: buzz --format compact messages get --channel U --limit 20
CLI->>R: peticion HTTP firmada NIP-98
R-->>CLI: eventos JSON
CLI-->>LLM: stdout con array JSON, salida 0
LLM->>CLI: buzz messages send --channel U --content resumen
CLI-->>LLM: objeto con event_id, salida 0
Los puntos concretos:
- JSON siempre, en ambos canales. El modelo nunca interpreta tablas ni
prosa; y como los errores van a
stderr, unstdoutque no parsea significa algo distinto de un error de negocio. - Firmas eliminadas en las lecturas. Una firma Schnorr son 128 caracteres
hex que el modelo no puede usar y que cuestan tokens en cada evento. Y
--format compactexiste únicamente por esto: su doc-comment dice “Reduced fields for agent scanning”. - Los códigos de salida son una máquina de estados diminuta. Seis valores
con una acción obvia: 1 corrige la llamada, 2 reintenta con backoff, 3 pide
credenciales, 5 relee el estado antes de reescribir. Y
retryablecierra el bucle: el modelo no decide si un 503 es transitorio. - El inventario está congelado por tests, así que un prompt que enseña la superficie del CLI no se pudre en silencio. Y no hay estado local: cada invocación es independiente, dos llamadas concurrentes no se pisan.
Cómo se lo conectas a tu agente
En un agente gestionado por el harness ACP de Buzz no tienes que hacer nada: el
harness inyecta automáticamente BUZZ_RELAY_URL, BUZZ_PRIVATE_KEY y
BUZZ_AUTH_TAG en el entorno del subproceso, y buzz ya está en su PATH
gracias al multicall de buzz-dev-mcp.
Fuera de ese caso —un agente tuyo, un script, un bot propio— el procedimiento es:
- Dale su propio par de claves. Cada agente necesita el suyo. La
generación de claves y el alta como miembro del relay se hacen con
buzz-admin, no conbuzz: territorio de operador, ver el curso de administrador. - Exporta el entorno en el proceso del agente, no en tu shell interactivo:
BUZZ_RELAY_URLyBUZZ_PRIVATE_KEYcon la clave del agente, no la tuya. - Expón el shell como herramienta. Si tu agente ya ejecuta comandos, no hay
más trabajo:
buzzes un binario normal. - Enseña la superficie en el prompt: los grupos que quieres que use, los
seis códigos de salida y la forma del objeto de error. Y usa
--format compacten los ejemplos de lectura, porque el modelo copia la forma de los ejemplos; recuerda que el flag va antes del grupo.
Un detalle que quizá veas en los eventos que publica un agente: para dejar
constancia del origen de una operación git, el harness exporta
BUZZ_GIT_ORIGIN_CHANNEL_ID o BUZZ_GIT_ORIGIN_AGENT_NAME. La primera añade un
tag h con el canal público; la segunda, en conversaciones privadas, añade un
tag buzz-origin-agent solo con el nombre visible del agente y omite la
coordenada del canal. Es procedencia, no configuración: no las pones a mano.
Cosas que no existen y te van a tentar
El CLI se parece a otros que conoces, así que es fácil inventar comandos:
| Lo que escribirías | Lo que existe |
|---|---|
buzz login, logout, whoami, init, config | nada: la clave privada es la identidad |
buzz agents create/list/start/stop | draft-create, draft-update, archive, unarchive, archived |
buzz users list, mem list, notes list | users get --name …, mem ls, notes ls |
--json, --verbose, --quiet, --debug, --no-color | --format json|compact global |
channels create --private | --visibility private |
messages send --thread | --reply-to <event-id> |
workflows create --file wf.yaml | --yaml con el YAML literal, o - |
--json solo existe dentro de mem ls, -o/--output solo en media get y
--dry-run solo en emoji import y mem patch. Cuando dudes,
buzz <grupo> --help da el inventario exacto de tu versión.
Resumen
- Se instala con
cargo install --path crates/buzz-cli; el binario se llamabuzzy viaja embebido enbuzz-dev-mcpcomo multicall. - La autenticación es una sola cosa: tu clave privada Nostr en
BUZZ_PRIVATE_KEY, que firma cada petición con NIP-98. No hay login, tokens ni sesiones.BUZZ_AUTH_TAGes opcional y solo lo exigen los borradores de agente. La configuración completa son cuatro perillas —--relay,--private-key,--auth-tagy--format— y los flags ganan al entorno. - El contrato es JSON en
stdout, error JSON de una línea enstderrconerror,messageyretryable, y seis códigos: 0 ok, 1 error de usuario, 2 red o relay, 3 auth, 4 otro, 5 conflicto de escritura. - Hay 22 grupos congelados por tests; los del día a día son
messages,channels,reactions,emoji,users,dms,canvasyfeed. Las lecturas devuelven arrays de eventos sin firma, las escrituras{event_id, accepted, message}y las creaciones añaden el id de entidad. --format compactes global y va antes del grupo; solo afecta amessages,channels,users,feedymoderation. El guion-lee de stdin en los flags de contenido, con topes de 64 KiB y 60 KiB para diffs.buzz pack validateeinspectcorren sin clave y sin relay.- El harness ACP inyecta el entorno en los agentes gestionados; fuera de ahí,
cada agente necesita su propio par de claves, dado de alta con
buzz-adminpor un operador.
Siguiente: Workflows en YAML: automatizar sin salir del canal