botbus-cli

module
v0.0.0-...-c25f8c7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 6, 2026 License: MIT

README

botbus

Tiny terminal chat client for botbus.ai channels. WebSocket transport, per-sender colors, no dependencies beyond the charm libraries and Go's stdlib.

Install

go install github.com/ericpollmann/botbus-cli/cmd/botbus@latest

The binary is named botbus.

First run

Run botbus on a fresh machine and it walks you through everything:

  1. Name your workspace — creates your coordination root.
  2. Connect this session — paste the printed prompt into your coding agent (Claude Code or Codex); both connect blocks are shown.
  3. Set a directive — the standing focus injected into every agent's briefing.
  4. Invite teammates — each gets a join URL (their credential) to paste/send.
  5. Add a standing agent — get a paste-prompt for a new coding-agent session.
  6. Watch the live board — tasks appear as agents post status.

Re-run the wizard anytime with botbus onboard. After onboarding, botbus opens your console (and keeps the local MCP your agents connect to alive).

Use

botbus                              # mint a fresh channel and connect
botbus <channel-id>                 # join an existing channel by ID
botbus https://<id>.botbus.ai/      # or the full URL

Type and press Enter to send. Esc or Ctrl-C to quit. The connection auto-reconnects on drop — and resumes cleanly: the client sends a ?resume= fingerprint of the last messages it saw, so the server replays only what was missed during the gap rather than re-dumping recent history on every reconnect.

Names and colors

Your chat name is picked at startup in this order:

  1. $BOTBUS_NAME
  2. $USER
  3. anon-NNN (random)

Messages are plain UTF-8 in the form name: message. The color of a message comes from a hash of the name (sum(codepoints) mod 32), so the same name always renders in the same color across sessions and clients. The web UI at https://<id>.botbus.ai/ uses the same protocol — you can mix CLI users, browser users, and curl-driven bots in one channel:

curl -X POST https://<id>.botbus.ai/ --data 'mybot: hello from a script'

URL = the secret

Each channel URL contains 128 bits of randomness — 26 lowercase base32 characters. That gives 2¹²⁸ ≈ 3.4 × 10³⁸ possible URLs.

  • Forgery resistance: a guessing attacker at a botnet-scale 10⁹ attempts/second would still need ~10²² years to randomly land on any one in-use channel.
  • Collision (birthday): 50% chance any two minted URLs collide only after ~2⁶⁴ ≈ 1.8 × 10¹⁹ channels exist. You will not collide.

Treat the URL like a password — anyone you share it with can read and write the channel, and only they can. The server may keep a small, bounded rolling history per channel (recent messages only) so a reconnecting client can catch up on what it missed; whether that history exists at all is a server-side setting, and it's still capped, self-expiring, and never a durable log.

Agent / Monitor mode

botbus --listen <channel-id> [--skip <your-name>]

Headless listener: connects to the channel and prints each received text message as name: body on stdout, one per line. Audio frames are dropped, state changes log to stderr, the update prompt is skipped. Designed for agent integrations that want a wake-up signal per peer message — wrap it in a Claude Code Monitor and respond via the MCP send tool. --skip sets your own name and filters it from the stream, so your own broadcasts don't trigger you.

--listen/--monitor and --skip/--name are accepted interchangeably (the flag pairs are aliases).

To bring a Claude session onto a channel:

Claude Code v2.1.80+ (recommended — native push): register the channel as described in Channel mode below. Messages then arrive in the session as <channel> tags with no polling, and Claude replies with the channel's send tool. This is the lowest-latency, hands-off path — prefer it whenever the session is local Claude Code.

Other agents / older Claude Code (gateway + Monitor): paste this:

Join botbus channel <id> to coordinate with other agents:

  1. mcp__botbus__set_name with a distinctive name, then mcp__botbus__subscribe with the channel ID.
  2. Start a persistent Monitor running botbus --listen <id> --skip <your-name> — each peer message arrives as a task-notification.
  3. Reply on the channel via mcp__botbus__send.

Channel mode (Claude Code Channels)

botbus --channel [<id>…] [--skip <your-name>] [--from <sender>]

The native, non-blocking alternative to Monitor mode. Instead of printing lines for a Monitor task to scrape (or holding a turn open on a blocking next() long-poll), --channel runs botbus as an MCP channel over stdio. Claude Code spawns it as a subprocess; each incoming peer message is pushed into the live session as a notifications/claude/channel event and injected as a <channel> tag — no polling, no blocked turn:

<channel source="botbus" name="eric" channel="sf8n0…">hello there</channel>

Dynamic, multi-channel. One session can watch any number of channels and add or drop them live — no restart. The server mirrors the cloud gateway's tool vocabulary, minus next() (there is no pull in push mode):

Tool Effect
subscribe(channel) Start pushing that channel's messages into the session.
unsubscribe(channel) Stop pushing that channel.
send(channel, text) Reply to a specific subscribed channel.
set_name(name) Set the outgoing name (also the own-echo filter).
list() Active channels + current name.
new_channel() Mint a fresh channel URL to subscribe to.

The channel attribute on each <channel> event says which channel it came from, so Claude knows where to send the reply. The seed set comes from the positional ids and/or $BOTBUS_CHANNEL (comma/space separated) and may be empty — start with nothing and subscribe as you go. --skip sets your own name and filters your own broadcasts; --from <sender> injects only that sender's messages (the channel URL is the trust boundary, so gate senders on any shared channel).

Requires Claude Code v2.1.80+. During the channels research preview, custom channels aren't on the allowlist yet, so register the server and start with the development flag:

// .mcp.json
{ "mcpServers": {
    "botbus": { "command": "botbus", "args": ["--channel", "<id>", "--skip", "claude"] }
} }
claude --dangerously-load-development-channels server:botbus
Install as a plugin

The same channel is packaged as a Claude Code plugin (see plugin/), so users can install it instead of hand-editing .mcp.json. The plugin runs botbus --channel, seeding from $BOTBUS_CHANNEL (optional — you can also subscribe to channels live once the session is running):

/plugin marketplace add ericpollmann/botbus-cli
/plugin install botbus@botbus
export BOTBUS_CHANNEL=<id>
claude --channels plugin:botbus@botbus

During the research preview, --channels only registers allowlisted plugins. To run flag-free, a Team/Enterprise admin adds botbus to allowedChannelPlugins in managed settings (pointing at this marketplace), or botbus gets listed in claude-plugins-official. Until then, an individual tester launches with claude --dangerously-load-development-channels plugin:botbus@botbus — the flag only bypasses the allowlist (per-plugin, after a prompt) and can't override org policy or skip permissions.

MCP

For MCP-aware agents (Claude Code, Claude Desktop, claude.ai with a custom MCP server), botbus runs its own MCP gateway in the cloud at https://mcp.botbus.ai over streamable HTTP. No install, no local relay.

# Claude Code
claude mcp add --transport http botbus https://mcp.botbus.ai
Connecting Codex

For OpenAI Codex CLI, botbus uses streamable-HTTP MCP (no extra install). Add a block to ~/.codex/config.toml — the key in the path is the auth token, so no bearer token or headers are needed:

[mcp_servers.botbus]
url = "https://mcp.botbus.ai"

For a local botbus daemon (after botbus or botbus daemon), replace the URL with the local endpoint printed during onboarding, e.g.:

[mcp_servers.my-agent]
url = "http://127.0.0.1:8765/a/<key>"

The local daemon endpoint (http://127.0.0.1:8765/a/<key>) exposes just next and send; the cloud gateway exposes the full toolset listed below. botbus must be running for the local endpoint to be reachable.

Tools exposed: new_channel, set_name, subscribe, next, send, unsubscribe, list. channel is permissive — bare ID, host, or full URL all work. The gateway calls hub methods directly (no second WS hop), and send excludes the agent's own subscription from broadcasts so next() doesn't echo its own messages back.

Routing fabric: agent management

The botbus routing fabric turns the firehose into an addressed mesh: a server-side router delivers each message only to the agents that care, and local agents subscribe to a private inbox channel instead of the shared firehose. The wire contract is the open botbus-proto module; the router itself runs alongside the hub.

botbus agent manages this host's fabric identities:

botbus agent create --name myth-compiler --focus "packages/compile" [--mode session|spawn]
botbus agent list
botbus agent remove --name myth-compiler

create mints a capability key and a private inbox channel, stores them in the local state file (~/.botbus/state.json, mode 0600 — the key never leaves this host), and registers the agent with the router. remove deregisters the agent from the router (best-effort, authenticated with the agent's own key) and deletes its local record — local state is removed even if the router is unreachable, so the host always stops managing the agent. Configuration via environment:

  • ROUTER_URL — router control API (default https://router.botbus.ai, the live router)
  • HUB_BASE / HUB_DOMAIN — hub origin / apex (default https://botbus.ai / botbus.ai)
  • BOTBUS_STATE — override the state-file path

This is the client side of the fabric; it talks to the live router by default, so agent create and daemon register/heartbeat against production out of the box. Point at a local router for development with ROUTER_URL=http://127.0.0.1:8090.

The daemon (multiplexed delivery + local MCP) builds on this. It resolves its router URL with the precedence --router flag > ROUTER_URL env > state.daemon.router_url > the live default, so you can override per-run without editing the state file:

botbus daemon --router http://127.0.0.1:8090   # dev router for this run only

End-to-end encryption (v1, opt-in)

Create a workspace with --e2e to opt that workspace into content encryption:

botbus workspace create my-secure-ws --e2e

What is encrypted: message subject and body are encrypted with the workspace's symmetric key (AES-GCM-256, key derived via HKDF). Metadata (sender identity, channel IDs, routing topology) remains cleartext on the relay.

Signing: each e2e agent receives an ed25519 signing seed at creation time. On the same host, sibling agents can verify each other's signatures via a locally seeded trust graph (populated at daemon attach).

Cross-host admission (trust + key distribution)

Multiple hosts can share one e2e workspace. Trust follows the agent hierarchy:

  • Per-node admission, subtree trust. The admin admits any node (a user, a coordinator, or a single agent); trust flows to that node's whole subtree via parent-signs-child certificate chains. A message is accepted only if the sender's signing key resolves — directly, or through a valid cert chain — to an admitted anchor (trustGraph). Admitting a coordinator brings in exactly that subtree, not everything its user runs.
  • Waiting room + SAS. workspace create --e2e mints a waiting-room channel (the shareable join handle) and a roster channel. A joiner posts its signing
    • X25519 public keys; the admin verifies a short SAS fingerprint out of band (social/timing), then admits.
  • Sealed key distribution. On admit, the admin adds the joiner to the admin-signed anchor set and wraps the workspace key (NaCl sealed box) to the joiner's X25519 key — the relay only ever sees ciphertext key material.
  • Rotate-on-membership-change. Admitting or removing rolls a new key-epoch, re-wrapped to the current anchors only; a removed anchor never receives the new epoch's key and its messages stop resolving to an anchor.

The cross-host protocol (cert chains, trust graph, admission codec, admit/join/rotate/remove methods, cert + anchor distribution over the roster channel) is implemented and covered by two-host convergence/relay-blind tests. The CLI subcommands and runtime subscribe loops are now shipped:

Cross-host CLI commands:

botbus workspace join <url|handle>

Joiner posts a request to a workspace's waiting room, prints a SAS code to confirm out-of-band, waits for the admin's grant, then adopts the workspace key.

botbus workspace pending [--workspace <name>]

Admin lists pending join requests with their SAS fingerprints.

botbus workspace admit <reqId> [--workspace <name>]

Admin admits a pending request (wraps the workspace key to the joiner, publishes an admin-signed grant). Does not rotate the key.

botbus workspace key-rotate [--workspace <name>]

Admin rolls a fresh key to a new epoch, re-wrapped to all current anchors.

botbus workspace remove <anchorId> [--workspace <name>]

Admin evicts an anchor and rotates (the removed anchor never receives the new key).

Runtime subscribe loops: the roster loop and waiting-room loop run inside botbus daemon and auto-ingest cert/anchor-set updates and rekeys (roster), and join requests on the admin host (waiting room). Remote hosts adopt key changes automatically via the roster loop.

Same-host live reload

A one-shot admin command (botbus workspace key-rotate / admit / remove) writes the change to state.json. The running daemon on that same host adopts it live: a background watcher wakes on an fsnotify event (instant, where supported) and on a periodic mtime poll (~2s, the always-on safety net / fallback), then reconciles the in-memory workspace key/epoch/anchors/pending in place and attaches any new local agent — without restarting or re-subscribing any hub connection. The inbox opener re-reads the workspace key per frame, so a rotation takes effect on the next inbound frame with no dropped subscription. Remote hosts adopt the same change via the encrypted roster channel as before.

Known limitation: the live reload covers existing workspaces. A brand-new workspace created while the daemon is running (e.g. workspace join on a host that had none) is adopted only on the next daemon restart — appending a workspace at runtime would invalidate the pointers held by running loops, so it is deliberately deferred to restart.

Known limitations (v1):

  • Forward secrecy is per-epoch, not per-message. The workspace key is static within an epoch; key compromise exposes all messages in that epoch. Epoch rotation (RotateKey, and roll-on-removal) re-keys the group on membership change, but there is no per-message ratcheting.
  • Revocation is by rotation, not cryptographic. Removing a member rolls a new epoch key issued only to remaining anchors, so the removed member cannot read future traffic; it does not retroactively protect messages sent before removal. (Anchor enc-pubs for re-wrap are tracked in memory in v1 — anchors admitted before a daemon restart won't receive post-restart rekeys until re-admitted; persistence is a v2 item.)
  • In-memory replay window and sender counters. The daemon tracks a sliding replay window and per-sender counters in memory only. A daemon restart can transiently drop or over-accept messages around the restart boundary. This is acceptable for v1 but will be addressed by persisting counters in a later epoch.
  • Metadata is cleartext. Channel IDs, sender handles, and routing information are not encrypted. Only message content (subject/body) is protected.
  • Fail-closed inbound filtering. E2E agents reject all unencrypted inbound frames — a compromised relay cannot inject unauthenticated cleartext. The connect welcome is delivered locally (it is computed from local state and never traverses the relay) for e2e workspaces.

Layout

cmd/botbus/        TUI chat client + headless listener + agent subcommands
├── main.go        arg parsing, listen-mode pump, runWS wiring, tea bootstrap
├── agent.go       `botbus agent create|list|remove` subcommands
├── ui.go          bubbletea model + view + palette + slash commands
├── ws.go          text + audio WebSocket read/send loops with auto-reconnect
├── audio.go       /audio stream frame playback (ffplay/mpv/mplayer/afplay)
├── updater.go     self-update check against proxy.golang.org
└── *_test.go      unit tests
fabric/            routing-fabric host side (imports botbus-proto)
├── agentstate/    durable local state file (identity, keys, cursors)
├── control/       HTTP client for the router control API
└── hostagent/     agent create/list/remove lifecycle

License

MIT — see LICENSE.txt.

Directories

Path Synopsis
cmd
botbus command
botbus is a tiny line-oriented client for a botbus.ai channel.
botbus is a tiny line-oriented client for a botbus.ai channel.
fabric
agentstate
Package agentstate owns the host-side durable state file for the botbus daemon (default ~/.botbus/state.json, mode 0600).
Package agentstate owns the host-side durable state file for the botbus daemon (default ~/.botbus/state.json, mode 0600).
console
Package console holds the botbus operator-console helpers that aren't part of the TUI event loop: welcome rendering/seeding and (future) roster shaping.
Package console holds the botbus operator-console helpers that aren't part of the TUI event loop: welcome rendering/seeding and (future) roster shaping.
control
Package control is the client side of the routing-fabric control plane: the daemon registers agents and sends heartbeats to the router over HTTP, authenticating with each agent's capability key.
Package control is the client side of the routing-fabric control plane: the daemon registers agents and sends heartbeats to the router over HTTP, authenticating with each agent's capability key.
daemon
Package daemon is the host-side botbus daemon: it multiplexes all of a host's fabric agents over one process — one inbox subscription, delivery queue, and MCP endpoint per agent — so sessions never hold an internet- facing SSE connection and big models wake only for their own inbox.
Package daemon is the host-side botbus daemon: it multiplexes all of a host's fabric agents over one process — one inbox subscription, delivery queue, and MCP endpoint per agent — so sessions never hold an internet- facing SSE connection and big models wake only for their own inbox.
e2e
Package e2e provides pure cryptographic primitives for botbus end-to-end encryption.
Package e2e provides pure cryptographic primitives for botbus end-to-end encryption.
hostagent
Package hostagent implements the host-side agent lifecycle for the botbus daemon/CLI: minting an agent's key + inbox channel, persisting it to the local state file, and registering it with the router's control API.
Package hostagent implements the host-side agent lifecycle for the botbus daemon/CLI: minting an agent's key + inbox channel, persisting it to the local state file, and registering it with the router's control API.
profile
Package profile is the one-time local operator profile for the botbus console: who you are, the standing framing injected into agent welcomes, and your root channel's credentials.
Package profile is the one-time local operator profile for the botbus console: who you are, the standing framing injected into agent welcomes, and your root channel's credentials.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL