@botbuddy/cli 1.31.1 → 1.31.2

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.31.1",
3
+ "version": "1.31.2",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agent-key.mjs CHANGED
@@ -9,6 +9,8 @@
9
9
  // (supabase/functions/_shared/sessionToken.ts). The prior `bb_agent_` shape is
10
10
  // accepted as a legacy alias for one release (its hash is already on `sessions`).
11
11
  // This is a SHAPE check only — authorization is the server's hashed lookup.
12
+ import { TIER3_LEGACY, credentialTier } from "./credential-kinds.mjs";
13
+
12
14
  export const AGENT_KEY_RE = /^bb_(agent|sess)_[0-9a-f]{64}$/;
13
15
 
14
16
  // BOT-1649: BOTBUDDY_AGENT_SESSION_TOKEN names the short-lived credential
@@ -32,3 +34,57 @@ export function readAgentSessionTokenEnv(env = process.env) {
32
34
 
33
35
  // Deprecated exported alias for downstream CLI integrations.
34
36
  export const readAgentKeyEnv = readAgentSessionTokenEnv;
37
+
38
+ // BOT-1701 (AC-6) — one canonical stderr nudge when a tier-3 legacy alias is used
39
+ // for a wait/run: a legacy CLI flag (--agent-key / --session-token), a legacy env
40
+ // var (BOTBUDDY_AGENT_KEY / BOTBUDDY_SESSION_TOKEN) that actually supplied the
41
+ // token, or a bb_agent_-prefixed token value. Returns a SINGLE line (never more)
42
+ // naming the canonical flag + env, or null when only canonical names were used.
43
+ // The caller writes it to STDERR only, so a machine-parseable stdout receipt is
44
+ // byte-identical to a canonical-flag run. Precedence matches the CLI: a token
45
+ // flag overrides the env, so the env is only "used" when no token flag was given.
46
+ // The value passed after any tier-3 token flag (canonical or legacy), or null.
47
+ function tokenAfterFlag(argv, flags) {
48
+ for (let i = 0; i < argv.length; i++) {
49
+ if (flags.includes(argv[i])) return argv[i + 1] ?? null;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ export function legacyAgentAliasHint({ argv = [], env = process.env, token = null } = {}) {
55
+ const tier3 = credentialTier(3);
56
+ const canonicalFlag = tier3.flag; // --agent-session-token
57
+ const canonicalEnv = tier3.env; // BOTBUDDY_AGENT_SESSION_TOKEN
58
+ const tokenFlags = [canonicalFlag, ...TIER3_LEGACY.flags];
59
+
60
+ // Only the CLI's own args count. `bb run/test … -- <workload> --agent-key …`
61
+ // forwards a workload after `--` whose identically named flags are NOT BotBuddy
62
+ // credentials, so never scan past the separator (Codex R6).
63
+ const sep = argv.indexOf("--");
64
+ const cliArgs = sep === -1 ? argv : argv.slice(0, sep);
65
+
66
+ const legacyFlagUsed = TIER3_LEGACY.flags.some((f) => cliArgs.includes(f));
67
+ const flagToken = tokenAfterFlag(cliArgs, tokenFlags);
68
+ const anyTokenFlag = flagToken !== null || legacyFlagUsed || cliArgs.includes(canonicalFlag);
69
+
70
+ // The value the env would supply (canonical wins), so a bb_agent_ VALUE behind
71
+ // the canonical flag/env is nudged even though the NAME is canonical.
72
+ let legacyEnvUsed = false;
73
+ let envToken = null;
74
+ if (!anyTokenFlag) {
75
+ const canonical = typeof env[canonicalEnv] === "string" ? env[canonicalEnv].trim() : "";
76
+ if (canonical) {
77
+ envToken = env[canonicalEnv];
78
+ } else {
79
+ for (const e of TIER3_LEGACY.envs) {
80
+ if (typeof env[e] === "string" && env[e].trim() !== "") { legacyEnvUsed = true; envToken = env[e]; break; }
81
+ }
82
+ }
83
+ }
84
+
85
+ const effectiveToken = token ?? flagToken ?? envToken;
86
+ const legacyPrefixUsed = typeof effectiveToken === "string" && effectiveToken.startsWith(TIER3_LEGACY.prefix);
87
+
88
+ if (!legacyFlagUsed && !legacyEnvUsed && !legacyPrefixUsed) return null;
89
+ return `note: --agent-key / --session-token / $BOTBUDDY_AGENT_KEY / ${TIER3_LEGACY.prefix} are one-release legacy aliases — use ${canonicalFlag} or $${canonicalEnv} (docs/agent-connectivity.md)`;
90
+ }
Binary file
@@ -30,6 +30,48 @@ export const CREDENTIAL_PREFIXES = [
30
30
  { prefix: "bb_", kind: "legacy", label: "legacy personal key" },
31
31
  ];
32
32
 
33
+ // BOT-1701 — the ONE canonical per-tier credential contract. The same noun,
34
+ // prefix, CLI flag, env var, and `credential_kind` for each tier, named the same
35
+ // way EVERYWHERE it is met: the setup-block help text, the connectivity guide's
36
+ // tier table, the `bb wait/run/test/pw --help` flags, the connectivity error
37
+ // strings, and the server's `whoami credential_kind`. A drift test
38
+ // (credential-naming-contract.test.mjs) fails when any of those surfaces names a
39
+ // tier-3 credential by anything other than the canonical noun/flag/env below.
40
+ // Decided by BOT-1649 — do not re-decide here. Additive only: legacy aliases
41
+ // (TIER3_LEGACY) keep working for one release and are listed separately so a
42
+ // surface can name them ONLY on a line that says "alias"/"legacy".
43
+ /**
44
+ * @typedef {{ tier: 1|2|3, noun: string, prefix: string, flag: string|null, env: string, kind: string }} CredentialTier
45
+ * @type {ReadonlyArray<CredentialTier>}
46
+ */
47
+ export const CREDENTIAL_TIERS = Object.freeze([
48
+ Object.freeze({ tier: 1, noun: "client key", prefix: "bb_cli_", flag: null, env: "BOTBUDDY_CLIENT_KEY", kind: "cli" }),
49
+ Object.freeze({ tier: 2, noun: "MCP key", prefix: "bb_mcp_", flag: null, env: "BOTBUDDY_MCP_KEY", kind: "mcp" }),
50
+ Object.freeze({ tier: 3, noun: "agent session token", prefix: "bb_sess_", flag: "--agent-session-token", env: "BOTBUDDY_AGENT_SESSION_TOKEN", kind: "agent_session" }),
51
+ ]);
52
+
53
+ /**
54
+ * The tier-3 legacy aliases, accepted for one release. Kept apart from the
55
+ * canonical table so a surface can name them only on an "alias"/"legacy" line and
56
+ * the CLI can nudge an operator toward the canonical flag/env (BOT-1701 AC-6).
57
+ */
58
+ export const TIER3_LEGACY = Object.freeze({
59
+ flags: Object.freeze(["--agent-key", "--session-token"]),
60
+ envs: Object.freeze(["BOTBUDDY_AGENT_KEY", "BOTBUDDY_SESSION_TOKEN"]),
61
+ prefix: "bb_agent_",
62
+ });
63
+
64
+ /** The canonical tier row for a tier number (1|2|3), or undefined. */
65
+ export function credentialTier(tier) {
66
+ return CREDENTIAL_TIERS.find((t) => t.tier === tier);
67
+ }
68
+
69
+ /** The canonical tier row whose prefix a secret carries, or undefined. */
70
+ export function credentialTierByPrefix(secret) {
71
+ if (typeof secret !== "string") return undefined;
72
+ return CREDENTIAL_TIERS.find((t) => secret.startsWith(t.prefix));
73
+ }
74
+
33
75
  /**
34
76
  * Classify a raw secret by its prefix.
35
77
  * @param {unknown} secret
package/src/pw/run.mjs CHANGED
@@ -7,13 +7,13 @@ import { canonicalizeHostString } from "./host.mjs";
7
7
  import { resolveAgentBinding } from "../wait-profile.mjs";
8
8
  import { loadConfig, getConfig } from "../config.mjs";
9
9
  import { VERSION } from "../version.mjs";
10
- import { readAgentSessionTokenEnv, AGENT_KEY_RE } from "../agent-key.mjs";
10
+ import { readAgentSessionTokenEnv, AGENT_KEY_RE, legacyAgentAliasHint } from "../agent-key.mjs";
11
11
  import { clearAgentState, isRejectedCachedMcpSession, resolveAgentSessionCredential } from "../agent-session.mjs";
12
12
  // BOT-1488: canonicalize the raw hostname the SAME way acquire_resources does
13
13
  // server-side, so the lane name bb-pw builds/matches/prints is the one the lock
14
14
  // kernel actually stored ("jonos-mbp:8", not "Jonos-MBP.localdomain:8").
15
15
  const hostFor = (env) => canonicalizeHostString(env.PLAYWRIGHT_MCP_HOST || env.HOSTNAME || os.hostname());
16
- function help(out) { out.write("Usage: pw [--tenant <slug>] [--session-id <id>] <lane> <verb> [args…]\n\nAliases: bb-pw <lane> <verb> [args…] · botbuddy pw <lane> <verb> [args…]\n (all three drive the same lock-gated Playwright lane)\n\n--session-id <id> accept a lane held by this arming-session agent id (from\n register_agent); defaults to $BOTBUDDY_SESSION_ID, then the\n id saved by `botbuddy register`.\n--tenant <slug> override the worktree .botbuddy-agent.json tenant when falling\n back to the .mcp.json ($BOTBUDDY_MCP_KEY) credential.\n"); }
16
+ function help(out) { out.write("Usage: pw [--tenant <slug>] [--session-id <id>] [--agent-session-token <token>] <lane> <verb> [args…]\n\nAliases: bb-pw <lane> <verb> [args…] · botbuddy pw <lane> <verb> [args…]\n (all three drive the same lock-gated Playwright lane)\n\n--session-id <id> accept a lane held by this arming-session agent id (from\n register_agent); defaults to $BOTBUDDY_SESSION_ID, then the\n id saved by `botbuddy register`.\n--agent-session-token <token> session credential (env $BOTBUDDY_AGENT_SESSION_TOKEN);\n --agent-key / --session-token are one-release legacy aliases.\n--tenant <slug> override the worktree .botbuddy-agent.json tenant when falling\n back to the .mcp.json ($BOTBUDDY_MCP_KEY) credential.\n"); }
17
17
  function redact(value, secretValues = []) { return secretValues.reduce((text, secret) => secret ? text.split(secret).join("[redacted]") : text, String(value ?? "")); }
18
18
  // BOT-1488: the register_agent identity for this machine, persisted by
19
19
  // `botbuddy register` into ~/.botbuddy/config.json. This is the SESSION agent
@@ -99,9 +99,28 @@ export async function runPw(argv, deps = {}) {
99
99
  if (code !== 0) stderr.write(`${versionLine()} — if your checkout's cli/package.json is newer, this global is stale: npm i -g @botbuddy/cli@latest, or run node cli/bin/bb-pw.mjs from the repo.\n`);
100
100
  return code;
101
101
  }
102
+ // The leading option/value pairs bb pw consumes before the lane/verb (mirrors the
103
+ // while-loop in runPwInner). Everything after the first non-option (the lane) is a
104
+ // Playwright workload argument and is never a BotBuddy credential.
105
+ const PW_LEADING_FLAGS = ["--tenant", "--session-id", "--agent-session-token", "--agent-key", "--session-token"];
106
+ function leadingPwOptions(args) {
107
+ const lead = [];
108
+ for (let i = 0; i < args.length && PW_LEADING_FLAGS.includes(args[i]); i += 2) {
109
+ lead.push(args[i]);
110
+ if (i + 1 < args.length) lead.push(args[i + 1]);
111
+ }
112
+ return lead;
113
+ }
114
+
102
115
  async function runPwInner(argv, deps = {}) {
103
116
  const env = deps.env ?? process.env, stdout = deps.stdout ?? process.stdout, stderr = deps.stderr ?? process.stderr; let args = [...argv]; if (["--help", "-h"].includes(args[0])) { help(stdout); return 0; }
104
117
  if (["--version", "-v"].includes(args[0])) { stdout.write(`${versionLine()}\n`); return 0; }
118
+ // BOT-1701 (AC-6): one stderr nudge for a tier-3 legacy flag/env alias. bb pw
119
+ // only recognizes credential flags as LEADING options (the while-loop below);
120
+ // a `--agent-key` after the lane/verb is a Playwright workload argument, not a
121
+ // BotBuddy credential, so scan only the leading options (Codex R9).
122
+ const aliasHint = legacyAgentAliasHint({ argv: leadingPwOptions(args), env });
123
+ if (aliasHint) stderr.write(`bb pw: ${aliasHint}\n`);
105
124
  // BOT-1649: --agent-session-token / $BOTBUDDY_AGENT_SESSION_TOKEN is canonical;
106
125
  // legacy flags and env aliases remain accepted as a
107
126
  // session identity alongside --session-id, so a token-armed session need not
package/src/run.mjs CHANGED
@@ -14,7 +14,7 @@ import { spawn } from "node:child_process";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { callToolJson } from "./api.mjs";
16
16
  import { parseLaneEvents, laneCaseCounts, flushLaneCases, buildLaneSummary, laneSummaryFilename } from "./test-lane-events.mjs";
17
- import { AGENT_KEY_RE, readAgentSessionTokenEnv } from "./agent-key.mjs";
17
+ import { AGENT_KEY_RE, readAgentSessionTokenEnv, legacyAgentAliasHint } from "./agent-key.mjs";
18
18
  import { clearAgentState, isRejectedCachedMcpSession, resolveAgentSessionCredential } from "./agent-session.mjs";
19
19
  import { createTelemetryOutbox } from "./telemetry-outbox.mjs";
20
20
  import { deliverExecutionEvent } from "./telemetry-delivery.mjs";
@@ -532,9 +532,14 @@ export async function cmdRun(args) {
532
532
  return;
533
533
  }
534
534
  if (args[0] === "help" || args[0] === "--help") {
535
- console.log("botbuddy run --session-id <id> --environment <local|preview|staging|production|none> [--timeout <sec>] [--rerun-reason <json>] -- <command> [args...]\nbotbuddy run --foreground --kind <test|wait|browser|ci|hook|stack> --environment <local|preview|staging|production|none> -- <command> [args...]\nbotbuddy run cancel <run_id>");
535
+ console.log("botbuddy run --session-id <id> --environment <local|preview|staging|production|none> [--timeout <sec>] [--rerun-reason <json>] -- <command> [args...]\nbotbuddy run --foreground --kind <test|wait|browser|ci|hook|stack> --environment <local|preview|staging|production|none> -- <command> [args...]\nbotbuddy run cancel <run_id>\n --agent-session-token <token> session credential (env $BOTBUDDY_AGENT_SESSION_TOKEN); --agent-key / --session-token are one-release legacy aliases");
536
536
  return;
537
537
  }
538
+ // BOT-1701 (AC-6): one stderr nudge when a tier-3 legacy flag/env alias is used
539
+ // (stderr only, so the stdout receipt stays byte-identical). A cached bb_agent_
540
+ // token is nudged inside resolveAgentSessionCredential.
541
+ const aliasHint = legacyAgentAliasHint({ argv: args, env: process.env });
542
+ if (aliasHint) process.stderr.write(`botbuddy run: ${aliasHint}\n`);
538
543
  if (args.includes("--foreground")) {
539
544
  let identity;
540
545
  try { identity = await loadTelemetryIdentity(); }
@@ -8,46 +8,39 @@
8
8
  // session persistence; normal operator output must never turn that into a
9
9
  // multi-command credential handoff.
10
10
 
11
+ import { credentialTier } from "./credential-kinds.mjs";
12
+
11
13
  /** The canonical guide, referenced from every SETUP block and recovery. */
12
14
  export const CONNECTIVITY_GUIDE = "docs/agent-connectivity.md";
13
15
 
14
16
  // The three connectivity credential tiers, named consistently everywhere. A
15
17
  // connectivity error names the MISSING tier and the exact next command; the
16
18
  // guide's tiered table and the help SETUP block use these same labels.
19
+ //
20
+ // BOT-1701: the noun, prefix, flag, env, and kind for each tier are OWNED by
21
+ // credential-kinds.mjs (CREDENTIAL_TIERS) — the single source of truth. This map
22
+ // only adds the setup-specific fields (the tier number, the `setup` command, and
23
+ // the scope). A drift test asserts every user-facing surface agrees with the
24
+ // canonical table, so `label`/`prefix`/`env`/`flag` here can never fork from it.
17
25
  export const TIERS = Object.freeze({
18
26
  // Tier 1 — per-machine client key. `bb login`, USER-scoped (reaches every
19
27
  // 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
+ client: Object.freeze({ ...tierFields(1), setup: "bb login", scope: "user" }),
28
29
  // Tier 2 — per-tenant MCP config key. `bb mcp setup`, TENANT-bound, lives in
29
30
  // .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, tenant-bound, 8 h TTL, revoked on
31
+ mcp: Object.freeze({ ...tierFields(2), setup: "bb mcp setup", scope: "tenant" }),
32
+ // Tier 3 — per-session agent session token, tenant-bound, 8 h TTL, revoked on
39
33
  // re-register. The CLI caches it in .botbuddy/agent-state.json and restores
40
34
  // it automatically for wait/run/test/pw.
41
- session: Object.freeze({
42
- n: 3,
43
- label: "session token",
44
- prefix: "bb_sess_",
45
- env: "BOTBUDDY_AGENT_SESSION_TOKEN",
46
- setup: "register_agent",
47
- scope: "session",
48
- }),
35
+ session: Object.freeze({ ...tierFields(3), setup: "register_agent", scope: "session" }),
49
36
  });
50
37
 
38
+ /** Project a canonical CREDENTIAL_TIERS row onto the TIERS shape (n/label/…). */
39
+ function tierFields(tier) {
40
+ const t = credentialTier(tier);
41
+ return { n: t.tier, label: t.noun, prefix: t.prefix, env: t.env, flag: t.flag };
42
+ }
43
+
51
44
  /**
52
45
  * The shared SETUP block reproduced verbatim in every `--help` output. Plain
53
46
  * text (no ANSI) so the snapshot test can match it byte-for-byte across surfaces.
package/src/test-lane.mjs CHANGED
@@ -21,7 +21,7 @@ import { execFileSync } from "node:child_process";
21
21
 
22
22
  import { EXIT, launchRun, receiptPath, DEFAULT_TIMEOUT_SECONDS } from "./run.mjs";
23
23
  import { callToolJson } from "./api.mjs";
24
- import { readAgentSessionTokenEnv } from "./agent-key.mjs";
24
+ import { readAgentSessionTokenEnv, legacyAgentAliasHint } from "./agent-key.mjs";
25
25
  import { clearAgentState, isRejectedCachedMcpSession, resolveAgentSessionCredential } from "./agent-session.mjs";
26
26
 
27
27
  // EXIT.{OK,INVALID,BACKEND,INTERNAL} plus a --wait timeout code (AC-9).
@@ -338,7 +338,8 @@ function laneList(cwd) {
338
338
  }
339
339
 
340
340
  function testHelp() {
341
- console.log(`botbuddy test <lane> [--session-id <uuid>] [--environment local] [--ticket <KEY>] [--pr <n>] [--repo <owner/repo>] [--lane-kind <${LANE_KINDS.join("|")}>] [--wait] [--json] [-- <extra args>]
341
+ console.log(`botbuddy test <lane> [--session-id <uuid>] [--agent-session-token <token>] [--environment local] [--ticket <KEY>] [--pr <n>] [--repo <owner/repo>] [--lane-kind <${LANE_KINDS.join("|")}>] [--wait] [--json] [-- <extra args>]
342
+ --agent-session-token <token> session credential (env $BOTBUDDY_AGENT_SESSION_TOKEN); --agent-key / --session-token are one-release legacy aliases
342
343
  botbuddy test list List the lanes configured in .botbuddy/lanes.json
343
344
  botbuddy test help Show this help
344
345
 
@@ -351,6 +352,9 @@ export async function cmdTest(args, { cwd = process.cwd() } = {}) {
351
352
  const { subcommand } = parseTestArgs(args);
352
353
  if (subcommand === "help" || args[0] === "--help" || args[0] === "-h") { testHelp(); return; }
353
354
  if (subcommand === "list") { process.exitCode = laneList(cwd); return; }
355
+ // BOT-1701 (AC-6): one stderr nudge for a tier-3 legacy flag/env alias.
356
+ const aliasHint = legacyAgentAliasHint({ argv: args, env: process.env });
357
+ if (aliasHint) process.stderr.write(`botbuddy test: ${aliasHint}\n`);
354
358
  const result = await launchTestLane(args, { cwd });
355
359
  process.stdout.write(result.line + "\n");
356
360
  // --wait (AC-9): after launch, block until the run completes and exit with the
package/src/wait.mjs CHANGED
@@ -18,7 +18,7 @@ import { readAgentBinding, withPrincipalReceipt } from "./wait-profile.mjs";
18
18
  import { VERSION } from "./version.mjs";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { latestPublicCliCommand } from "./public-invocation.mjs";
21
- import { AGENT_KEY_RE, readAgentSessionTokenEnv } from "./agent-key.mjs";
21
+ import { AGENT_KEY_RE, readAgentSessionTokenEnv, legacyAgentAliasHint } from "./agent-key.mjs";
22
22
  import { clearAgentState, hasBootstrapAgentSession, isRejectedCachedSession, resolveAgentSessionCredential, selfHealAgentSession } from "./agent-session.mjs";
23
23
  import { touchAgentStateExpiry } from "./agent-state.mjs";
24
24
  import { SETUP_BLOCK } from "./setup-block.mjs";
@@ -1344,6 +1344,16 @@ async function acknowledgeSavedWait(id) {
1344
1344
 
1345
1345
  export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
1346
1346
  if (argv.length === 0 || argv[0] === "--offset") return inspectSavedWaits(argv);
1347
+ // BOT-1701 (AC-6): one stderr nudge for a tier-3 legacy alias (flag, env, or a
1348
+ // bb_agent_ token value), emitted BEFORE any subcommand dispatch so the
1349
+ // credentialed recovery actions (resume/cancel/acknowledge) warn too (Codex R7).
1350
+ // STDERR only, so a machine-parseable stdout receipt is byte-identical. Skipped
1351
+ // on the internal recovery re-entry (resumeSavedWait recurses into runWait with
1352
+ // recoveryLocalWaitId set) so a resume never nudges twice (Codex R8).
1353
+ if (recoveryLocalWaitId == null) {
1354
+ const aliasHint = legacyAgentAliasHint({ argv, env: process.env });
1355
+ if (aliasHint) process.stderr.write(`botbuddy wait: ${aliasHint}\n`);
1356
+ }
1347
1357
  if (argv[0] === "resume") {
1348
1358
  const id = argv[1];
1349
1359
  if (!id || argv.length > 2) return emitSavedWaitError(new WaitCheckpointError("wait_not_resumable", "select exactly one saved wait: bb wait resume <local_wait_id>"));