@botbuddy/cli 1.27.0 → 1.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.27.0",
3
+ "version": "1.29.0",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/commands.mjs CHANGED
@@ -12,6 +12,7 @@ import { cmdDocker } from "./docker-hygiene.mjs";
12
12
  import { cmdRun } from "./run.mjs";
13
13
  import { cmdTest } from "./test-lane.mjs";
14
14
  import { runWait } from "./wait.mjs";
15
+ import { SETUP_BLOCK } from "./setup-block.mjs";
15
16
  import { green, red, cyan, dim, bold, die } from "./utils.mjs";
16
17
  import { VERSION } from "./version.mjs";
17
18
  import { setupMcpKey, revokeMcpKey, resolveMcpConfigKey, resolveEnvVarName, DEFAULT_MCP_ENV_VAR, McpKeyError, McpKeyStoreError } from "./mcp-key.mjs";
@@ -114,8 +115,8 @@ async function cmdTelemetry(args) {
114
115
  return 0;
115
116
  }
116
117
 
117
- function cmdHelp() {
118
- console.log(`${bold("bb")} ${dim(`v${VERSION}`)} — Swarm coordination CLI
118
+ export function cmdHelp(log = console.log) {
119
+ log(`${bold("bb")} ${dim(`v${VERSION}`)} — Swarm coordination CLI
119
120
 
120
121
  ${dim(`Run everything as ${cyan("bb <command>")} — the short, preferred alias. ${cyan("botbuddy <command>")} is the identical long form.`)}
121
122
 
@@ -184,7 +185,9 @@ ${bold("OTHER")}
184
185
  locks -m [--host name] Reserve typed local resources, including Playwright MCP lanes
185
186
  update Install the latest @botbuddy/cli in the background via npm
186
187
  help Show this help
187
- version Show version`);
188
+ version Show version
189
+
190
+ ${SETUP_BLOCK}`);
188
191
  }
189
192
 
190
193
  // ─── BOT-876: generic tool access ───────────────────────────────
@@ -325,8 +328,8 @@ async function cmdLogin(args, { errorLog = (line) => console.error(line) } = {})
325
328
  }
326
329
  }
327
330
 
328
- function loginHelp() {
329
- console.log(`${bold("botbuddy login")} — authenticate via OAuth (browser + loopback callback)
331
+ export function loginHelp(log = console.log) {
332
+ log(`${bold("botbuddy login")} — authenticate via OAuth (browser + loopback callback)
330
333
 
331
334
  ${bold("USAGE")}
332
335
  botbuddy login [--no-browser] [--tenant <slug>]
@@ -390,7 +393,9 @@ ${bold("NOTES")}
390
393
  ${bold("RECOVERY")}
391
394
  If ${cyan("botbuddy status")} shows no client key, run ${cyan("botbuddy login")}. For an
392
395
  unattended CI host (no browser), mint a key on a machine with a browser
393
- (${cyan("botbuddy mcp setup")}) and export it as ${cyan("$BOTBUDDY_MCP_KEY")} on the CI host.`);
396
+ (${cyan("botbuddy mcp setup")}) and export it as ${cyan("$BOTBUDDY_MCP_KEY")} on the CI host.
397
+
398
+ ${SETUP_BLOCK}`);
394
399
  }
395
400
 
396
401
  async function cmdStart(args) {
@@ -599,7 +604,7 @@ function cmdHeartbeat(args) {
599
604
 
600
605
  // ─── BOT-1607: `botbuddy mcp` — the tier-2 bb_mcp_ config key ────────────────
601
606
 
602
- function mcpHelp(log) {
607
+ export function mcpHelp(log = console.log) {
603
608
  log(`Usage: botbuddy mcp <setup|revoke|status|env> [options]
604
609
 
605
610
  setup [--env <NAME>] [--tenant <slug>] [--label <label>] [--expiry-days <n>] [--force]
@@ -607,6 +612,9 @@ function mcpHelp(log) {
607
612
  ${cyan("botbuddy login")} owner/client credential), store it in the
608
613
  Keychain under ${dim("BOTBUDDY_MCP_KEY")} (or --env <NAME>), and print
609
614
  the .mcp.json / .codex/config.toml snippets that reference it.
615
+ A bb_mcp_ key is tenant-sealed: on a machine serving more than
616
+ one tenant, mint each tenant under a distinct --env <NAME> var
617
+ and commit that repo's matching .botbuddy-agent.json mcp_env.
610
618
  Refuses an occupied Keychain slot unless --force (revoke the
611
619
  old key first — --force leaves it live server-side).
612
620
  revoke <agent_id> [--tenant <slug>] [--env <NAME>]
@@ -625,7 +633,9 @@ function mcpHelp(log) {
625
633
  or in the desktop LaunchAgent env-install recipe.
626
634
 
627
635
  Present the key from .mcp.json instead of reusing an agent session token — it is
628
- independently revocable, so a leaked config credential rotates without a re-login.`);
636
+ independently revocable, so a leaked config credential rotates without a re-login.
637
+
638
+ ${SETUP_BLOCK}`);
629
639
  }
630
640
 
631
641
  // Actionable recovery for an mcp setup/revoke failure code.
package/src/pw/run.mjs CHANGED
@@ -35,7 +35,7 @@ async function gate({ env, host, lane, deps }) {
35
35
  // server's owner_is_caller.
36
36
  const sessionToken = deps.sessionToken ?? readAgentKeyEnv(env);
37
37
  if (sessionToken && !AGENT_KEY_RE.test(sessionToken)) {
38
- return { allowed: false, message: "bb-pw: $BOTBUDDY_AGENT_KEY is malformed (expected bb_agent_<64 hex>). Re-register the agent, or set BB_PW_NO_LOCK=1 for local-only work." };
38
+ return { allowed: false, message: "bb-pw: $BOTBUDDY_AGENT_KEY is malformed (tier-3 session token; expected bb_agent_<64 hex>). Re-register: register_agent export BOTBUDDY_AGENT_KEY. See docs/agent-connectivity.md. (BB_PW_NO_LOCK=1 runs local-only, unlocked.)" };
39
39
  }
40
40
  const sessionAgentId = deps.sessionId ?? env.BOTBUDDY_SESSION_ID ?? null;
41
41
  const registeredAgentId = await (deps.readSessionAgentId ?? readRegisteredAgentId)();
@@ -49,7 +49,7 @@ async function gate({ env, host, lane, deps }) {
49
49
  } catch { token = null; }
50
50
  }
51
51
  if (!token) {
52
- return { allowed: false, message: "bb-pw: no BotBuddy credential — export $BOTBUDDY_AGENT_KEY (from register_agent) or your .mcp.json key ($BOTBUDDY_MCP_KEY), or set BB_PW_NO_LOCK=1 for local-only work." };
52
+ return { allowed: false, message: "bb-pw: no BotBuddy credential — a lane needs the tier-3 session token (bb_agent_: register_agent → export $BOTBUDDY_AGENT_KEY) or your tier-2 .mcp.json key ($BOTBUDDY_MCP_KEY, bb mcp setup). See docs/agent-connectivity.md. (BB_PW_NO_LOCK=1 runs local-only, unlocked.)" };
53
53
  }
54
54
  coordinator = createSessionTokenCoordinator({ token, fetchImpl: deps.fetch });
55
55
  }
@@ -0,0 +1,75 @@
1
+ // BOT-1644 — ONE source of truth for the connectivity ladder and the credential
2
+ // tier names, so every CLI help surface (`bb --help`, `bb wait --help`,
3
+ // `bb login --help`, `bb mcp --help`), the recovery strings, and the canonical
4
+ // guide all agree. The snapshot test (agent-connectivity.test.mjs) asserts the
5
+ // SETUP block appears verbatim in all four help outputs, so drift is caught.
6
+ //
7
+ // This documents the CURRENT supported process (BOT-1644 scope). It does NOT
8
+ // describe the P1–P6 "self-healing auth" redesign, which is a separate future
9
+ // project.
10
+
11
+ /** The canonical guide, referenced from every SETUP block and recovery. */
12
+ export const CONNECTIVITY_GUIDE = "docs/agent-connectivity.md";
13
+
14
+ // The three connectivity credential tiers, named consistently everywhere. A
15
+ // connectivity error names the MISSING tier and the exact next command; the
16
+ // guide's tiered table and the help SETUP block use these same labels.
17
+ export const TIERS = Object.freeze({
18
+ // Tier 1 — per-machine client key. `bb login`, USER-scoped (reaches every
19
+ // tenant you belong to; each request pins one). Never a wait credential.
20
+ client: Object.freeze({
21
+ n: 1,
22
+ label: "client key",
23
+ prefix: "bb_cli_",
24
+ env: "BOTBUDDY_CLIENT_KEY",
25
+ setup: "bb login",
26
+ scope: "user",
27
+ }),
28
+ // Tier 2 — per-tenant MCP config key. `bb mcp setup`, TENANT-bound, lives in
29
+ // .mcp.json under $BOTBUDDY_MCP_KEY. Authenticates MCP calls + register_agent.
30
+ mcp: Object.freeze({
31
+ n: 2,
32
+ label: "MCP key",
33
+ prefix: "bb_mcp_",
34
+ env: "BOTBUDDY_MCP_KEY",
35
+ setup: "bb mcp setup",
36
+ scope: "tenant",
37
+ }),
38
+ // Tier 3 — per-session agent token. `register_agent` (over MCP), tenant-bound,
39
+ // 8 h TTL, revoked on re-register. The ONLY credential a wait/run/test/pw
40
+ // accepts. Exported as $BOTBUDDY_AGENT_KEY (+ $BOTBUDDY_SESSION_ID for
41
+ // attribution).
42
+ session: Object.freeze({
43
+ n: 3,
44
+ label: "session token",
45
+ prefix: "bb_agent_",
46
+ env: "BOTBUDDY_AGENT_KEY",
47
+ setup: "register_agent",
48
+ scope: "session",
49
+ }),
50
+ });
51
+
52
+ // The register_agent → export handoff, named once. Recovery strings reuse it so
53
+ // the exact two export lines never drift from the guide.
54
+ export const EXPORT_STEP =
55
+ "register_agent → export BOTBUDDY_AGENT_KEY=<session_token> (and BOTBUDDY_SESSION_ID=<session_id>)";
56
+
57
+ /**
58
+ * The shared SETUP block reproduced verbatim in every `--help` output. Plain
59
+ * text (no ANSI) so the snapshot test can match it byte-for-byte across surfaces.
60
+ * The E2E ladder in order, then the fresh-shell caveat and the guide link.
61
+ */
62
+ export const SETUP_BLOCK = `SETUP — the connectivity ladder (install → connect → wait)
63
+ 1. npm i -g @botbuddy/cli install the CLI
64
+ 2. bb login tier 1 · client key (bb_cli_, user-scoped) → Keychain
65
+ 3. bb mcp setup tier 2 · MCP key (bb_mcp_, tenant-bound) → Keychain + .mcp.json ($BOTBUDDY_MCP_KEY)
66
+ (Keychainless host: the key is printed ONCE — store + export + edit .mcp.json yourself)
67
+ the repo's pre-committed .botbuddy-agent.json binds tenant + mcp_env
68
+ multi-tenant machine: mint each tenant under a distinct --env <NAME> var (they collide otherwise)
69
+ 4. register_agent (over MCP) tier 3 · session token (bb_agent_, per-session, 8 h)
70
+ 5. export BOTBUDDY_AGENT_KEY=<session_token> and BOTBUDDY_SESSION_ID=<session_id>
71
+ 6. bb wait needs the tier-3 token; bb run/test also accept $BOTBUDDY_SESSION_ID,
72
+ bb pw falls back to the tier-2 MCP key
73
+ NOTE: each Claude Code Bash call is a FRESH shell — exports do NOT persist between tool calls;
74
+ re-export (or pass --agent-key <token>) in the same command that runs the wait.
75
+ Full guide: ${CONNECTIVITY_GUIDE}`;
package/src/wait.mjs CHANGED
@@ -19,6 +19,7 @@ import { VERSION } from "./version.mjs";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { latestPublicCliCommand } from "./public-invocation.mjs";
21
21
  import { AGENT_KEY_RE, readAgentKeyEnv } from "./agent-key.mjs";
22
+ import { SETUP_BLOCK } from "./setup-block.mjs";
22
23
 
23
24
  // A protocol is deliberately distinct from package semver: compatible pinned
24
25
  // clients keep working until the server raises this minimum, while a stale
@@ -36,7 +37,7 @@ const MIN_RECEIPT_MAX_BYTES = 512;
36
37
  // BOT-1572 bb_sess_ legacy alias, both accepted for one release.
37
38
  const SESSION_TOKEN_RE = AGENT_KEY_RE;
38
39
 
39
- const HELP = `botbuddy wait — one wait command instead of a polling loop (BOT-989)
40
+ export const HELP = `botbuddy wait — one wait command instead of a polling loop (BOT-989)
40
41
 
41
42
  USAGE
42
43
  botbuddy wait [--any] <condition>... [options]
@@ -159,6 +160,8 @@ EXAMPLES
159
160
  # after pushing review fixes, wait for the next review comment or a state change
160
161
  botbuddy wait --any 'pr-review:repo=bot-buddy/botbuddy-webapp,pr=425' \\
161
162
  'pr-state:repo=bot-buddy/botbuddy-webapp,pr=425' --timeout 3600
163
+
164
+ ${SETUP_BLOCK}
162
165
  `;
163
166
 
164
167
  function parseArgv(argv) {
@@ -630,11 +633,11 @@ function emit(receipt, { versioned = false, maxBytes = null } = {}) {
630
633
  // SAME string is written to stderr and the receipt `recovery` field that /waits
631
634
  // renders. Keep each to one line and never embed a secret value — names, slots,
632
635
  // and env-var names only.
633
- const RECOVERY = Object.freeze({
636
+ export const RECOVERY = Object.freeze({
634
637
  // BOT-1582: $BOTBUDDY_AGENT_KEY (bb_agent_+64hex, renamed from
635
638
  // $BOTBUDDY_SESSION_TOKEN) is minted by register_agent; the harness exports it
636
639
  // at session start. The legacy var is still accepted for one release.
637
- sessionToken: "register_agent export BOTBUDDY_AGENT_KEY=<session_token> (the harness exports it at session start)",
640
+ sessionToken: "the tier-3 session token (bb_agent_) is minted by register_agent export BOTBUDDY_AGENT_KEY=<session_token> (and BOTBUDDY_SESSION_ID=<session_id>); the harness exports both at session start. See docs/agent-connectivity.md",
638
641
  // $BOTBUDDY_SESSION_ID is the work-graph session id returned by register_agent.
639
642
  sessionId: "register_agent → export BOTBUDDY_SESSION_ID=<session_id> (the work-graph session id from register_agent)",
640
643
  // A --token / session-token contradiction: the session token is the whole
@@ -962,7 +965,7 @@ export async function runWait(argv) {
962
965
  // register a work agent and export its session token.
963
966
  if (err.errorCode === "client_key_cannot_wait") {
964
967
  process.stderr.write(
965
- "botbuddy wait: a client key (botbuddy login) cannot arm a wait — register_agent, export BOTBUDDY_AGENT_KEY=<session_token>, then re-run\n",
968
+ "botbuddy wait: a tier-1 client key (bb_cli_, botbuddy login) cannot arm a wait — you need the tier-3 session token: register_agent, export BOTBUDDY_AGENT_KEY=<session_token>, then re-run\n",
966
969
  );
967
970
  emitReceipt({
968
971
  schema_version: 1,