buzz-cli: el workspace desde la terminal

Por: Artiko
buzznostrcliterminalagentesjqautomatizacion

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:

FlagVariable de entornoValorDefault
--relayBUZZ_RELAY_URLURL base del relay, http:// o https://http://localhost:3000
--private-keyBUZZ_PRIVATE_KEYhex de 64 caracteres o nsec1…ninguno, obligatorio
--auth-tagBUZZ_AUTH_TAGtag NIP-OA en JSONninguno, opcional
--format(sin variable)json o compactjson

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ódigoSignificadoCuándo aparece
0okéxito; también --help y --version
1error de usuarioargumento o flag inválido, UUID mal formado, recurso no encontrado
2red o relayfallo de transporte, o el relay devolvió un status distinto de 401/403; también entrega desconocida
3autenticaciónfalta la clave, la clave no parsea, o el relay respondió 401 o 403
4otrofallo inesperado
5conflicto de escrituratu 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:

GrupoSubcomandos
reposcreate, get, list, bind, protect list/set/remove
projectscreate, get, list, add-repo, remove-repo, update, delete
patchessend, get, list, status
propen, update, get, list, status
issuescreate, 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, un stdout que 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 compact existe ú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 retryable cierra 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:

  1. 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 con buzz: territorio de operador, ver el curso de administrador.
  2. Exporta el entorno en el proceso del agente, no en tu shell interactivo: BUZZ_RELAY_URL y BUZZ_PRIVATE_KEY con la clave del agente, no la tuya.
  3. Expón el shell como herramienta. Si tu agente ya ejecuta comandos, no hay más trabajo: buzz es un binario normal.
  4. 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 compact en 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íasLo que existe
buzz login, logout, whoami, init, confignada: la clave privada es la identidad
buzz agents create/list/start/stopdraft-create, draft-update, archive, unarchive, archived
buzz users list, mem list, notes listusers 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 llama buzz y viaja embebido en buzz-dev-mcp como 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_TAG es opcional y solo lo exigen los borradores de agente. La configuración completa son cuatro perillas —--relay, --private-key, --auth-tag y --format— y los flags ganan al entorno.
  • El contrato es JSON en stdout, error JSON de una línea en stderr con error, message y retryable, 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, canvas y feed. Las lecturas devuelven arrays de eventos sin firma, las escrituras {event_id, accepted, message} y las creaciones añaden el id de entidad.
  • --format compact es global y va antes del grupo; solo afecta a messages, channels, users, feed y moderation. El guion - lee de stdin en los flags de contenido, con topes de 64 KiB y 60 KiB para diffs. buzz pack validate e inspect corren 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-admin por un operador.

Siguiente: Workflows en YAML: automatizar sin salir del canal