@blahai/bode
v0.1.0
Published
Bode collaborative sessions and device executor
Readme
@repo/bode-cli
bode, the interactive command-line client.
Install on a computer with Node.js 22 or newer:
npm install -g https://bode.blah.dev/downloads/blahai-bode-0.1.0.tgz
bode login
bode device register "My computer"
bode device runbode login prints a verification URL and code to approve in your browser. Keep
bode device run running under the same user account after registration; a user-level
service manager can restart it after reboot. Once the device is online, open the
session's Manage devices dialog, choose Attach a device, and set an absolute
workspace path and capabilities. Registration alone does not grant a session access.
The public @blahai/bode registry release is pending npm account authentication.
The npm release is assembled and verified with ops/bode/publish-cli.sh; the script's
--publish option publishes the tested tarball as public @blahai/bode once signed in.
pnpm --filter @repo/bode-cli bode login # device grant: approve the code on any device
pnpm --filter @repo/bode-cli bode new "Launch plan" # start a session and open it
pnpm --filter @repo/bode-cli bode sessions # list sessions
pnpm --filter @repo/bode-cli bode open <session-id> # continue a session started anywhere
pnpm --filter @repo/bode-cli bode logoutIn a session, type a message and press Enter. /cancel stops the agent's current turn and
/quit leaves. Leaving ends only this client; the session and any turn in progress continue
on the server, and the web app or another terminal can pick them up.
A message is kept as a draft until bode accepts it, in $XDG_STATE_HOME/bode/drafts/ (or
~/.local/state/bode/drafts/), one private file per session. If bode cannot be reached, the
message stays there with the key of its first attempt: /send tries again, including after you
reopen the session, and bode accepts it once. /discard drops it. A draft started before the
session's configuration changed is held until you /send it a second time. Nothing runs offline.
Pausing, archiving, and the trash
bode pause <session-id> # hold new runs; messages are still accepted and wait
bode resume <session-id> # let a paused or archived session run again
bode archive <session-id> # take it off the active list; people's messages still run
bode trash <session-id> [--yes] # move it to the trash, after asking; nothing is deleted
bode restore <session-id> # bring it back from the trash as it was
bode delete <session-id> [--yes] # delete a session in the trash permanently, after askingbode delete works only on a session already in the trash, so deletion is always a second
decision. It is hidden at once; its messages, runs, events, files, and session memory are purged,
and it cannot be restored, even from a database backup. Memory people own is not deleted.
Each change applies at the session's current revision, so two clients cannot silently undo
each other. Pausing and archiving are for anyone who may change the session's settings; the
trash and restoring from it are for its administrators. A session in the trash is left out of
bode sessions, refuses new messages, and holds its queued runs until it is restored. Work
already running continues to its next boundary.
Sign-in
bode login uses the OAuth device authorization grant (RFC 8628) against blah.dev as the
public client bode-cli, so it works over SSH and on machines without a browser. The token
is saved only if the bode scope was granted, in $XDG_CONFIG_HOME/bode/credentials.json
(or ~/.config/bode/), written atomically with mode 0600 in a 0700 directory. It is refreshed
shortly before it expires; when refresh is refused, the CLI asks you to sign in again.
| Variable | Default |
|---|---|
| BODE_API_URL | https://bode.blah.dev |
| BODE_ISSUER | https://blah.dev |
| BODE_CLIENT_ID | bode-cli |
Plain HTTP is accepted only for loopback addresses, for development.
Sharing and organizations
bode participants <session-id> # who can open a session, and each role
bode share <session-id> [email protected] --role viewer # viewer, contributor (default), or admin
bode unshare <session-id> [email protected] # takes away the role you gave; org roles stay
bode orgs
bode org create "Acme" # you become its owner
bode org add <organization-id> [email protected] --role admin # member (default), admin, or owner
bode org remove <organization-id> [email protected]
bode new "Team plan" --org <organization-id> # a session the organization ownsPeople are named by the email of their blah.dev account. Sharing never shares your devices, inference connections, or builds; contributors can use the devices attached to the session.
Agents
bode agents # agents you can bring into sessions
bode agent create "Reviewer" [--org <organization-id>]
bode agent add <session-id> <agent-id> [--handle reviewer]
bode agent stop <session-id> @reviewer # stops that agent's unfinished runs only
bode agent remove <session-id> @reviewer # stops its runs and takes it outA message goes to every agent it mentions by @handle, one run each, or to the session's
designated @bode when it mentions none. An agent's own message wakes no one; agents hand work
to each other only through the delegate tool, which starts a child run.
Work
bode tasks <session-id> # tasks, unfinished first, with who holds each
bode budget <session-id> # each allowance, with its limits and use
bode budget set <session-id> --max-attempts 5 --max-parallel 2
bode budget set <session-id> --agent @reviewer --max-tokens 200000
bode run detach <session-id> <run-id> # a delegated run keeps going when its parent stopsbode budget set replaces every limit of its scope: a limit not named is unset, which means
no limit. Agents create, claim, and complete tasks with their session tools; a claim is a lease
that only its holder can renew or complete.
Memory
bode memory <session-id> # what is attached, for whom, and recent disclosures
HAM_TOKEN=... bode memory connect HAM https://ham.example/mcp --token-env HAM_TOKEN
bode memory attach <session-id> notes --backend <build-id>:postgres --agents @reviewer
bode memory attach <session-id> mine --backend <build-id>:postgres --scope user --read-only
bode memory recall <session-id> @reviewer notes,mine # retrieved before each of its answers
bode memory search <session-id> notes launch owner
bode memory write <session-id> notes decision "Alice owns the launch" --title Owner
bode memory retract <session-id> notes <record-id> --revision 2 --reason "Owner changed"A connection's token is read from the environment variable you name, sealed on the server, and
never shown again. bode decides whose memory a scope is: --scope user always means your own,
and attaching it is recorded as your disclosure. Searches, writes, and retractions are performed
by the worker through the attachment's backend and logged; the command waits for the result.
Agents use the same memory through their memory tools and, with bode memory recall, before
each answer.
Branches
bode branches <session-id> # each branch and where it continues from
bode branch <session-id> Other approach # continue after the latest event
bode branch <session-id> Deeper --of <branch-id> --from 12 # continue a branch after event 12
bode promote <session-id> <branch-id> The other approach # make a branch a session of its ownPromoting gives a branch its own session, owned by you: its conversation becomes that session's history, with the configuration and tasks that went with it. Every binding is rechecked for you — agents you may not use, devices that are not yours, and memory, which is always attached again by the person who reaches it — and each is reported rather than silently dropped or silently granted. Nothing runs as a result.
A branch continues the conversation under a name: its history is the line it continues, up to the point it branched, and then its own messages. Nothing is copied — no files are restored, no finished tool call is repeated, and runs still going stay where they are; the branch records how many were left behind. Each agent works a branch in its own lane, so a branch and its source line proceed independently.
Export
bode export <session-id> ./launch-export # the whole session
bode export <session-id> ./branch-export --branch <id> # one branch's linebode import ./launch-export --title "Their launch plan" # read one back as a session of yoursAn export is a directory of bode.export.v1: manifest.json first, then the session, its
branches, its conversation, its history as one event per line, its tasks, and its configuration,
which names builds by package and digest. Artifacts are named by digest; their bytes are not in
the export. No credential, sealed secret, or device credential is written, and nothing in an
export executes.
Importing reconstructs that as a new session of your own: the messages become history and start no work, the tasks arrive unclaimed, and the configuration binds only to builds you may use — anything else is reported and left unbound. A long conversation is cut to the newest messages it can carry, and what was left behind is listed rather than dropped quietly.
Triggers
bode triggers <session-id> # schedules and webhooks, and what each last did
bode trigger schedule <session-id> Morning summary --cron "0 9 * * 1-5" \
--timezone Europe/Stockholm --agent @reviewer --message "Summarise what changed since yesterday."
DEPLOY_SECRET=... bode trigger webhook <session-id> Deploys \
--secret-env DEPLOY_SECRET --message "A deployment finished: {{payload}}"
bode trigger disable <session-id> <trigger-id> # its history staysA trigger writes its message as the person who added it, so it can never do more than they can.
A schedule follows an IANA timezone: a local time a daylight-saving change skips occurs at the
next valid instant, and one it repeats occurs once. While the session is paused, archived, or in
the trash, occurrences are held with the reason, visible in the session and in bode triggers.
A webhook's secret is read from the environment variable you name, sealed on the server, and
never shown again; each delivery is signed sha256=<HMAC-SHA256 of '<timestamp>.<body>'> in
x-bode-signature, with x-bode-timestamp within five minutes and your own x-bode-delivery,
which a redelivery repeats. A delivery's body is data in the message: it never grants authority.
Devices
bode device register [name] # this computer's credential goes in ~/.config/bode/device.json (0600)
bode device run # the executor; sessions can use the device only while it runs
bode devices # your devices and whether each is online
bode attach <session-id> --root ~/project # shows the grant and attaches after a yes
bode detach <session-id> <name>
bode device rotate # replaces the credential; the old one stops workingbode attach takes --grant (comma-separated capabilities), --name, --default or
--no-default, and --yes. The executor keeps its journal and processes under
$XDG_STATE_HOME/bode/devices/<device-id>/.
Plugins
bode plugin new my-compactor --name @you/my-compactor # --kind compactor (default), tool, preset, or memory
bode plugin test [dir] # run fixtures against the bundle in a plugin host
bode plugin build [dir] # write dist/bode-bundle.json and print its digest
bode plugin attach [dir] --session <session-id> --bind my-compactor
bode plugin publish <build-id> # shows the build and publishes only after a yesnew writes a package that uses the monorepo's TypeScript, ESLint, Vitest, and Biome configuration and links the plugin SDK by path, so it works inside or outside the repository. Run
pnpm installin it. The@reponamespace is reserved. Where bode is only installed, as in the device image, it writes just the package, its source, and its fixtures, linked to the installed SDK:bode plugin testandbuildresolve what the package has not installed from the CLI itself, so there is nothing to install.test bundles the package and loads the bundle in a plugin process with no environment, so it tests what an upload would run. Each
fixtures/*.jsonfile is one case:{ "kind": "compaction", "plugin": "my-compactor", "request": { "turns": [{ "role": "user", "text": "Hi" }, { "role": "user", "text": "Now" }], "recentTail": 1, "targetTokens": 1000, "fixedTokens": 0, "state": null }, "inference": ["answers the compactor's inference requests receive, in order"], "expect": { "status": "succeeded", "output": { "replacedTurns": 1 } } }A tool fixture gives
inputand optionallyworkspace, a directory beside the fixtures that is copied to serve as the workspace; the tool is held to the capability it declares. A memory fixture gives a memoryrequest(and optionallyscopeKeyandprovenance), withrecordsandmcp: what bode's record storage and the MCP connection answer, in order. A memory result the backend classifies as failed fails with its error code. An expectedoutputnames only the fields it checks, and a failure may name its class or tool or memory error code.attach uploads the bundle as a private build that only you can select. The worker verifies it by loading it and comparing its exports with its manifest. With
--sessionand--bind, attach waits for verification and binds the named plugins in one configuration change: a compactor, tools alongside the session's others, or a preset. A memory backend is attached withbode memory attach --backend <build-id>:<name>instead. A changed package is a new build, so attach again after each change.attach --as-device uploads with this computer's device credential, as an agent working on the device does. The build is a private build of the device's owner that names the device; a device can never publish or list builds. It binds nothing: the agent binds the build with its
configure_sessiontool, and a configuration that fails to activate leaves the session's previous revision in place.attach --device keeps the code on this computer: bode records the package's digest and manifest, and only this device runs it, while
bode device runruns. Only compactors run this way.--attachmentpicks which of this device's attachments runs it when there are several. A device-only compactor never falls back to another: while the device is offline, the run waits, blocked.publish is a separate decision. Attaching, binding, and verification never publish.
Behaviour
Session semantics live in @repo/bode-client: the CLI follows a snapshot plus the live
event stream, reconnects from its last cursor, and retries submissions with the same
idempotency key, so a flaky connection never duplicates a turn. Partial output from an
interrupted or failed turn is labelled as incomplete.
Packaging
The PRD publishes the CLI as @blahai/bode. That needs a bundled build of the workspace
packages it uses and is part of opening bode; until then it runs from this repository.
