@botbuddy/cli 1.55.0 → 1.56.1

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.55.0",
3
+ "version": "1.56.1",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -158,7 +158,7 @@ export async function hasAgentSessionRuntime({
158
158
  /** Resolve without touching the network when an explicit source or fresh cache exists. */
159
159
  export async function resolveAgentSessionCredential({
160
160
  argv = [], env = process.env, cwd = process.cwd(), readState = readAgentState,
161
- isFresh = isAgentStateFresh, selfHeal = selfHealAgentSession,
161
+ isFresh = isAgentStateFresh, selfHeal = selfHealAgentSession, allowSelfHeal = true,
162
162
  // BOT-1682 (Codex R13): callers that have ALREADY parsed the session id from
163
163
  // their own LEADING options (bb pw's runPwInner) pass it here, so this resolver
164
164
  // never re-scans raw argv — where a target-flag VALUE such as
@@ -190,6 +190,7 @@ export async function resolveAgentSessionCredential({
190
190
  warnLegacyResolvedToken(cached.agent_session_token, warn);
191
191
  return withIdentity({ token: cached.agent_session_token, sessionId: explicitSessionId ?? cached.session_id, agentId: cached.agent_id, source: "cache" }, cached);
192
192
  }
193
+ if (!allowSelfHeal) return null;
193
194
  const healed = await selfHeal({ cwd, env, sessionId: explicitSessionId });
194
195
  warnLegacyResolvedToken(healed?.token, warn);
195
196
  return healed;
package/src/commands.mjs CHANGED
@@ -31,7 +31,7 @@ import { deliverExecutionEvent } from "./telemetry-delivery.mjs";
31
31
  import { doctorAgentAuth, doctorHelp, parseDoctorArgs, readClientKeyStrict, repairFailureRecovery } from "./agent-doctor.mjs";
32
32
  import { formatPublishIp, isLoopbackIp, probeEnginePublishIp, publishIpFix } from "./docker-publish-ip.mjs";
33
33
  import { cmdHost, fixHelperWorkers, formatHelperWorkersLines, inspectHelperWorkers } from "./helper-host.mjs";
34
- import { clearAgentState, selfHealAgentSession } from "./agent-session.mjs";
34
+ import { clearAgentState, resolveAgentSessionCredential, selfHealAgentSession } from "./agent-session.mjs";
35
35
  import { readAgentState } from "./agent-state.mjs";
36
36
  import { runUsage, usageStatus } from "./usage/sync.mjs";
37
37
  import { cmdTestIngest, doctorTestIngest, formatTestIngestLine } from "./test-ingest.mjs";
@@ -931,6 +931,10 @@ export async function cmdBootstrap(args, {
931
931
  let harnessSessionId = null;
932
932
  let type = "custom";
933
933
  let codexThreadId = null;
934
+ // Claude Desktop injects this only for a desktop-hosted agent session. The
935
+ // CLI deliberately treats every other environment as absent, so terminal and
936
+ // cloud Claude sessions never manufacture a deep link.
937
+ const claudeDesktopSessionId = process.env.CLAUDE_CODE_HOST_SESSION_ID || null;
934
938
  let branch = null;
935
939
  let ticketId = null;
936
940
  let ticketUrl = null;
@@ -969,6 +973,7 @@ export async function cmdBootstrap(args, {
969
973
  type,
970
974
  harnessSessionId,
971
975
  codexThreadId,
976
+ claudeDesktopSessionId,
972
977
  branch,
973
978
  ticketId,
974
979
  ticketUrl,
@@ -1163,8 +1168,24 @@ export async function cmdLogout({ cwd = process.cwd(), clearState = clearAgentSt
1163
1168
  console.log(`${green("✓")} Logged out. Credentials removed.`);
1164
1169
  }
1165
1170
 
1166
- function cmdHeartbeat(args) {
1167
- return args[0] ? callTool("heartbeat", { current_task: args[0] }) : callTool("heartbeat");
1171
+ export async function cmdHeartbeat(args, {
1172
+ resolveSession = resolveAgentSessionCredential,
1173
+ callTool: call = callTool,
1174
+ } = {}) {
1175
+ // A Desktop identity belongs to the work-graph session that is currently
1176
+ // running. Supply the cached session id with the host-provided value so the
1177
+ // server refreshes that exact session rather than only mutable agent state.
1178
+ const session = await resolveSession({
1179
+ sessionId: process.env.BOTBUDDY_SESSION_ID || null,
1180
+ allowSelfHeal: false,
1181
+ });
1182
+ const claudeDesktopSessionId = process.env.CLAUDE_CODE_HOST_SESSION_ID || null;
1183
+ const heartbeat = {
1184
+ ...(args[0] ? { current_task: args[0] } : {}),
1185
+ ...(typeof session?.sessionId === "string" && session.sessionId ? { session_id: session.sessionId } : {}),
1186
+ ...(claudeDesktopSessionId ? { claude_desktop_session_id: claudeDesktopSessionId } : {}),
1187
+ };
1188
+ return call("heartbeat", heartbeat, session?.token ? { auth: { "x-agent-api-key": session.token } } : undefined);
1168
1189
  }
1169
1190
 
1170
1191
  // ─── BOT-1607: `botbuddy mcp` — the tier-2 bb_mcp_ config key ────────────────
@@ -0,0 +1,29 @@
1
+ {
2
+ "ticket": "BOT-2403",
3
+ "captured_at": "2026-10-16T20:43:49Z",
4
+ "provenance": "Actual Docker Desktop container inspect .Mounts and context inspect on owned MacBook lease 0c68ff71-3a1a-4d3c-b7d2-d06617609772; requester and container destination both contained43 entries; actual MCP readyz HTTP200/version2.97.1. No environment/credentials captured.",
5
+ "project_id": "bbbot240305b549",
6
+ "host_functions_dir": "/Users/jono/Developer/botbuddy-web/.claude/worktrees/bot-2402/.botbuddy/stacks/bbbot240305b549/supabase/functions",
7
+ "docker_context": "desktop-linux",
8
+ "docker_endpoint": "unix:///Users/jono/.docker/run/docker.sock",
9
+ "mounts": [
10
+ {
11
+ "Type": "volume",
12
+ "Name": "supabase_edge_runtime_bbbot240305b549",
13
+ "Source": "/var/lib/docker/volumes/supabase_edge_runtime_bbbot240305b549/_data",
14
+ "Destination": "/root/.cache/deno",
15
+ "Driver": "local",
16
+ "Mode": "rw",
17
+ "RW": true,
18
+ "Propagation": ""
19
+ },
20
+ {
21
+ "Type": "bind",
22
+ "Source": "/host_mnt/Users/jono/Developer/botbuddy-web/.claude/worktrees/bot-2402/.botbuddy/stacks/bbbot240305b549/supabase/functions",
23
+ "Destination": "/Users/jono/Developer/botbuddy-web/.claude/worktrees/bot-2402/.botbuddy/stacks/bbbot240305b549/supabase/functions",
24
+ "Mode": "ro",
25
+ "RW": false,
26
+ "Propagation": "rprivate"
27
+ }
28
+ ]
29
+ }
@@ -130,6 +130,7 @@ async function recordOrphanAndFail({ session, cwd, retirementError, primaryError
130
130
  export async function heartbeatHarnessSession({
131
131
  session,
132
132
  codexThreadId = null,
133
+ claudeDesktopSessionId = null,
133
134
  model = null,
134
135
  call = callToolJson,
135
136
  cwd = process.cwd(),
@@ -141,6 +142,7 @@ export async function heartbeatHarnessSession({
141
142
  const response = await call("heartbeat", {
142
143
  session_id: session.session_id,
143
144
  ...(codexThreadId ? { codex_thread_id: codexThreadId } : {}),
145
+ ...(claudeDesktopSessionId ? { claude_desktop_session_id: claudeDesktopSessionId } : {}),
144
146
  ...(model ? { model } : {}),
145
147
  }, { auth: { "x-agent-api-key": session.session_token } });
146
148
  if (!response?.ok || response.isError) {
@@ -212,6 +214,7 @@ export async function bootstrapHarnessSession({
212
214
  type = "codex",
213
215
  harnessSessionId,
214
216
  codexThreadId = null,
217
+ claudeDesktopSessionId = null,
215
218
  cwd = process.cwd(),
216
219
  branch = null,
217
220
  ticketId = null,
@@ -273,6 +276,7 @@ export async function bootstrapHarnessSession({
273
276
  ...(prNumber != null ? { pr_number: prNumber } : {}),
274
277
  ...(prUrl ? { pr_url: prUrl } : {}),
275
278
  ...(codexThreadId ? { codex_thread_id: codexThreadId } : {}),
279
+ ...(claudeDesktopSessionId ? { claude_desktop_session_id: claudeDesktopSessionId } : {}),
276
280
  ...(model ? { model } : {}),
277
281
  ...(effort ? { effort } : {}),
278
282
  harness,
@@ -38,20 +38,41 @@ export function runDocker(args, { timeoutMs = MOUNT_CHECK_TIMEOUT_MS } = {}) {
38
38
  }
39
39
 
40
40
  /** The container path the functions directory is mounted at, or null (from `docker inspect … .Mounts`). */
41
- export function functionsMountDestination(mountsJson, hostFunctionsDir) {
41
+ export function functionsMountDestination(mountsJson, hostFunctionsDir, { dockerDesktop = false } = {}) {
42
42
  let mounts;
43
43
  try { mounts = JSON.parse(String(mountsJson ?? "").trim()); } catch { return null; }
44
44
  if (!Array.isArray(mounts)) return null;
45
- const mount = mounts.find((m) => m?.Type === "bind" && m?.Source === hostFunctionsDir);
45
+ const mount = mounts.find((m) => m?.Type === "bind" && (
46
+ m?.Source === hostFunctionsDir || (
47
+ dockerDesktop === true && hostFunctionsDir.startsWith("/Users/") &&
48
+ m?.Source === `/host_mnt${hostFunctionsDir}`
49
+ )
50
+ ));
46
51
  return typeof mount?.Destination === "string" && mount.Destination ? mount.Destination : null;
47
52
  }
48
53
 
54
+ // Docker Desktop exposes a macOS bind through its VM's /host_mnt alias.
55
+ // Authorize that alias only for the selected engine, never an arbitrary remote
56
+ // daemon or another VM. Explicit context selection wins over endpoint metadata,
57
+ // exactly as dockerSelectorArgs does. A failed probe keeps the gate closed.
58
+ async function isSelectedDockerDesktop(target, run, platform) {
59
+ if (platform !== "darwin") return false;
60
+ const context = typeof target?.context === "string" && target.context ? target.context : null;
61
+ let endpoint = context ? null : target?.resolved_endpoint ?? target?.endpoint;
62
+ if (!endpoint) {
63
+ const result = await run(["context", "inspect", ...(context ? [context] : []), "--format", "{{.Endpoints.docker.Host}}"]);
64
+ if (result.code !== 0 || result.timedOut) return false;
65
+ endpoint = String(result.stdout ?? "").trim();
66
+ }
67
+ return typeof endpoint === "string" && /^unix:\/\/\/Users\/[^/]+\/\.docker\/run\/docker\.sock$/.test(endpoint);
68
+ }
69
+
49
70
  /**
50
71
  * Check (and once repair) the functions mount of a re-adopted idle lease.
51
72
  * `lease` is the re-adoption answer: `{ execution_mode, connection: { project_id, botbuddy_docker_target? } }`.
52
73
  * Returns `{ ok: true, skipped? , restarted? }` or `{ ok: false, restarted, reason }`. Never throws.
53
74
  */
54
- export async function checkReadoptedStackMounts(lease, stackDir, { run = runDocker, listHost = (dir) => readdirSync(dir), log = () => {} } = {}) {
75
+ export async function checkReadoptedStackMounts(lease, stackDir, { run = runDocker, listHost = (dir) => readdirSync(dir), log = () => {}, platform = process.platform } = {}) {
55
76
  if (!CO_LOCATED_MODES.has(lease?.execution_mode)) return { ok: true, skipped: "remote" };
56
77
  const projectId = lease?.connection?.project_id;
57
78
  if (typeof projectId !== "string" || !PROJECT_ID_RE.test(projectId)) return { ok: true, skipped: "no_project_id" };
@@ -72,7 +93,13 @@ export async function checkReadoptedStackMounts(lease, stackDir, { run = runDock
72
93
  if (!inspect.timedOut && /No such (container|object)/i.test(inspect.stderr ?? "")) return { ok: true, skipped: "no_edge_runtime" };
73
94
  return { ok: false, restarted: false, reason: `could not inspect ${container} (exit ${inspect.code}${inspect.timedOut ? ", timed out" : ""})` };
74
95
  }
75
- const destination = functionsMountDestination(inspect.stdout, hostFunctions);
96
+ let destination = functionsMountDestination(inspect.stdout, hostFunctions);
97
+ if (!destination) {
98
+ const desktopDestination = functionsMountDestination(inspect.stdout, hostFunctions, { dockerDesktop: true });
99
+ if (desktopDestination && await isSelectedDockerDesktop(lease.connection.botbuddy_docker_target, run, platform)) {
100
+ destination = desktopDestination;
101
+ }
102
+ }
76
103
  if (!destination) return { ok: false, restarted: false, reason: `${container} does not bind-mount ${hostFunctions}` };
77
104
 
78
105
  const mountedEntries = async () => {