codeam-cli 2.59.0 → 2.60.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/index.js +777 -112
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,52 @@ All notable changes to `codeam-cli` are documented here.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.60.0] — 2026-07-08
8
+
9
+ ### Added
10
+
11
+ - **cli:** Baton local-session gate + feature flag
12
+ - **cli:** BatonController turn-safe single-driver state machine
13
+ - **cli:** TranscriptMirror emits JSONL deltas for the read-only view
14
+ - **cli:** NativeTuiDriver over AgentService with idle turn-boundary
15
+ - **cli:** AcpDriver over AcpClient (spawn + loadSession resume)
16
+ - **cli:** PostBatonEvent publisher driver-state
17
+ - **cli:** Take_control/handback relay handlers
18
+ - **cli:** Wire local baton branch before the ACP fork (flag-gated)
19
+ - **cli:** Make the session baton drivable after a hand-off
20
+ - **cli:** Enable the session baton by default for local sessions (CODEAM_BATON=0 kill switch)
21
+ - **shared:** Add baton_state to USER_EVENTS (session baton wire type)
22
+
23
+ ### Changed
24
+
25
+ - **cli:** Extract shared ACP command-context assembler + proxy-relaunch helper
26
+
27
+ ### Fixed
28
+
29
+ - **cli:** Mark SessionDriver.kind readonly (contract)
30
+ - **cli:** Baton switch-failure recovery + ack-on-failure; test cleanups
31
+ - **cli:** EncodeCwd underscore-collapse + ClaudeRuntimeStrategy.resolveHistoryFile (baton mirror)
32
+ - **cli:** AcpClient.start cleans up on handshake failure (baton take-control recovery)
33
+
34
+ ### Tests
35
+
36
+ - **cli:** Prove noteOutput resets the idle window + cover throw/undefined branches
37
+ - **cli:** Gated cross-mode-resume integration test (real claude)
38
+
39
+ ## [2.59.0] — 2026-07-08
40
+
41
+ ### Added
42
+
43
+ - **vsc-plugin:** Route stop_task through AgentStrategyRegistry, alias escape_key
44
+ - **vsc-plugin:** CopilotLmStrategy.stop() cancels the active LM stream
45
+ - **jetbrains-plugin:** Alias escape_key to stop_task
46
+ - **jetbrains-plugin:** Best-effort surface-interrupt helper for GUI agents
47
+ - **jetbrains-plugin:** Concrete strategies interrupt their surface on stop()
48
+
49
+ ### Tests
50
+
51
+ - **cli:** Regression-guard stopTaskH cancels ACP turn and acks once
52
+
7
53
  ## [2.58.1] — 2026-07-08
8
54
 
9
55
  ### CI
package/dist/index.js CHANGED
@@ -559,7 +559,8 @@ var USER_EVENTS = {
559
559
  AGENT_INSTALL_PROGRESS: "agent_install_progress",
560
560
  AGENT_INSTALL_FAILED: "agent_install_failed",
561
561
  CLI_UPDATE_PROGRESS: "cli_update_progress",
562
- CLI_UPDATE_FAILED: "cli_update_failed"
562
+ CLI_UPDATE_FAILED: "cli_update_failed",
563
+ BATON_STATE: "baton_state"
563
564
  };
564
565
 
565
566
  // ../../packages/shared/src/preview-prompts.ts
@@ -5652,7 +5653,7 @@ function readAnonId() {
5652
5653
  }
5653
5654
  function superProperties() {
5654
5655
  return {
5655
- cliVersion: true ? "2.59.0" : "0.0.0-dev",
5656
+ cliVersion: true ? "2.60.1" : "0.0.0-dev",
5656
5657
  nodeVersion: process.version,
5657
5658
  platform: process.platform,
5658
5659
  arch: process.arch,
@@ -5833,7 +5834,7 @@ var os4 = __toESM(require("os"));
5833
5834
  // package.json
5834
5835
  var package_default = {
5835
5836
  name: "codeam-cli",
5836
- version: "2.59.0",
5837
+ version: "2.60.1",
5837
5838
  description: "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device \u2014 async. The terminal companion for CodeAgent Mobile.",
5838
5839
  type: "commonjs",
5839
5840
  main: "dist/index.js",
@@ -6114,6 +6115,29 @@ async function postPreviewEvent(input) {
6114
6115
  };
6115
6116
  }
6116
6117
  }
6118
+ async function postBatonEvent(input) {
6119
+ try {
6120
+ await _transport.postJsonAuthed(
6121
+ `${API_BASE}/api/baton/events`,
6122
+ {
6123
+ sessionId: input.sessionId,
6124
+ pluginId: input.pluginId,
6125
+ state: input.state,
6126
+ driver: input.driver,
6127
+ conversationId: input.conversationId
6128
+ },
6129
+ input.pluginAuthToken
6130
+ );
6131
+ return { ok: true };
6132
+ } catch (err) {
6133
+ const e = err;
6134
+ return {
6135
+ ok: false,
6136
+ status: typeof e.statusCode === "number" ? e.statusCode : 0,
6137
+ message: e.message || "unknown"
6138
+ };
6139
+ }
6140
+ }
6117
6141
  async function postHeadroomEvent(input) {
6118
6142
  try {
6119
6143
  await _transport.postJsonAuthed(
@@ -6881,7 +6905,7 @@ var CommandRelayService = class {
6881
6905
  // fresh + clear the "CLI update available" banner after a self-update
6882
6906
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
6883
6907
  // pair/reconnect). Older backends ignore the extra field.
6884
- ..."2.59.0" ? { ideVersion: "2.59.0" } : {}
6908
+ ..."2.60.1" ? { ideVersion: "2.60.1" } : {}
6885
6909
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
6886
6910
  }
6887
6911
  /**
@@ -12097,7 +12121,7 @@ var fs16 = __toESM(require("fs"));
12097
12121
  var path19 = __toESM(require("path"));
12098
12122
  var os14 = __toESM(require("os"));
12099
12123
  function encodeCwd(cwd) {
12100
- return cwd.replace(/[\\/:]/g, "-");
12124
+ return cwd.replace(/[\\/:_]/g, "-");
12101
12125
  }
12102
12126
  function resolveHistoryDir(cwd, projectsRoot) {
12103
12127
  const root = projectsRoot ?? path19.join(os14.homedir(), ".claude", "projects");
@@ -12117,6 +12141,12 @@ function resolveHistoryDir(cwd, projectsRoot) {
12117
12141
  }
12118
12142
  return null;
12119
12143
  }
12144
+ function resolveHistoryFile(cwd, sessionId, projectsRoot) {
12145
+ const dir = resolveHistoryDir(cwd, projectsRoot);
12146
+ if (!dir) return null;
12147
+ const filePath = path19.join(dir, `${sessionId}.jsonl`);
12148
+ return fs16.existsSync(filePath) ? filePath : null;
12149
+ }
12120
12150
  function getCurrentUsage(historyDir, bootTimeMs = 0) {
12121
12151
  const GRACE_MS = 5e3;
12122
12152
  const cutoff = bootTimeMs > 0 ? bootTimeMs - GRACE_MS : 0;
@@ -12350,6 +12380,9 @@ var ClaudeRuntimeStrategy = class {
12350
12380
  resolveHistoryDir(cwd) {
12351
12381
  return resolveHistoryDir(cwd);
12352
12382
  }
12383
+ resolveHistoryFile(cwd, sessionId) {
12384
+ return resolveHistoryFile(cwd, sessionId);
12385
+ }
12353
12386
  parseHistoryFile(filePath) {
12354
12387
  return parseHistoryFile(filePath);
12355
12388
  }
@@ -12794,7 +12827,7 @@ function listResumableSessions2(cwd, homeOverride) {
12794
12827
  out2.sort((a, b) => b.timestamp - a.timestamp);
12795
12828
  return out2;
12796
12829
  }
12797
- function resolveHistoryFile(cwd, sessionId, homeOverride) {
12830
+ function resolveHistoryFile2(cwd, sessionId, homeOverride) {
12798
12831
  const home = homeOverride ?? import_node_os.default.homedir();
12799
12832
  const sessionsRoot = import_node_path2.default.join(home, ".codex", "sessions");
12800
12833
  if (!import_node_fs3.default.existsSync(sessionsRoot)) return null;
@@ -13017,7 +13050,7 @@ var CodexRuntimeStrategy = class {
13017
13050
  return parseHistoryFile2(filePath);
13018
13051
  }
13019
13052
  resolveHistoryFile(cwd, sessionId) {
13020
- return resolveHistoryFile(cwd, sessionId);
13053
+ return resolveHistoryFile2(cwd, sessionId);
13021
13054
  }
13022
13055
  getCurrentUsage(historyDir) {
13023
13056
  return getCurrentUsage2(historyDir);
@@ -15888,7 +15921,7 @@ async function autoUpgradeBeforeCriticalCommand() {
15888
15921
  if (process.env.NODE_ENV === "test") return;
15889
15922
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
15890
15923
  if (process.env.CI) return;
15891
- const current = true ? "2.59.0" : null;
15924
+ const current = true ? "2.60.1" : null;
15892
15925
  if (!current) return;
15893
15926
  const cache = readCache();
15894
15927
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -15905,7 +15938,7 @@ function checkForUpdates() {
15905
15938
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
15906
15939
  if (process.env.CI) return;
15907
15940
  if (!process.stdout.isTTY) return;
15908
- const current = true ? "2.59.0" : null;
15941
+ const current = true ? "2.60.1" : null;
15909
15942
  if (!current) return;
15910
15943
  const cache = readCache();
15911
15944
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -15925,7 +15958,7 @@ var SELF_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
15925
15958
  var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
15926
15959
  var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
15927
15960
  function currentCliVersion() {
15928
- return true ? "2.59.0" : null;
15961
+ return true ? "2.60.1" : null;
15929
15962
  }
15930
15963
  function runCmd(cmd, args2, timeoutMs) {
15931
15964
  return new Promise((resolve7) => {
@@ -19663,6 +19696,32 @@ var envWriteH = async (ctx, cmd, parsed) => {
19663
19696
  await ctx.relay.sendResult(cmd.id, "failed", { error: err.message });
19664
19697
  }
19665
19698
  };
19699
+ var takeControlH = async (ctx, cmd) => {
19700
+ if (!ctx.baton) {
19701
+ await ctx.relay.sendResult(cmd.id, "failed", { code: "NO_BATON" });
19702
+ return;
19703
+ }
19704
+ try {
19705
+ await ctx.baton.takeControl();
19706
+ } catch {
19707
+ await ctx.relay.sendResult(cmd.id, "failed", { code: "BATON_SWITCH_FAILED" });
19708
+ return;
19709
+ }
19710
+ await ctx.relay.sendResult(cmd.id, "completed", { state: ctx.baton.state });
19711
+ };
19712
+ var handbackH = async (ctx, cmd) => {
19713
+ if (!ctx.baton) {
19714
+ await ctx.relay.sendResult(cmd.id, "failed", { code: "NO_BATON" });
19715
+ return;
19716
+ }
19717
+ try {
19718
+ await ctx.baton.handback();
19719
+ } catch {
19720
+ await ctx.relay.sendResult(cmd.id, "failed", { code: "BATON_SWITCH_FAILED" });
19721
+ return;
19722
+ }
19723
+ await ctx.relay.sendResult(cmd.id, "completed", { state: ctx.baton.state });
19724
+ };
19666
19725
  var _activeReporter = null;
19667
19726
  var _headroomEmitChain = Promise.resolve();
19668
19727
  var headroomConfigureH = async (ctx, cmd, parsed) => {
@@ -20503,6 +20562,8 @@ var handlers = {
20503
20562
  save_preview_config: savePreviewConfigH,
20504
20563
  env_read: envReadH,
20505
20564
  env_write: envWriteH,
20565
+ take_control: takeControlH,
20566
+ handback: handbackH,
20506
20567
  headroom_configure: headroomConfigureH,
20507
20568
  headroom_budget: headroomBudgetH,
20508
20569
  beads_configure: beadsConfigureH,
@@ -24816,62 +24877,85 @@ var AcpClient = class {
24816
24877
  this.opts.onStderr?.(`Failed to launch ${adapter.command}: ${code}${hint}`);
24817
24878
  this.opts.onUnexpectedExit?.(null, null);
24818
24879
  });
24819
- if (!child.stdin || !child.stdout) {
24820
- throw new Error("Spawned ACP adapter is missing stdio handles");
24821
- }
24822
- const input = import_node_stream.Readable.toWeb(child.stdout);
24823
- const output = import_node_stream.Writable.toWeb(child.stdin);
24824
- const stream = ndJsonStream(output, input);
24825
- this.connection = new ClientSideConnection(
24826
- (_agent) => this.buildClient(),
24827
- stream
24828
- );
24829
- log.info("acpClient", "initialize \u2192 sending");
24830
- const initialize = await this.connection.initialize({
24831
- protocolVersion: PROTOCOL_VERSION2,
24832
- clientCapabilities: CLIENT_CAPABILITIES
24833
- });
24834
- log.info(
24835
- "acpClient",
24836
- `initialize \u2190 ok protocolVersion=${initialize.protocolVersion} agentCaps=${JSON.stringify(initialize.agentCapabilities ?? {}).slice(0, 200)}`
24837
- );
24838
- log.info("acpClient", "newSession \u2192 sending");
24839
- let startupTimer;
24840
- const startupFailure = new Promise((_2, reject) => {
24841
- this.startupFailureReject = reject;
24842
- startupTimer = setTimeout(() => {
24843
- const tail = this.recentStderr.slice(-4).join(" | ");
24844
- reject(
24845
- new Error(
24846
- `AGENT_STARTUP_TIMEOUT: ${this.opts.adapter.requiresAgentBinary} did not create a session within ${Math.round(
24847
- NEWSESSION_TIMEOUT_MS / 1e3
24848
- )}s${tail ? ` \u2014 last output: ${tail}` : ""}`
24849
- )
24850
- );
24851
- }, NEWSESSION_TIMEOUT_MS);
24852
- });
24853
- let newSession;
24854
24880
  try {
24855
- newSession = await Promise.race([
24856
- this.connection.newSession({ cwd, mcpServers: [] }),
24857
- startupFailure
24858
- ]);
24859
- } finally {
24860
- if (startupTimer) clearTimeout(startupTimer);
24861
- this.startupFailureReject = null;
24881
+ if (!child.stdin || !child.stdout) {
24882
+ throw new Error("Spawned ACP adapter is missing stdio handles");
24883
+ }
24884
+ const input = import_node_stream.Readable.toWeb(child.stdout);
24885
+ const output = import_node_stream.Writable.toWeb(child.stdin);
24886
+ const stream = ndJsonStream(output, input);
24887
+ this.connection = new ClientSideConnection(
24888
+ (_agent) => this.buildClient(),
24889
+ stream
24890
+ );
24891
+ log.info("acpClient", "initialize \u2192 sending");
24892
+ const initialize = await this.connection.initialize({
24893
+ protocolVersion: PROTOCOL_VERSION2,
24894
+ clientCapabilities: CLIENT_CAPABILITIES
24895
+ });
24896
+ log.info(
24897
+ "acpClient",
24898
+ `initialize \u2190 ok protocolVersion=${initialize.protocolVersion} agentCaps=${JSON.stringify(initialize.agentCapabilities ?? {}).slice(0, 200)}`
24899
+ );
24900
+ log.info("acpClient", "newSession \u2192 sending");
24901
+ let startupTimer;
24902
+ const startupFailure = new Promise((_2, reject) => {
24903
+ this.startupFailureReject = reject;
24904
+ startupTimer = setTimeout(() => {
24905
+ const tail = this.recentStderr.slice(-4).join(" | ");
24906
+ reject(
24907
+ new Error(
24908
+ `AGENT_STARTUP_TIMEOUT: ${this.opts.adapter.requiresAgentBinary} did not create a session within ${Math.round(
24909
+ NEWSESSION_TIMEOUT_MS / 1e3
24910
+ )}s${tail ? ` \u2014 last output: ${tail}` : ""}`
24911
+ )
24912
+ );
24913
+ }, NEWSESSION_TIMEOUT_MS);
24914
+ });
24915
+ let newSession;
24916
+ try {
24917
+ newSession = await Promise.race([
24918
+ this.connection.newSession({ cwd, mcpServers: [] }),
24919
+ startupFailure
24920
+ ]);
24921
+ } finally {
24922
+ if (startupTimer) clearTimeout(startupTimer);
24923
+ this.startupFailureReject = null;
24924
+ }
24925
+ this.sessionId = newSession.sessionId;
24926
+ const newSessionMeta = newSession;
24927
+ log.info(
24928
+ "acpClient",
24929
+ `newSession \u2190 ok sessionId=${newSession.sessionId.slice(0, 8)} model=${newSessionMeta.currentModelId ?? "?"} tier=${newSessionMeta.currentServiceTier ?? "?"}`
24930
+ );
24931
+ return {
24932
+ sessionId: newSession.sessionId,
24933
+ initialize,
24934
+ model: newSessionMeta.currentModelId,
24935
+ tier: newSessionMeta.currentServiceTier
24936
+ };
24937
+ } catch (err) {
24938
+ this.cleanupAfterFailedStart(child);
24939
+ throw err;
24862
24940
  }
24863
- this.sessionId = newSession.sessionId;
24864
- const newSessionMeta = newSession;
24865
- log.info(
24866
- "acpClient",
24867
- `newSession \u2190 ok sessionId=${newSession.sessionId.slice(0, 8)} model=${newSessionMeta.currentModelId ?? "?"} tier=${newSessionMeta.currentServiceTier ?? "?"}`
24868
- );
24869
- return {
24870
- sessionId: newSession.sessionId,
24871
- initialize,
24872
- model: newSessionMeta.currentModelId,
24873
- tier: newSessionMeta.currentServiceTier
24874
- };
24941
+ }
24942
+ /**
24943
+ * Undo the partial state `start()` set before its handshake threw, so a
24944
+ * fresh `start()` call is retryable instead of permanently hard-throwing
24945
+ * 'AcpClient already started'. Removes OUR `exit`/`error` listeners first
24946
+ * so killing the half-started child doesn't also fire
24947
+ * `onUnexpectedExit` — the baton's `onUnexpectedExit` calls
24948
+ * `process.exit`, which must never fire for a failure the caller already
24949
+ * received as a thrown error from `start()`.
24950
+ */
24951
+ cleanupAfterFailedStart(child) {
24952
+ child.removeAllListeners("exit");
24953
+ child.removeAllListeners("error");
24954
+ killQuiet(child, "SIGKILL");
24955
+ this.child = null;
24956
+ this.connection = null;
24957
+ this.sessionId = null;
24958
+ this.startupFailureReject = null;
24875
24959
  }
24876
24960
  /**
24877
24961
  * Send a user prompt to the active session. Returns the
@@ -25177,6 +25261,38 @@ function expandPathForAgentBinaries(existingPath) {
25177
25261
  return [...additions, existingPath].filter((p2) => p2.length > 0).join(path58.delimiter);
25178
25262
  }
25179
25263
 
25264
+ // src/agents/acp/headroom-budget-proxy.ts
25265
+ function buildRelaunchProxyEnv(baseEnv) {
25266
+ const env = { ...baseEnv, HEADROOM_KOMPRESS_BACKEND: "onnx_cpu" };
25267
+ delete env["HEADROOM_BUDGET"];
25268
+ delete env["HEADROOM_BUDGET_PERIOD"];
25269
+ return env;
25270
+ }
25271
+ var relaunchProxyWithoutBudget = async () => {
25272
+ const { spawn: spawn38 } = await import("child_process");
25273
+ killHeadroomProxy();
25274
+ await new Promise((r) => setTimeout(r, 500));
25275
+ const proxyEnv = buildRelaunchProxyEnv(process.env);
25276
+ try {
25277
+ const proxy = spawn38(
25278
+ "headroom",
25279
+ ["proxy", "--port", "8787"],
25280
+ { stdio: "ignore", detached: true, env: proxyEnv }
25281
+ );
25282
+ proxy.once("error", (e) => {
25283
+ log.warn("acpRunner", `budget recovery proxy relaunch error (best-effort): ${e.message}`);
25284
+ });
25285
+ proxy.unref();
25286
+ writeHeadroomProxyPidfile(proxy.pid);
25287
+ } catch (e) {
25288
+ log.warn(
25289
+ "acpRunner",
25290
+ `budget recovery proxy relaunch failed (best-effort): ${e instanceof Error ? e.message : String(e)}`
25291
+ );
25292
+ }
25293
+ await new Promise((r) => setTimeout(r, 3e3));
25294
+ };
25295
+
25180
25296
  // src/services/streaming/transport.ts
25181
25297
  var http7 = __toESM(require("http"));
25182
25298
  var https8 = __toESM(require("https"));
@@ -26063,7 +26179,7 @@ function defaultRunGit(cwd, args2) {
26063
26179
  });
26064
26180
  }
26065
26181
  async function discoverRepos(workingDir, maxDepth = 4) {
26066
- const fs60 = await import("fs/promises");
26182
+ const fs61 = await import("fs/promises");
26067
26183
  const out2 = [];
26068
26184
  await walk(workingDir, 0);
26069
26185
  return out2;
@@ -26071,7 +26187,7 @@ async function discoverRepos(workingDir, maxDepth = 4) {
26071
26187
  if (depth > maxDepth) return;
26072
26188
  let entries = [];
26073
26189
  try {
26074
- const dirents = await fs60.readdir(dir, { withFileTypes: true });
26190
+ const dirents = await fs61.readdir(dir, { withFileTypes: true });
26075
26191
  entries = dirents.filter((d3) => !d3.name.startsWith(".") || d3.name === ".git").map((d3) => ({ name: d3.name, isDirectory: d3.isDirectory() }));
26076
26192
  } catch {
26077
26193
  return;
@@ -26710,6 +26826,9 @@ function formatAgentReplyLine(text) {
26710
26826
  }
26711
26827
 
26712
26828
  // src/agents/acp/command-handlers.ts
26829
+ function assembleAcpCommandContext(session, cmd) {
26830
+ return { ...session, cmd };
26831
+ }
26713
26832
  var ACP_QUICK_REPLIES = ["Continue", "Yes, go ahead", "Explain"];
26714
26833
  async function recoverFromFailedTurn(client2, streaming) {
26715
26834
  await cancelStuckTurn(client2);
@@ -27680,12 +27799,6 @@ async function surfaceStartupFailure(opts) {
27680
27799
  );
27681
27800
  errRelay.start();
27682
27801
  }
27683
- function buildRelaunchProxyEnv(baseEnv) {
27684
- const env = { ...baseEnv, HEADROOM_KOMPRESS_BACKEND: "onnx_cpu" };
27685
- delete env["HEADROOM_BUDGET"];
27686
- delete env["HEADROOM_BUDGET_PERIOD"];
27687
- return env;
27688
- }
27689
27802
  function computeAdapterExtraEnv(params) {
27690
27803
  const env = {};
27691
27804
  if (params.disable1mContext) env.CLAUDE_CODE_DISABLE_1M_CONTEXT = "1";
@@ -27802,30 +27915,6 @@ async function runAcpSession(opts) {
27802
27915
  };
27803
27916
  const client2 = new AcpClient(clientOptions);
27804
27917
  let _budgetReachedPosted = false;
27805
- const relaunchProxyWithoutBudget = async () => {
27806
- const { spawn: spawn38 } = await import("child_process");
27807
- killHeadroomProxy();
27808
- await new Promise((r) => setTimeout(r, 500));
27809
- const proxyEnv = buildRelaunchProxyEnv(process.env);
27810
- try {
27811
- const proxy = spawn38(
27812
- "headroom",
27813
- ["proxy", "--port", "8787"],
27814
- { stdio: "ignore", detached: true, env: proxyEnv }
27815
- );
27816
- proxy.once("error", (e) => {
27817
- log.warn("acpRunner", `budget recovery proxy relaunch error (best-effort): ${e.message}`);
27818
- });
27819
- proxy.unref();
27820
- writeHeadroomProxyPidfile(proxy.pid);
27821
- } catch (e) {
27822
- log.warn(
27823
- "acpRunner",
27824
- `budget recovery proxy relaunch failed (best-effort): ${e instanceof Error ? e.message : String(e)}`
27825
- );
27826
- }
27827
- await new Promise((r) => setTimeout(r, 3e3));
27828
- };
27829
27918
  const budgetRecovery = createBudgetRecovery({
27830
27919
  publishText: (text) => publisher.publishOutput({ type: "text", content: text, done: true }),
27831
27920
  publishSelectPrompt: (question, options) => publisher.publishOutput({
@@ -27993,8 +28082,7 @@ async function runAcpSession(opts) {
27993
28082
  });
27994
28083
  }
27995
28084
  async function handleCommand(cmd, client2, relay, acpSessionId, models, streaming, opts, history, jsonlHistory, agentCaps, turnFiles, getBeads, publisher, recentStderr, budgetRecovery, budgetReachedFlag) {
27996
- await dispatchAcpCommand({
27997
- cmd,
28085
+ const session = {
27998
28086
  client: client2,
27999
28087
  relay,
28000
28088
  acpSessionId,
@@ -28010,7 +28098,8 @@ async function handleCommand(cmd, client2, relay, acpSessionId, models, streamin
28010
28098
  recentStderr,
28011
28099
  budgetRecovery,
28012
28100
  budgetReachedFlag
28013
- });
28101
+ };
28102
+ await dispatchAcpCommand(assembleAcpCommandContext(session, cmd));
28014
28103
  }
28015
28104
  function buildBannerSubtitle(agentId, acpSessionId, model, tier) {
28016
28105
  const meta = AGENT_REGISTRY[agentId];
@@ -29017,8 +29106,568 @@ function fetchQuotaUsage(runtime, historySvc) {
29017
29106
  });
29018
29107
  }
29019
29108
 
29020
- // src/agents/claude/onboarding.ts
29109
+ // src/baton/gate.ts
29110
+ function isLocalSession(env = process.env) {
29111
+ return env.CODESPACES !== "true" && env.CODEAM_AUTO_APPROVE !== "1" && env.HEADROOM_ENABLED !== "1" && !env.CODEAM_AUTO_TOKEN && !env.CODEAM_ENROLL_TOKEN;
29112
+ }
29113
+
29114
+ // src/baton/baton-controller.ts
29115
+ var BatonController = class {
29116
+ constructor(deps) {
29117
+ this.deps = deps;
29118
+ }
29119
+ deps;
29120
+ _state = "LOCAL_DRIVE";
29121
+ _active = "local_tui";
29122
+ _conversationId = null;
29123
+ get state() {
29124
+ return this._state;
29125
+ }
29126
+ get activeDriver() {
29127
+ return this._active;
29128
+ }
29129
+ /** The driver object currently holding the baton. The baton router forwards
29130
+ * non-baton commands to `activeSessionDriver.dispatch(cmd)`, so whichever side
29131
+ * holds the baton is the one that actually runs the command. Reads `_active`
29132
+ * live, so mid-turn commands during a `SWITCHING` window still target the
29133
+ * pre-switch driver (the switch waits for the turn boundary before flipping). */
29134
+ get activeSessionDriver() {
29135
+ return this._active === "local_tui" ? this.deps.local : this.deps.mobile;
29136
+ }
29137
+ get conversationId() {
29138
+ return this._conversationId;
29139
+ }
29140
+ async begin() {
29141
+ this._conversationId = await this.deps.local.start(void 0);
29142
+ this._active = "local_tui";
29143
+ this.setState("LOCAL_DRIVE");
29144
+ }
29145
+ async takeControl() {
29146
+ await this.switchDriver(
29147
+ "LOCAL_DRIVE",
29148
+ this.deps.local,
29149
+ this.deps.mobile,
29150
+ "MOBILE_DRIVE",
29151
+ "mobile_acp"
29152
+ );
29153
+ }
29154
+ async handback() {
29155
+ await this.switchDriver(
29156
+ "MOBILE_DRIVE",
29157
+ this.deps.mobile,
29158
+ this.deps.local,
29159
+ "LOCAL_DRIVE",
29160
+ "local_tui"
29161
+ );
29162
+ }
29163
+ async shutdown() {
29164
+ await Promise.allSettled([this.deps.local.stop(), this.deps.mobile.stop()]);
29165
+ }
29166
+ async switchDriver(from, current, next, to, nextKind) {
29167
+ if (this._state !== from) return;
29168
+ this.setState("SWITCHING");
29169
+ const priorActive = this._active;
29170
+ const priorConversationId = this._conversationId;
29171
+ try {
29172
+ await current.whenSafeToYield();
29173
+ await current.stop();
29174
+ this._conversationId = await next.start(this._conversationId ?? void 0);
29175
+ this._active = nextKind;
29176
+ this.setState(to);
29177
+ } catch (err) {
29178
+ this._active = priorActive;
29179
+ this._conversationId = priorConversationId;
29180
+ this.setState(from);
29181
+ throw err;
29182
+ }
29183
+ }
29184
+ setState(state) {
29185
+ this._state = state;
29186
+ this.deps.publishState(state, this._active, this._conversationId);
29187
+ }
29188
+ };
29189
+
29190
+ // src/baton/native-tui-driver.ts
29191
+ var NativeTuiDriver = class {
29192
+ constructor(deps) {
29193
+ this.deps = deps;
29194
+ this.agent = deps.agent;
29195
+ this.idleMs = deps.idleMs ?? 750;
29196
+ this.now = deps.now ?? Date.now;
29197
+ this.lastOutput = this.now();
29198
+ this.historySvc = new HistoryService(deps.runtime, deps.opts.pluginId, deps.opts.cwd, {
29199
+ pluginAuthToken: deps.opts.pluginAuthToken
29200
+ });
29201
+ this.outputSvc = new OutputService(
29202
+ deps.opts.sessionId,
29203
+ deps.opts.pluginId,
29204
+ (conversationId) => this.historySvc.setCurrentConversationId(conversationId),
29205
+ (reset) => this.historySvc.setRateLimitReset(reset),
29206
+ void 0,
29207
+ void 0,
29208
+ deps.opts.pluginAuthToken,
29209
+ deps.runtime
29210
+ );
29211
+ this.keepAliveCtx = { inCodespace: false, codespaceName: void 0 };
29212
+ this.setKeepAlive = buildKeepAlive(this.keepAliveCtx).apply;
29213
+ }
29214
+ deps;
29215
+ kind = "local_tui";
29216
+ agent;
29217
+ idleMs;
29218
+ now;
29219
+ lastOutput;
29220
+ outputSvc;
29221
+ historySvc;
29222
+ setKeepAlive;
29223
+ keepAliveCtx;
29224
+ async start(resumeId) {
29225
+ if (resumeId !== void 0) {
29226
+ await this.agent.restart(resumeId, false);
29227
+ return resumeId;
29228
+ }
29229
+ await this.agent.spawn();
29230
+ const id = this.agent.spawnedSessionId;
29231
+ if (!id) throw new Error("NativeTuiDriver: agent did not expose a session id after spawn");
29232
+ return id;
29233
+ }
29234
+ async stop() {
29235
+ this.agent.kill();
29236
+ }
29237
+ async dispatch(cmd) {
29238
+ const ctx = {
29239
+ outputSvc: this.outputSvc,
29240
+ agent: this.agent,
29241
+ historySvc: this.historySvc,
29242
+ runtime: this.deps.runtime,
29243
+ relay: this.deps.getRelay(),
29244
+ setKeepAlive: this.setKeepAlive,
29245
+ keepAliveCtx: this.keepAliveCtx,
29246
+ pluginId: this.deps.opts.pluginId,
29247
+ sessionId: this.deps.opts.sessionId,
29248
+ agentId: this.deps.opts.agentId,
29249
+ pluginAuthToken: this.deps.opts.pluginAuthToken,
29250
+ beads: this.deps.getBeads()
29251
+ };
29252
+ await dispatchCommand(ctx, cmd);
29253
+ }
29254
+ /** Call on every PTY data chunk: reset the idle timer AND feed the output
29255
+ * pipe so a mobile-routed turn's reply streams back to the app. */
29256
+ handlePtyData(raw) {
29257
+ this.noteOutput();
29258
+ this.outputSvc.push(raw);
29259
+ }
29260
+ /** Reset the idle timer only. Retained for callers that just need the boundary
29261
+ * clock nudged without routing bytes through the output pipe. */
29262
+ noteOutput() {
29263
+ this.lastOutput = this.now();
29264
+ }
29265
+ whenSafeToYield() {
29266
+ return new Promise((resolve7) => {
29267
+ const tick = () => {
29268
+ const quietFor = this.now() - this.lastOutput;
29269
+ if (quietFor >= this.idleMs) resolve7();
29270
+ else setTimeout(tick, this.idleMs - quietFor);
29271
+ };
29272
+ tick();
29273
+ });
29274
+ }
29275
+ };
29276
+
29277
+ // src/baton/acp-driver.ts
29278
+ var import_node_crypto8 = require("crypto");
29279
+ var AcpDriver = class {
29280
+ constructor(deps) {
29281
+ this.deps = deps;
29282
+ }
29283
+ deps;
29284
+ kind = "mobile_acp";
29285
+ turnActive = false;
29286
+ waiters = [];
29287
+ /** Set once the adapter has handshaked (in {@link start}); the shared
29288
+ * conversation id (resumed id, or the freshly minted one). */
29289
+ acpSessionId = null;
29290
+ agentCaps;
29291
+ /** Lazily-built, memoised per {@link start} — cleared when the conversation
29292
+ * changes so history/models rebind to the current conversation. */
29293
+ session = null;
29294
+ budgetReachedPosted = false;
29295
+ async start(resumeId) {
29296
+ let started;
29297
+ try {
29298
+ started = await this.deps.client.start();
29299
+ if (resumeId !== void 0) {
29300
+ await this.deps.client.loadSession(resumeId);
29301
+ }
29302
+ } catch (err) {
29303
+ await this.deps.client.stop().catch(() => void 0);
29304
+ throw err;
29305
+ }
29306
+ const conversationId = resumeId !== void 0 ? resumeId : started.sessionId;
29307
+ this.acpSessionId = conversationId;
29308
+ this.agentCaps = started.initialize.agentCapabilities;
29309
+ this.session = null;
29310
+ this.budgetReachedPosted = false;
29311
+ return conversationId;
29312
+ }
29313
+ async stop() {
29314
+ await this.deps.client.stop();
29315
+ this.acpSessionId = null;
29316
+ this.session = null;
29317
+ }
29318
+ async dispatch(cmd) {
29319
+ const relay = this.deps.getRelay();
29320
+ const acpSessionId = this.acpSessionId;
29321
+ if (acpSessionId === null) {
29322
+ await relay.sendResult(cmd.id, "failed", { code: "BATON_MOBILE_NOT_STARTED" });
29323
+ return;
29324
+ }
29325
+ const session = await this.ensureSession(relay, acpSessionId, this.agentCaps);
29326
+ this.beginTurn();
29327
+ try {
29328
+ await dispatchAcpCommand(assembleAcpCommandContext(session, cmd));
29329
+ } finally {
29330
+ this.endTurn();
29331
+ }
29332
+ }
29333
+ /** Build (once per conversation) the session-scoped ACP context — the same
29334
+ * shape `runAcpSession` assembles. Memoised on {@link session}. */
29335
+ async ensureSession(relay, acpSessionId, agentCaps) {
29336
+ if (this.session) return this.session;
29337
+ const { opts, publisher, streaming, runtime, recentStderr } = this.deps;
29338
+ const models = await runtime.listModels();
29339
+ const jsonlHistory = new HistoryService(runtime, opts.pluginId, opts.cwd, {
29340
+ pluginAuthToken: opts.pluginAuthToken
29341
+ });
29342
+ const history = new AcpHistory(publisher, { agent: opts.agent, acpSessionId });
29343
+ const turnFiles = new TurnFileAggregator({
29344
+ workingDir: opts.cwd,
29345
+ sessionId: opts.sessionId,
29346
+ pluginId: opts.pluginId,
29347
+ pluginAuthToken: opts.pluginAuthToken,
29348
+ agentId: opts.agent
29349
+ });
29350
+ const budgetReachedFlag = {
29351
+ get: () => this.budgetReachedPosted,
29352
+ set: (v) => {
29353
+ this.budgetReachedPosted = v;
29354
+ }
29355
+ };
29356
+ const budgetRecovery = createBudgetRecovery({
29357
+ publishText: (text) => publisher.publishOutput({ type: "text", content: text, done: true }),
29358
+ publishSelectPrompt: (question, options) => publisher.publishOutput({
29359
+ type: "select_prompt",
29360
+ content: question,
29361
+ options,
29362
+ optionDescriptions: options.map(() => ""),
29363
+ currentIndex: 0,
29364
+ done: true
29365
+ }),
29366
+ publishAwaitingAnswer: (prompt, options) => publisher.publishAwaitingAnswer({ questionId: (0, import_node_crypto8.randomUUID)(), prompt, options }),
29367
+ publishRawChunk: (chunk) => publisher.publishOutput(chunk),
29368
+ sendResult: (commandId, status2, result) => relay.sendResult(commandId, status2, result),
29369
+ appendAgentReply: (text) => history.appendAgentReply(text),
29370
+ flushHistory: () => void history.flush(),
29371
+ relaunchProxyWithoutBudget,
29372
+ agentId: opts.agent
29373
+ });
29374
+ this.session = {
29375
+ client: this.deps.client,
29376
+ relay,
29377
+ acpSessionId,
29378
+ models,
29379
+ streaming,
29380
+ opts,
29381
+ history,
29382
+ jsonlHistory,
29383
+ agentCaps,
29384
+ turnFiles,
29385
+ getBeads: opts.getBeads ?? (() => null),
29386
+ publisher,
29387
+ recentStderr,
29388
+ budgetRecovery,
29389
+ budgetReachedFlag
29390
+ };
29391
+ return this.session;
29392
+ }
29393
+ beginTurn() {
29394
+ this.turnActive = true;
29395
+ }
29396
+ endTurn() {
29397
+ this.turnActive = false;
29398
+ const waiters = this.waiters;
29399
+ this.waiters = [];
29400
+ waiters.forEach((resolve7) => resolve7());
29401
+ }
29402
+ whenSafeToYield() {
29403
+ if (!this.turnActive) return Promise.resolve();
29404
+ return new Promise((resolve7) => this.waiters.push(resolve7));
29405
+ }
29406
+ };
29407
+
29408
+ // src/baton/transcript-mirror.ts
29021
29409
  var fs58 = __toESM(require("fs"));
29410
+ var TranscriptMirror = class {
29411
+ constructor(deps) {
29412
+ this.deps = deps;
29413
+ }
29414
+ deps;
29415
+ emitted = 0;
29416
+ unwatch = null;
29417
+ start() {
29418
+ const file = this.deps.runtime.resolveHistoryFile?.(this.deps.cwd, this.deps.conversationId);
29419
+ if (!file) return;
29420
+ this.emit(file);
29421
+ const watch2 = this.deps.watch ?? defaultWatch;
29422
+ this.unwatch = watch2(file, () => this.emit(file));
29423
+ }
29424
+ stop() {
29425
+ this.unwatch?.();
29426
+ this.unwatch = null;
29427
+ }
29428
+ emit(file) {
29429
+ let all;
29430
+ try {
29431
+ all = this.deps.runtime.parseHistoryFile(file);
29432
+ } catch {
29433
+ return;
29434
+ }
29435
+ if (all.length <= this.emitted) return;
29436
+ const delta = all.slice(this.emitted);
29437
+ this.emitted = all.length;
29438
+ this.deps.onNewMessages(delta);
29439
+ }
29440
+ };
29441
+ function defaultWatch(file, onChange) {
29442
+ const w3 = fs58.watch(file, { persistent: false }, () => onChange());
29443
+ return () => w3.close();
29444
+ }
29445
+
29446
+ // src/baton/wire-baton.ts
29447
+ function makeOnCommand(deps) {
29448
+ return async function onCommand(cmd) {
29449
+ if (cmd.type === "take_control") {
29450
+ try {
29451
+ await deps.controller.takeControl();
29452
+ } catch (err) {
29453
+ await deps.ack(cmd.id, "failed", {
29454
+ code: "BATON_SWITCH_FAILED",
29455
+ message: err instanceof Error ? err.message : String(err)
29456
+ });
29457
+ return;
29458
+ }
29459
+ await deps.ack(cmd.id, "completed", { state: deps.controller.state });
29460
+ return;
29461
+ }
29462
+ if (cmd.type === "handback") {
29463
+ try {
29464
+ await deps.controller.handback();
29465
+ } catch (err) {
29466
+ await deps.ack(cmd.id, "failed", {
29467
+ code: "BATON_SWITCH_FAILED",
29468
+ message: err instanceof Error ? err.message : String(err)
29469
+ });
29470
+ return;
29471
+ }
29472
+ await deps.ack(cmd.id, "completed", { state: deps.controller.state });
29473
+ return;
29474
+ }
29475
+ await deps.dispatchActive(cmd);
29476
+ };
29477
+ }
29478
+ async function publishMirroredTurnsLive(publisher, messages) {
29479
+ for (const m of messages) {
29480
+ if (m.role === "user") {
29481
+ await publisher.publishOutput({ type: "clear" });
29482
+ await publisher.publishOutput({ type: "user_message", content: m.text, done: true });
29483
+ await publisher.publishOutput({ type: "new_turn", done: false });
29484
+ } else {
29485
+ await publisher.publishOutput({ type: "text", content: m.text, done: true });
29486
+ }
29487
+ }
29488
+ }
29489
+ function makeMirrorOnNewMessages(deps) {
29490
+ let isFirstEmit = true;
29491
+ let publishChain = Promise.resolve();
29492
+ return (messages) => {
29493
+ const relevant = messages.filter((m) => m.role !== "system");
29494
+ if (relevant.length === 0) return;
29495
+ const mapped = relevant.map((m) => ({
29496
+ id: m.id,
29497
+ role: m.role === "user" ? "user" : "agent",
29498
+ text: m.text,
29499
+ timestamp: toEpochMs(m.timestamp)
29500
+ }));
29501
+ void deps.publisher.pushConversation({
29502
+ agentId: deps.agentId,
29503
+ sessionId: deps.conversationId,
29504
+ messages: mapped
29505
+ });
29506
+ if (!isFirstEmit) {
29507
+ publishChain = publishChain.then(() => publishMirroredTurnsLive(deps.publisher, relevant)).catch((err) => {
29508
+ log.warn(
29509
+ "wireBaton",
29510
+ `mirror live-publish failed: ${err instanceof Error ? err.message : String(err)}`
29511
+ );
29512
+ });
29513
+ }
29514
+ isFirstEmit = false;
29515
+ };
29516
+ }
29517
+ async function runBatonSession(opts) {
29518
+ const publisher = new AcpPublisher({
29519
+ sessionId: opts.sessionId,
29520
+ pluginId: opts.pluginId,
29521
+ pluginAuthToken: opts.pluginAuthToken,
29522
+ refreshAuthToken: () => fetchCurrentPluginAuthToken(opts.sessionId, opts.pluginId, opts.pollSecret)
29523
+ });
29524
+ const runtime = createRuntimeStrategy(opts.agent);
29525
+ const streaming = new StreamingState(publisher);
29526
+ const recentStderr = [];
29527
+ const client2 = new AcpClient({
29528
+ adapter: opts.adapter,
29529
+ cwd: opts.cwd,
29530
+ onSessionUpdate: (notification) => {
29531
+ for (const delta of mapSessionUpdate(notification)) streaming.append(delta);
29532
+ },
29533
+ onRequestPermission: async (request) => {
29534
+ const { event, optionIdByLabel } = mapPermissionRequest(request);
29535
+ await publisher.publishAwaitingAnswer(event);
29536
+ return streaming.registerPermission({
29537
+ questionId: event.questionId,
29538
+ labels: event.options ?? [],
29539
+ optionIdByLabel
29540
+ });
29541
+ },
29542
+ onStderr: (line) => {
29543
+ recentStderr.push(line);
29544
+ if (recentStderr.length > 40) recentStderr.shift();
29545
+ },
29546
+ onUnexpectedExit: (code, signal) => {
29547
+ log.warn("baton", `ACP adapter exited code=${code} signal=${signal}; flushing`);
29548
+ void streaming.closeAll().finally(() => process.exit(code ?? 1));
29549
+ }
29550
+ });
29551
+ let relay;
29552
+ const acpOpts = {
29553
+ agent: opts.agent,
29554
+ sessionId: opts.sessionId,
29555
+ pluginId: opts.pluginId,
29556
+ pluginAuthToken: opts.pluginAuthToken,
29557
+ adapter: opts.adapter,
29558
+ cwd: opts.cwd,
29559
+ getBeads: opts.getBeads,
29560
+ pollSecret: opts.pollSecret
29561
+ };
29562
+ const mobileDriver = new AcpDriver({
29563
+ client: client2,
29564
+ publisher,
29565
+ streaming,
29566
+ runtime,
29567
+ recentStderr,
29568
+ opts: acpOpts,
29569
+ getRelay: () => relay
29570
+ });
29571
+ let nativeDriver;
29572
+ const agent = new AgentService(runtime, {
29573
+ cwd: opts.cwd,
29574
+ onData(raw) {
29575
+ nativeDriver.handlePtyData(raw);
29576
+ },
29577
+ onExit(code) {
29578
+ teardown();
29579
+ process.exit(code);
29580
+ }
29581
+ });
29582
+ nativeDriver = new NativeTuiDriver({
29583
+ agent,
29584
+ runtime,
29585
+ opts: {
29586
+ sessionId: opts.sessionId,
29587
+ pluginId: opts.pluginId,
29588
+ agentId: opts.agent,
29589
+ pluginAuthToken: opts.pluginAuthToken,
29590
+ cwd: opts.cwd
29591
+ },
29592
+ getRelay: () => relay,
29593
+ getBeads: opts.getBeads ?? (() => null)
29594
+ });
29595
+ let mirror = null;
29596
+ const startMirror = (conversationId) => {
29597
+ mirror?.stop();
29598
+ mirror = new TranscriptMirror({
29599
+ runtime,
29600
+ cwd: opts.cwd,
29601
+ conversationId,
29602
+ onNewMessages: makeMirrorOnNewMessages({
29603
+ publisher,
29604
+ agentId: opts.agent,
29605
+ conversationId
29606
+ })
29607
+ });
29608
+ mirror.start();
29609
+ };
29610
+ const controller = new BatonController({
29611
+ local: nativeDriver,
29612
+ mobile: mobileDriver,
29613
+ publishState: (state, driver, conversationId) => {
29614
+ void postBatonEvent({
29615
+ sessionId: opts.sessionId,
29616
+ pluginId: opts.pluginId,
29617
+ pluginAuthToken: opts.pluginAuthToken,
29618
+ state,
29619
+ driver,
29620
+ conversationId
29621
+ });
29622
+ if (state === "LOCAL_DRIVE" && conversationId) startMirror(conversationId);
29623
+ else if (state !== "LOCAL_DRIVE") mirror?.stop();
29624
+ }
29625
+ });
29626
+ const dispatchActive = (cmd) => controller.activeSessionDriver.dispatch(cmd);
29627
+ relay = new CommandRelayService(
29628
+ opts.pluginId,
29629
+ makeOnCommand({
29630
+ controller,
29631
+ dispatchActive,
29632
+ ack: (id, status2, result) => relay.sendResult(id, status2, result)
29633
+ }),
29634
+ runtime.meta
29635
+ );
29636
+ let torn = false;
29637
+ function teardown() {
29638
+ if (torn) return;
29639
+ torn = true;
29640
+ process.removeListener("SIGINT", onSignal);
29641
+ process.removeListener("SIGTERM", onSignal);
29642
+ process.removeListener("SIGHUP", onSignal);
29643
+ mirror?.stop();
29644
+ relay.stop();
29645
+ void controller.shutdown();
29646
+ }
29647
+ const onSignal = () => {
29648
+ teardown();
29649
+ process.exit(0);
29650
+ };
29651
+ process.once("SIGINT", onSignal);
29652
+ process.once("SIGTERM", onSignal);
29653
+ process.once("SIGHUP", onSignal);
29654
+ showInfo(`Starting ${opts.agent} baton (local) \u2014 native TUI + mobile take-control\u2026`);
29655
+ await controller.begin();
29656
+ relay.start();
29657
+ showSuccess(`${opts.agent} baton online \u2014 you're driving locally; mobile can take control.`);
29658
+ showRelayNotice();
29659
+ await new Promise(() => {
29660
+ });
29661
+ }
29662
+ function toEpochMs(ts) {
29663
+ const asNum = Number(ts);
29664
+ if (Number.isFinite(asNum) && asNum > 0) return asNum;
29665
+ const parsed = Date.parse(ts);
29666
+ return Number.isFinite(parsed) ? parsed : Date.now();
29667
+ }
29668
+
29669
+ // src/agents/claude/onboarding.ts
29670
+ var fs59 = __toESM(require("fs"));
29022
29671
  var os45 = __toESM(require("os"));
29023
29672
  var path62 = __toESM(require("path"));
29024
29673
  function ensureClaudeOnboarded() {
@@ -29026,7 +29675,7 @@ function ensureClaudeOnboarded() {
29026
29675
  const file = path62.join(os45.homedir(), ".claude.json");
29027
29676
  let config = {};
29028
29677
  try {
29029
- config = JSON.parse(fs58.readFileSync(file, "utf8"));
29678
+ config = JSON.parse(fs59.readFileSync(file, "utf8"));
29030
29679
  } catch {
29031
29680
  }
29032
29681
  if (config.hasCompletedOnboarding === true && typeof config.theme === "string") {
@@ -29037,8 +29686,8 @@ function ensureClaudeOnboarded() {
29037
29686
  if (typeof config.lastOnboardingVersion !== "string") {
29038
29687
  config.lastOnboardingVersion = "2.1.177";
29039
29688
  }
29040
- fs58.mkdirSync(path62.dirname(file), { recursive: true });
29041
- fs58.writeFileSync(file, JSON.stringify(config, null, 2));
29689
+ fs59.mkdirSync(path62.dirname(file), { recursive: true });
29690
+ fs59.writeFileSync(file, JSON.stringify(config, null, 2));
29042
29691
  log.info("claude", "pre-completed Claude onboarding (skip first-run theme picker)");
29043
29692
  } catch (err) {
29044
29693
  log.warn("claude", `ensureClaudeOnboarded failed (non-fatal): ${err.message}`);
@@ -29162,6 +29811,22 @@ async function start(requestedAgent) {
29162
29811
  `agent-spawn gate released \u2014 beads ${beads ? "ready" : "pending"}; project deps provisioned`
29163
29812
  );
29164
29813
  }
29814
+ if (isLocalSession() && requiresAcp(session.agent)) {
29815
+ const adapter = getAcpAdapter(session.agent);
29816
+ if (adapter && session.pluginAuthToken) {
29817
+ await runBatonSession({
29818
+ agent: session.agent,
29819
+ sessionId: session.id,
29820
+ pluginId,
29821
+ pluginAuthToken: session.pluginAuthToken,
29822
+ pollSecret: session.pollSecret,
29823
+ cwd,
29824
+ adapter,
29825
+ getBeads
29826
+ });
29827
+ return;
29828
+ }
29829
+ }
29165
29830
  if (requiresAcp(session.agent)) {
29166
29831
  const adapter = getAcpAdapter(session.agent);
29167
29832
  if (!adapter || !session.pluginAuthToken) {
@@ -31739,8 +32404,8 @@ async function invite() {
31739
32404
  // src/commands/doctor.ts
31740
32405
  var import_node_dns = require("dns");
31741
32406
  var import_node_util5 = require("util");
31742
- var import_node_crypto8 = require("crypto");
31743
- var fs59 = __toESM(require("fs"));
32407
+ var import_node_crypto9 = require("crypto");
32408
+ var fs60 = __toESM(require("fs"));
31744
32409
  var path67 = __toESM(require("path"));
31745
32410
  var import_picocolors14 = __toESM(require("picocolors"));
31746
32411
  var dnsResolveP = (0, import_node_util5.promisify)(import_node_dns.resolve);
@@ -31799,11 +32464,11 @@ async function checkHealth(apiBase2) {
31799
32464
  function checkConfigDir() {
31800
32465
  const dir = path67.join(require("os").homedir(), ".codeam");
31801
32466
  try {
31802
- fs59.mkdirSync(dir, { recursive: true, mode: 448 });
32467
+ fs60.mkdirSync(dir, { recursive: true, mode: 448 });
31803
32468
  const probe = path67.join(dir, ".doctor-probe");
31804
- fs59.writeFileSync(probe, "ok", { mode: 384 });
31805
- const read2 = fs59.readFileSync(probe, "utf8");
31806
- fs59.unlinkSync(probe);
32469
+ fs60.writeFileSync(probe, "ok", { mode: 384 });
32470
+ const read2 = fs60.readFileSync(probe, "utf8");
32471
+ fs60.unlinkSync(probe);
31807
32472
  if (read2 !== "ok") throw new Error("write/read round-trip mismatch");
31808
32473
  return {
31809
32474
  id: "config-dir",
@@ -31909,9 +32574,9 @@ function checkChokidar() {
31909
32574
  }
31910
32575
  async function doctor(args2 = []) {
31911
32576
  const json = args2.includes("--json");
31912
- const cliVersion = true ? "2.59.0" : "0.0.0-dev";
32577
+ const cliVersion = true ? "2.60.1" : "0.0.0-dev";
31913
32578
  const apiBase2 = resolveApiBaseUrl();
31914
- const diagnosticId = (0, import_node_crypto8.randomUUID)();
32579
+ const diagnosticId = (0, import_node_crypto9.randomUUID)();
31915
32580
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
31916
32581
  const [dns, health] = await Promise.all([
31917
32582
  checkDns(apiBase2),
@@ -32108,7 +32773,7 @@ async function completion(args2) {
32108
32773
  // src/commands/version.ts
32109
32774
  var import_picocolors15 = __toESM(require("picocolors"));
32110
32775
  function version2() {
32111
- const v = true ? "2.59.0" : "unknown";
32776
+ const v = true ? "2.60.1" : "unknown";
32112
32777
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
32113
32778
  }
32114
32779
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.59.0",
3
+ "version": "2.60.1",
4
4
  "description": "Workflow-continuity bridge for AI coding agents. Wrap Claude Code or Codex in a PTY and supervise, approve, and redirect the session from any device — async. The terminal companion for CodeAgent Mobile.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",