@botbuddy/cli 1.64.17 → 1.64.19

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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.64.17",
3
- "source_commit": "d2fabc08d075fc51a591dd0ce38dc0ec9d66c168",
4
- "source_sha256": "a21dcce44a33eddc1392ae4604533ee412d1c0c2bed64a26f52e99a2e2feed30",
2
+ "release_version": "1.64.19",
3
+ "source_commit": "44604deb1f6fa2e2923d532bf4bbab095c3c429f",
4
+ "source_sha256": "5cd52ae09d485ea806fb37f86b1da9fa75dc6e37e7bb58952c25a6d695e1555b",
5
5
  "target": "darwin-arm64",
6
- "sha256": "f8f98b5bc212ed676fe2e97381b5a0c548331602ed7165360a331c3e8ff309fd"
6
+ "sha256": "d180a0533460babac47b360dfce0250ba0ef90ce0f21e40e22df3ab632033f3d"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.64.17",
3
- "source_commit": "d2fabc08d075fc51a591dd0ce38dc0ec9d66c168",
4
- "source_sha256": "a21dcce44a33eddc1392ae4604533ee412d1c0c2bed64a26f52e99a2e2feed30",
2
+ "release_version": "1.64.19",
3
+ "source_commit": "44604deb1f6fa2e2923d532bf4bbab095c3c429f",
4
+ "source_sha256": "5cd52ae09d485ea806fb37f86b1da9fa75dc6e37e7bb58952c25a6d695e1555b",
5
5
  "target": "linux-x64",
6
- "sha256": "f4a8fd0904f99e688e857b3674400b3e088fbca7f557bdea7cd1f14abcd84f31"
6
+ "sha256": "137ff9f4318cd332d1320847f3837b5d8f23812e626a96fdf61d9d331d750b1d"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.64.17",
4
- "source_commit": "d2fabc08d075fc51a591dd0ce38dc0ec9d66c168",
5
- "source_sha256": "a21dcce44a33eddc1392ae4604533ee412d1c0c2bed64a26f52e99a2e2feed30",
3
+ "release_version": "1.64.19",
4
+ "source_commit": "44604deb1f6fa2e2923d532bf4bbab095c3c429f",
5
+ "source_sha256": "5cd52ae09d485ea806fb37f86b1da9fa75dc6e37e7bb58952c25a6d695e1555b",
6
6
  "targets": {
7
7
  "darwin-arm64": {
8
8
  "goos": "darwin",
9
9
  "goarch": "arm64",
10
10
  "binary": "daemon/bin/darwin-arm64/botbuddy-daemon",
11
- "sha256": "f8f98b5bc212ed676fe2e97381b5a0c548331602ed7165360a331c3e8ff309fd",
11
+ "sha256": "d180a0533460babac47b360dfce0250ba0ef90ce0f21e40e22df3ab632033f3d",
12
12
  "mode": "0755"
13
13
  },
14
14
  "linux-x64": {
15
15
  "goos": "linux",
16
16
  "goarch": "amd64",
17
17
  "binary": "daemon/bin/linux-x64/botbuddy-daemon",
18
- "sha256": "f4a8fd0904f99e688e857b3674400b3e088fbca7f557bdea7cd1f14abcd84f31",
18
+ "sha256": "137ff9f4318cd332d1320847f3837b5d8f23812e626a96fdf61d9d331d750b1d",
19
19
  "mode": "0755"
20
20
  }
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.64.17",
3
+ "version": "1.64.19",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/commands.mjs CHANGED
@@ -147,7 +147,7 @@ export async function run(argv, {
147
147
  case "wait": return runWait(args);
148
148
  case "pw": return runPw(args);
149
149
  // BOT-2084: the human-installed staging session pool broker.
150
- case "staging-pool": return (await import("./staging-pool/cli.mjs")).runStagingPool(args);
150
+ case "staging-pool": return (await import("./staging-pool/cli.mjs")).runStagingPool(args, { cliVersion: VERSION });
151
151
  case "mcp": return cmdMcp(args);
152
152
  case "resources": return callTool("list_resources");
153
153
  case "agents": return callTool("list_agents");
package/src/feedback.mjs CHANGED
@@ -37,6 +37,19 @@ and its idempotency_key unchanged if delivery is uncertain; retry using the
37
37
  same reporter and tenant. Tenant identity sharing follows the server's policy;
38
38
  a report cannot override it.
39
39
 
40
+ To share separately reviewed anonymous details, prepare .botbuddy/anonymous-evidence.json
41
+ with feedback_id and an evidence object, then run:
42
+ bb call help --tool preview_feedback_anonymous_evidence
43
+ bb call preview_feedback_anonymous_evidence --json "$(cat .botbuddy/anonymous-evidence.json)"
44
+ Inspect every field: automatic checks cannot detect every person or organization.
45
+ Prepare .botbuddy/anonymous-approval.json with the exact evidence, preview_sha256,
46
+ expected_revision from the preview, and confirmed: true, then run:
47
+ bb call share_feedback_anonymous_evidence --json "$(cat .botbuddy/anonymous-approval.json)"
48
+ To withdraw, replace <feedback_id> with the original ID and run:
49
+ bb call withdraw_feedback_anonymous_evidence --feedback_id <feedback_id>
50
+ The separately approved details stay in BotBuddy and do not enable tenant identification.
51
+ For file templates and all commands, run: curl -fsS https://app.bot-buddy.ai/agent-feedback.json
52
+
40
53
  An unavailable service does not mean a report was recorded. A failed send is
41
54
  queued privately only when the original agent and tenant can be bound to the
42
55
  cached session. Run bb feedback flush to replay those drafts after reconnecting.
@@ -398,6 +398,26 @@ export function upgradeNotice(ctx) {
398
398
  return { upgrade: { reason: "installed_before_registry", message: "This pool was installed from a repository checkout; re-install it from the BotBuddy control plane (keeps counts and interval).", next: installCommandFor(ctx.tenant) } };
399
399
  }
400
400
 
401
+ // BOT-3016: the launchd job runs a pinned runtime copy, not this CLI. Status
402
+ // names both versions and, when they differ on a control-plane pool, the
403
+ // agent-safe refresh (bb update runs it too). cliVersion is null inside the
404
+ // bundled job copy, which has no package.json to read.
405
+ export function runtimeNotice(ctx, cliVersion) {
406
+ if (!ctx?.installed || ctx.installed.error) return {};
407
+ const runtimeVersion = ctx.installed.runtime?.version ?? null;
408
+ const versions = { runtimeVersion, ...(cliVersion ? { cliVersion } : {}) };
409
+ if (!cliVersion || runtimeVersion === cliVersion || ctx.installed.source !== "control-plane" || !ctx.tenant || !ctx.poolId) return versions;
410
+ const runs = runtimeVersion ? `runs v${runtimeVersion}` : "runs an untracked runtime copy";
411
+ return {
412
+ ...versions,
413
+ runtimeBehind: {
414
+ reason: "runtime_behind",
415
+ message: `The pool job ${runs}, but this CLI is v${cliVersion}. bb update refreshes it; so does the next command.`,
416
+ next: `bb staging-pool refresh-runtime --tenant ${shellWord(String(ctx.tenant))} --pool ${shellWord(String(ctx.poolId))}`,
417
+ },
418
+ };
419
+ }
420
+
401
421
  export async function runStagingPool(argv, deps = {}) {
402
422
  const {
403
423
  env = process.env,
@@ -415,6 +435,7 @@ export async function runStagingPool(argv, deps = {}) {
415
435
  nodeBin = process.execPath,
416
436
  sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
417
437
  readSecretFile = readFileSync,
438
+ cliVersion = null,
418
439
  } = deps;
419
440
 
420
441
  const redactor = createRedactor();
@@ -599,10 +620,10 @@ export async function runStagingPool(argv, deps = {}) {
599
620
  const entry = options.persona ? findPersona(ctx.poolBinding, options.persona) : null;
600
621
  if (options.persona && !entry) assertPersonaAllowed(target, options.persona);
601
622
  const health = await assessHealth({ ctx, pool, now, exec, uid, requestedPersona: entry?.email });
602
- emit({ command: "status", check: true, ...health, ...upgradeNotice(ctx) });
623
+ emit({ command: "status", check: true, ...health, ...upgradeNotice(ctx), ...runtimeNotice(ctx, cliVersion) });
603
624
  return health.ok ? 0 : 5;
604
625
  }
605
- emit({ ok: true, command: "status", ...(await pool.status()), ...upgradeNotice(ctx) });
626
+ emit({ ok: true, command: "status", ...(await pool.status()), ...upgradeNotice(ctx), ...runtimeNotice(ctx, cliVersion) });
606
627
  return 0;
607
628
  },
608
629
 
@@ -96,14 +96,16 @@ ANYONE (agents too) — existing runtime maintenance, never sign-in
96
96
  and this published CLI package. Preserves credentials, sessions, counts,
97
97
  interval and plist. The existing launchd job refills on its next tick.
98
98
  No caller path or runtime content is accepted; no sign-in is performed.
99
+ bb update runs it for every installed pool after a confirmed upgrade.
99
100
 
100
101
  HUMAN ONLY (Jono, from Terminal.app or iTerm) — refused inside a Claude or Codex session
101
102
  These sign in with the persona password or change the standing sessions.
102
103
  install --tenant <slug> [--pool <pool_id>] --botbuddy-bin <absolute path> [--interval <seconds>]
103
104
  Reads the tenant's registered pool from the BotBuddy control plane, copies
104
105
  the job to the pool home, writes its config and loads the launchd job. Run
105
- it from any directory. Re-run after upgrading @botbuddy/cli; it keeps the
106
- counts and interval. --pool is needed only when the tenant has several.
106
+ it from any directory. bb update refreshes the job's runtime, so re-run
107
+ install only when status or bb update names it; it keeps the counts and
108
+ interval. --pool is needed only when the tenant has several.
107
109
  e.g. bb staging-pool install --tenant botbuddy --botbuddy-bin "$(command -v botbuddy)"
108
110
  tick [--pool <pool_id>] [--home <pool home>]
109
111
  One refresh and refill pass now (what the job runs).
@@ -11,14 +11,23 @@
11
11
  // `cmdUpdate` deliberately does NOT update in-process (BOT-1499: a self-update
12
12
  // that replaces its own running files can kill itself mid-install). It hands
13
13
  // off to a detached, unref'd `npm install -g` and exits 0.
14
+ //
15
+ // BOT-3016: an installed staging session pool is a standing launchd job that
16
+ // runs a pinned copy of the CLI, so npm alone leaves it on old code. After a
17
+ // confirmed update, `cmdUpdate` refreshes each control-plane pool with the NEW
18
+ // package's agent-safe `staging-pool refresh-runtime` (BOT-2673), which derives
19
+ // its runtime from its own package path — hence a child of the new binary, not
20
+ // this process. A refusal is reported once and never fails the update.
14
21
 
15
- import { mkdir, readFile, writeFile } from "node:fs/promises";
22
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
16
23
  import { readFileSync } from "node:fs";
17
- import { spawn, spawnSync } from "node:child_process";
24
+ import { execFile, spawn, spawnSync } from "node:child_process";
18
25
  import { homedir } from "node:os";
19
26
  import { dirname, join } from "node:path";
27
+ import { promisify } from "node:util";
20
28
 
21
29
  import { VERSION } from "./version.mjs";
30
+ import { installLine } from "./staging-pool/errors.mjs";
22
31
 
23
32
  export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
24
33
  export const NPM_LATEST_URL = "https://registry.npmjs.org/@botbuddy/cli/latest";
@@ -118,11 +127,16 @@ export async function maybeWarnStale({
118
127
  }
119
128
  }
120
129
 
121
- export function readInstalledVersion({ npmBin = "npm", shellOpt = {}, probeImpl = spawnSync, readPackage = readFileSync } = {}) {
130
+ export function readNpmGlobalRoot({ npmBin = "npm", shellOpt = {}, probeImpl = spawnSync } = {}) {
122
131
  const root = probeImpl(npmBin, ["root", "-g"], { encoding: "utf8", ...shellOpt });
123
132
  if (root?.error || root?.status !== 0) throw new Error(`npm global root lookup failed for ${npmBin}`);
124
133
  const directory = String(root?.stdout ?? "").trim();
125
134
  if (!directory) throw new Error("npm global root lookup returned no directory");
135
+ return directory;
136
+ }
137
+
138
+ export function readInstalledVersion({ npmBin = "npm", shellOpt = {}, probeImpl = spawnSync, readPackage = readFileSync } = {}) {
139
+ const directory = readNpmGlobalRoot({ npmBin, shellOpt, probeImpl });
126
140
  const packageJson = JSON.parse(readPackage(join(directory, "@botbuddy", "cli", "package.json"), "utf8"));
127
141
  if (typeof packageJson?.version !== "string" || !parseSemver(packageJson.version)) throw new Error("installed @botbuddy/cli package has no valid version");
128
142
  return packageJson.version;
@@ -130,7 +144,7 @@ export function readInstalledVersion({ npmBin = "npm", shellOpt = {}, probeImpl
130
144
 
131
145
  // `botbuddy update` waits for npm because the terminal result, rather than the
132
146
  // child-process launch, is the only trustworthy confirmation an update worked.
133
- export function cmdUpdate({ version = VERSION, spawnImpl = spawn, probeImpl = spawnSync, readInstalledVersion: installedVersion = readInstalledVersion, log = (line) => console.log(line), platform = process.platform, stderr = process.stderr } = {}) {
147
+ export function cmdUpdate({ version = VERSION, spawnImpl = spawn, probeImpl = spawnSync, readInstalledVersion: installedVersion = readInstalledVersion, refreshPools = refreshPoolsFromNewPackage, log = (line) => console.log(line), platform = process.platform, stderr = process.stderr } = {}) {
134
148
  // BOT-1566 (Codex P1/P2): on Windows npm is the `npm.cmd` shim, and Node
135
149
  // cannot spawn a .cmd file shell-free (docs) — run it through a shell on
136
150
  // win32 (args are all static constants, so no injection surface) and select
@@ -179,14 +193,114 @@ export function cmdUpdate({ version = VERSION, spawnImpl = spawn, probeImpl = sp
179
193
  resolve(1);
180
194
  return;
181
195
  }
196
+ let installed;
182
197
  try {
183
- const installed = installedVersion({ npmBin, shellOpt });
184
- log(`✓ Updated to v${installed}`);
185
- resolve(0);
198
+ installed = installedVersion({ npmBin, shellOpt });
186
199
  } catch (error) {
187
200
  try { stderr.write(`botbuddy update: npm completed but could not verify the installed version (${error?.message ?? error})\n`); } catch { /* ignore */ }
188
201
  resolve(1);
202
+ return;
189
203
  }
204
+ log(`✓ Updated to v${installed}`);
205
+ // BOT-3016: the update succeeded; a pool refresh problem is reported, never an exit code.
206
+ Promise.resolve()
207
+ .then(() => refreshPools({ installedVersion: installed, npmBin, shellOpt, probeImpl, log, stderr }))
208
+ .catch((error) => {
209
+ try { stderr.write(`botbuddy update: the installed staging pools were not refreshed (${error?.message ?? error}); run bb staging-pool status\n`); } catch { /* ignore */ }
210
+ })
211
+ .finally(() => resolve(0));
190
212
  });
191
213
  });
192
214
  }
215
+
216
+ // ─── BOT-3016: refresh installed staging-pool runtimes after an update ───────
217
+
218
+ export const REFRESH_TIMEOUT_MS = 120_000;
219
+ const HUMAN_NEXT = "human, from Terminal.app or iTerm";
220
+
221
+ export function stagingPoolsRoot(home = homedir()) {
222
+ return join(home, ".botbuddy", "staging-session-pool");
223
+ }
224
+
225
+ function lastJsonLine(text) {
226
+ const lines = String(text ?? "").split("\n").filter((line) => line.startsWith("{"));
227
+ for (const line of lines.reverse()) {
228
+ try { return JSON.parse(line); } catch { /* keep looking */ }
229
+ }
230
+ return null;
231
+ }
232
+
233
+ async function refreshOne({ dirName, installedVersion, cliEntry, nodeBin, home, execFileImpl }) {
234
+ let config;
235
+ try {
236
+ config = JSON.parse(await readFile(join(stagingPoolsRoot(home), dirName, "config.json"), "utf8"));
237
+ } catch (error) {
238
+ if (error?.code === "ENOENT") return null; // not a pool home
239
+ return { poolId: dirName, status: "unreadable", code: "config_unreadable", message: `Its config.json could not be read (${error?.message ?? error}).`, next: "bb staging-pool status --check --pool " + dirName, actor: "agent or human" };
240
+ }
241
+ const poolId = typeof config?.poolId === "string" ? config.poolId : dirName;
242
+ const tenant = config?.tenant ?? config?.binding?.tenant ?? null;
243
+ if (config?.source !== "control-plane" || !tenant) {
244
+ return { poolId, status: "legacy", code: "installed_before_registry", message: "It was installed before the control-plane registry, so only a human re-install can update it.", next: tenant ? installLine(tenant, poolId) : `bb staging-pool status --pool ${poolId}`, actor: HUMAN_NEXT };
245
+ }
246
+ const runtimeVersion = config?.runtime?.version ?? null;
247
+ if (runtimeVersion === installedVersion) return { poolId, status: "current", runtimeVersion };
248
+ const retry = `bb staging-pool refresh-runtime --tenant ${tenant} --pool ${poolId}`;
249
+ try {
250
+ const { stdout } = await execFileImpl(nodeBin, [cliEntry, "staging-pool", "refresh-runtime", "--tenant", tenant, "--pool", poolId], { timeout: REFRESH_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
251
+ const receipt = lastJsonLine(stdout);
252
+ if (receipt?.ok === true) return { poolId, status: "refreshed", runtimeVersion: receipt.runtimeVersion ?? installedVersion };
253
+ return { poolId, status: "refused", code: receipt?.code ?? "refresh_failed", message: receipt?.message ?? "The refresh printed no success receipt.", next: retry, actor: "agent or human" };
254
+ } catch (error) {
255
+ // A refusal is one JSON line on stderr (then text guidance); stdout stays empty.
256
+ const receipt = lastJsonLine(error?.stderr) ?? lastJsonLine(error?.stdout);
257
+ if (receipt?.code) {
258
+ const human = /^human/i.test(String(receipt.actor ?? "")) || /install/.test(String(receipt.next ?? ""));
259
+ const actor = typeof receipt.actor === "string" && receipt.actor ? (human ? `${receipt.actor}, from Terminal.app or iTerm` : receipt.actor) : (human ? HUMAN_NEXT : "agent or human");
260
+ return { poolId, status: "refused", code: receipt.code, message: [receipt.message, receipt.detail].filter(Boolean).join(" "), next: receipt.next ?? retry, actor };
261
+ }
262
+ return { poolId, status: "refused", code: "refresh_failed", message: `The refresh did not finish (${error?.message ?? error}).`, next: retry, actor: "agent or human" };
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Refresh every control-plane staging pool installed under ~/.botbuddy whose
268
+ * recorded runtime differs from `installedVersion`, by running the new
269
+ * package's `staging-pool refresh-runtime`. Reports one line per pool and
270
+ * never throws for a pool's refusal. Returns [{ poolId, status, ... }].
271
+ */
272
+ export async function refreshInstalledPools({
273
+ installedVersion,
274
+ cliEntry,
275
+ nodeBin = process.execPath,
276
+ home = homedir(),
277
+ execFileImpl = promisify(execFile),
278
+ log = (line) => console.log(line),
279
+ stderr = process.stderr,
280
+ } = {}) {
281
+ let entries;
282
+ try {
283
+ entries = await readdir(stagingPoolsRoot(home), { withFileTypes: true });
284
+ } catch (error) {
285
+ if (error?.code === "ENOENT") return [];
286
+ throw error;
287
+ }
288
+ const results = [];
289
+ for (const entry of entries.filter((e) => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
290
+ const result = await refreshOne({ dirName: entry.name, installedVersion, cliEntry, nodeBin, home, execFileImpl });
291
+ if (!result) continue;
292
+ results.push(result);
293
+ if (result.status === "refreshed") log(`✓ Staging pool ${result.poolId} now runs v${result.runtimeVersion}`);
294
+ else if (result.status === "current") log(`✓ Staging pool ${result.poolId} already runs v${result.runtimeVersion}`);
295
+ else {
296
+ try { stderr.write(`! Staging pool ${result.poolId} was not refreshed (${result.code}): ${result.message}\n Next (${result.actor}): ${result.next}\n`); } catch { /* ignore */ }
297
+ }
298
+ }
299
+ return results;
300
+ }
301
+
302
+ // The default for `cmdUpdate`: locate the newly installed package and refresh with it.
303
+ export async function refreshPoolsFromNewPackage({ installedVersion, npmBin, shellOpt, probeImpl, log, stderr }) {
304
+ const root = readNpmGlobalRoot({ npmBin, shellOpt, probeImpl });
305
+ return refreshInstalledPools({ installedVersion, cliEntry: join(root, "@botbuddy", "cli", "bin", "botbuddy.mjs"), log, stderr });
306
+ }