codeam-cli 2.65.10 → 2.65.11

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 (2) hide show
  1. package/dist/index.js +190 -34
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8079,7 +8079,7 @@ function readAnonId() {
8079
8079
  }
8080
8080
  function superProperties() {
8081
8081
  return {
8082
- cliVersion: true ? "2.65.10" : "0.0.0-dev",
8082
+ cliVersion: true ? "2.65.11" : "0.0.0-dev",
8083
8083
  nodeVersion: process.version,
8084
8084
  platform: process.platform,
8085
8085
  arch: process.arch,
@@ -8321,7 +8321,7 @@ var http = __toESM(require("http"));
8321
8321
  // package.json
8322
8322
  var package_default = {
8323
8323
  name: "codeam-cli",
8324
- version: "2.65.10",
8324
+ version: "2.65.11",
8325
8325
  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.",
8326
8326
  type: "commonjs",
8327
8327
  main: "dist/index.js",
@@ -9542,12 +9542,30 @@ var CommandRelayService = class _CommandRelayService {
9542
9542
  this.connectSSE();
9543
9543
  }
9544
9544
  }
9545
+ /**
9546
+ * Stop the relay. Kept SYNCHRONOUS for the many callers that just tear down
9547
+ * — the goodbye heartbeat is fired but NOT awaited here.
9548
+ *
9549
+ * ⚠️ On a shutdown path that calls `process.exit()` right after, use
9550
+ * {@link stopAndFlush} (or {@link stopRelayWithGoodbye}) instead: the process
9551
+ * dies before the fire-and-forget POST hits the wire, so the backend never
9552
+ * learns the CLI went away and mobile keeps showing the session ONLINE until
9553
+ * the 30 s Redis heartbeat key expires — silently, since nothing publishes on
9554
+ * expiry (the 2026-08-18 "closing Claude Code leaves mobile online" report).
9555
+ */
9545
9556
  stop() {
9557
+ void this.stopAndFlush();
9558
+ }
9559
+ /**
9560
+ * Same teardown as {@link stop}, but resolves only once the `online:false`
9561
+ * heartbeat POST has settled — so a caller can `await` it before exiting.
9562
+ * Never rejects (`sendHeartbeat` swallows its own transport errors).
9563
+ */
9564
+ async stopAndFlush() {
9546
9565
  if (!this._running) return;
9547
9566
  this._running = false;
9548
9567
  this.cleanup();
9549
- this.sendHeartbeat(false).catch(() => {
9550
- });
9568
+ await this.sendHeartbeat(false);
9551
9569
  }
9552
9570
  async sendResult(commandId, status2, result) {
9553
9571
  if (this.pairingInvalid) return;
@@ -9824,7 +9842,7 @@ var CommandRelayService = class _CommandRelayService {
9824
9842
  // fresh + clear the "CLI update available" banner after a self-update
9825
9843
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
9826
9844
  // pair/reconnect). Older backends ignore the extra field.
9827
- ..."2.65.10" ? { ideVersion: "2.65.10" } : {}
9845
+ ..."2.65.11" ? { ideVersion: "2.65.11" } : {}
9828
9846
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
9829
9847
  }
9830
9848
  /**
@@ -9905,6 +9923,21 @@ var CommandRelayService = class _CommandRelayService {
9905
9923
  }
9906
9924
  }
9907
9925
  };
9926
+ var RELAY_GOODBYE_TIMEOUT_MS = 1500;
9927
+ async function stopRelayWithGoodbye(relay, timeoutMs = RELAY_GOODBYE_TIMEOUT_MS) {
9928
+ let timer;
9929
+ try {
9930
+ await Promise.race([
9931
+ relay.stopAndFlush(),
9932
+ new Promise((resolve10) => {
9933
+ timer = setTimeout(resolve10, timeoutMs);
9934
+ })
9935
+ ]);
9936
+ } catch {
9937
+ } finally {
9938
+ if (timer) clearTimeout(timer);
9939
+ }
9940
+ }
9908
9941
 
9909
9942
  // src/services/file-watcher.service.ts
9910
9943
  var import_child_process4 = require("child_process");
@@ -21753,7 +21786,7 @@ async function autoUpgradeBeforeCriticalCommand() {
21753
21786
  if (process.env.NODE_ENV === "test") return;
21754
21787
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
21755
21788
  if (process.env.CI) return;
21756
- const current2 = true ? "2.65.10" : null;
21789
+ const current2 = true ? "2.65.11" : null;
21757
21790
  if (!current2) return;
21758
21791
  const cache = readCache();
21759
21792
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -21770,7 +21803,7 @@ function checkForUpdates() {
21770
21803
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
21771
21804
  if (process.env.CI) return;
21772
21805
  if (!process.stdout.isTTY) return;
21773
- const current2 = true ? "2.65.10" : null;
21806
+ const current2 = true ? "2.65.11" : null;
21774
21807
  if (!current2) return;
21775
21808
  const cache = readCache();
21776
21809
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -21790,7 +21823,7 @@ var SELF_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
21790
21823
  var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
21791
21824
  var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
21792
21825
  function currentCliVersion() {
21793
- return true ? "2.65.10" : null;
21826
+ return true ? "2.65.11" : null;
21794
21827
  }
21795
21828
  function runCmd(cmd, args2, timeoutMs) {
21796
21829
  return new Promise((resolve10) => {
@@ -26585,7 +26618,7 @@ var sessionTerminated = async (ctx, cmd) => {
26585
26618
  } catch {
26586
26619
  }
26587
26620
  ctx.outputSvc.dispose();
26588
- ctx.relay.stop();
26621
+ await stopRelayWithGoodbye(ctx.relay);
26589
26622
  process.exit(0);
26590
26623
  };
26591
26624
  var shutdownSession = async (ctx, cmd) => {
@@ -26614,7 +26647,7 @@ var shutdownSession = async (ctx, cmd) => {
26614
26647
  } catch {
26615
26648
  }
26616
26649
  ctx.outputSvc.dispose();
26617
- ctx.relay.stop();
26650
+ await stopRelayWithGoodbye(ctx.relay);
26618
26651
  process.exit(0);
26619
26652
  };
26620
26653
  var showInstallCommand = async (ctx, cmd, parsed) => {
@@ -33869,6 +33902,7 @@ var _acpStartSeam = {
33869
33902
  var PROTOCOL_VERSION3 = 1;
33870
33903
  var FATAL_STARTUP_RE = /IneligibleTierError|UNSUPPORTED_CLIENT|no longer supported for Gemini Code Assist|not eligible for Gemini Code Assist|Error authenticating|ProjectIdRequiredError/i;
33871
33904
  var NEWSESSION_TIMEOUT_MS = 12e4;
33905
+ var LOADSESSION_TIMEOUT_MS = 6e4;
33872
33906
  var PROMPT_IDLE_TIMEOUT_MS = 9e4;
33873
33907
  var PROMPT_ACTIVE_IDLE_TIMEOUT_MS = 6e5;
33874
33908
  var CLIENT_CAPABILITIES = {
@@ -34383,13 +34417,30 @@ var AcpClient = class {
34383
34417
  log.info("acpClient", `loadSession \u2192 sessionId=${sessionId.slice(0, 8)}`);
34384
34418
  this.opts.beginLoadReplay?.();
34385
34419
  let loaded;
34420
+ let loadTimer;
34386
34421
  try {
34387
- loaded = await this.connection.loadSession({
34388
- sessionId,
34389
- cwd: this.opts.cwd,
34390
- mcpServers: this.opts.mcpServers ?? []
34391
- });
34422
+ loaded = await Promise.race([
34423
+ this.connection.loadSession({
34424
+ sessionId,
34425
+ cwd: this.opts.cwd,
34426
+ mcpServers: this.opts.mcpServers ?? []
34427
+ }),
34428
+ new Promise((_2, reject) => {
34429
+ loadTimer = setTimeout(() => {
34430
+ const tail = this.recentStderr.slice(-4).join(" | ");
34431
+ reject(
34432
+ new Error(
34433
+ `ACP_LOAD_SESSION_TIMEOUT: ${this.opts.adapter.requiresAgentBinary} did not answer session/load for ${sessionId.slice(
34434
+ 0,
34435
+ 8
34436
+ )} within ${Math.round(LOADSESSION_TIMEOUT_MS / 1e3)}s${tail ? ` \u2014 last output: ${tail}` : ""}`
34437
+ )
34438
+ );
34439
+ }, LOADSESSION_TIMEOUT_MS);
34440
+ })
34441
+ ]);
34392
34442
  } finally {
34443
+ if (loadTimer) clearTimeout(loadTimer);
34393
34444
  this.opts.endLoadReplay?.();
34394
34445
  }
34395
34446
  this.sessionId = sessionId;
@@ -38676,7 +38727,7 @@ async function sessionShutdownH(ctx) {
38676
38727
  } catch {
38677
38728
  }
38678
38729
  }
38679
- relay.stop();
38730
+ await stopRelayWithGoodbye(relay);
38680
38731
  closeAllTerminals();
38681
38732
  await client3.stop();
38682
38733
  process.exit(0);
@@ -39975,7 +40026,7 @@ async function runAcpSession(opts) {
39975
40026
  const shutdown = async (signal) => {
39976
40027
  showInfo(`Shutting down ACP session (${signal})\u2026`);
39977
40028
  clearTimeout(prewarmTimer);
39978
- relay.stop();
40029
+ await stopRelayWithGoodbye(relay);
39979
40030
  void fileWatcher.stop();
39980
40031
  turnFiles.stop();
39981
40032
  closeAllTerminals();
@@ -40444,7 +40495,27 @@ var OutputService = class _OutputService {
40444
40495
  ...lines.slice(banner.endIdx + 1)
40445
40496
  ];
40446
40497
  }
40498
+ /**
40499
+ * Mute/unmute the CHAT-output publish path. Every frame this service would
40500
+ * emit (clear / user_message / new_turn / text / chrome_steps / selectors /
40501
+ * banner / input_suggestion) is dropped while muted; `push()` and the tick
40502
+ * still run, so the detection side-effects (session id, rate limit,
40503
+ * terminal-turn gate) are untouched.
40504
+ *
40505
+ * ⚠️ The one caller is the baton's {@link NativeTuiDriver}: while LOCAL_DRIVE
40506
+ * holds the baton, the mobile view is the read-only TRANSCRIPT mirror, never
40507
+ * a screen-scrape — piping the native TUI's PTY bytes through here published
40508
+ * raw Claude Code chrome (box-drawing rules, `❯`, "auto mode on (shift+tab to
40509
+ * cycle) · esc to interrupt") into the chat as if it were agent output.
40510
+ * Terminal-panel frames (`sendTerminalChunk`/`sendTerminalExit`) go straight
40511
+ * to the emitter and are deliberately NOT affected.
40512
+ */
40513
+ setPublishSuppressed(suppressed) {
40514
+ this.publishSuppressed = suppressed;
40515
+ }
40516
+ publishSuppressed = false;
40447
40517
  async send(body, opts = {}) {
40518
+ if (this.publishSuppressed) return;
40448
40519
  const outcome = await this.emitter.send(body, opts);
40449
40520
  if (outcome.dead && this.pty.isActive) {
40450
40521
  this.dispose();
@@ -41135,11 +41206,18 @@ function provisionSkillsForStart(home = import_node_os13.default.homedir()) {
41135
41206
  }
41136
41207
 
41137
41208
  // src/baton/baton-controller.ts
41138
- var BatonController = class {
41209
+ var BatonController = class _BatonController {
41139
41210
  constructor(deps) {
41140
41211
  this.deps = deps;
41141
41212
  }
41142
41213
  deps;
41214
+ /**
41215
+ * Default upper bound on a whole hand-off. A cold `newSession` on claude is
41216
+ * ~20 s, so 45 s leaves generous headroom for a healthy switch while still
41217
+ * failing FAST enough that the user gets an honest error instead of a
41218
+ * permanent "Switching…".
41219
+ */
41220
+ static DEFAULT_SWITCH_TIMEOUT_MS = 45e3;
41143
41221
  _state = "LOCAL_DRIVE";
41144
41222
  _active = "local_tui";
41145
41223
  _conversationId = null;
@@ -41216,24 +41294,79 @@ var BatonController = class {
41216
41294
  this.setState("SWITCHING");
41217
41295
  const priorActive = this._active;
41218
41296
  const priorConversationId = this._conversationId;
41219
- try {
41297
+ const timeoutMs = this.deps.switchTimeoutMs ?? _BatonController.DEFAULT_SWITCH_TIMEOUT_MS;
41298
+ let stoppedCurrent = false;
41299
+ const handoff = (async () => {
41220
41300
  await current2.whenSafeToYield();
41221
41301
  await current2.stop();
41222
- this._conversationId = await next.start(this._conversationId ?? void 0);
41302
+ stoppedCurrent = true;
41303
+ return next.start(priorConversationId ?? void 0);
41304
+ })();
41305
+ handoff.catch(() => void 0);
41306
+ try {
41307
+ const conversationId = await withDeadline(
41308
+ handoff,
41309
+ timeoutMs,
41310
+ `BATON_SWITCH_TIMEOUT: ${from} \u2192 ${to} hand-off did not complete within ${Math.round(
41311
+ timeoutMs / 1e3
41312
+ )}s`
41313
+ );
41314
+ this._conversationId = conversationId;
41223
41315
  this._active = nextKind;
41224
41316
  this.setState(to);
41225
41317
  } catch (err) {
41318
+ await this.recoverFromFailedSwitch(
41319
+ current2,
41320
+ next,
41321
+ stoppedCurrent,
41322
+ priorConversationId,
41323
+ timeoutMs
41324
+ );
41226
41325
  this._active = priorActive;
41227
- this._conversationId = priorConversationId;
41228
41326
  this.setState(from);
41229
41327
  throw err;
41230
41328
  }
41231
41329
  }
41330
+ /**
41331
+ * Undo a half-done hand-off. The `next` driver may still be starting behind
41332
+ * the deadline — stop it so a second adapter/PTY can't race the revived one.
41333
+ * And when `current` was ALREADY stopped before the failure, reverting only
41334
+ * the STATE would leave the user staring at a dead terminal (or mobile at a
41335
+ * dead adapter), so bring it back on the same conversation. Both best-effort
41336
+ * and bounded: recovery must never itself hang the controller.
41337
+ */
41338
+ async recoverFromFailedSwitch(current2, next, stoppedCurrent, priorConversationId, timeoutMs) {
41339
+ this._conversationId = priorConversationId;
41340
+ await withDeadline(next.stop(), timeoutMs, "BATON_RECOVERY_STOP_TIMEOUT").catch(
41341
+ () => void 0
41342
+ );
41343
+ if (!stoppedCurrent) return;
41344
+ try {
41345
+ const revived = await withDeadline(
41346
+ current2.start(priorConversationId ?? void 0),
41347
+ timeoutMs,
41348
+ "BATON_RECOVERY_START_TIMEOUT"
41349
+ );
41350
+ if (revived) this._conversationId = revived;
41351
+ } catch {
41352
+ }
41353
+ }
41232
41354
  setState(state) {
41233
41355
  this._state = state;
41234
41356
  this.deps.publishState(state, this._active, this._conversationId);
41235
41357
  }
41236
41358
  };
41359
+ function withDeadline(promise, ms, message) {
41360
+ let timer;
41361
+ return Promise.race([
41362
+ promise,
41363
+ new Promise((_2, reject) => {
41364
+ timer = setTimeout(() => reject(new Error(message)), ms);
41365
+ })
41366
+ ]).finally(() => {
41367
+ if (timer) clearTimeout(timer);
41368
+ });
41369
+ }
41237
41370
 
41238
41371
  // src/baton/terminal.ts
41239
41372
  var TUI_MODE_RESET = "\x1B[?1004l\x1B[?2004l\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?25h";
@@ -41271,6 +41404,7 @@ var NativeTuiDriver = class _NativeTuiDriver {
41271
41404
  deps.opts.pluginAuthToken,
41272
41405
  deps.runtime
41273
41406
  );
41407
+ this.outputSvc.setPublishSuppressed(true);
41274
41408
  this.keepAliveCtx = { inCodespace: false, codespaceName: void 0 };
41275
41409
  this.setKeepAlive = buildKeepAlive(this.keepAliveCtx).apply;
41276
41410
  }
@@ -41375,10 +41509,17 @@ var AcpDriver = class {
41375
41509
  session = null;
41376
41510
  budgetReachedPosted = false;
41377
41511
  async start(resumeId) {
41512
+ const resumable = resumeId !== void 0 && this.hasTranscript(resumeId);
41513
+ if (resumeId !== void 0 && !resumable) {
41514
+ log.info(
41515
+ "batonAcp",
41516
+ `resume id ${resumeId.slice(0, 8)} has no transcript on disk (zero turns yet) \u2014 starting a fresh ACP session instead of session/load`
41517
+ );
41518
+ }
41378
41519
  let started;
41379
41520
  try {
41380
41521
  started = await this.deps.client.start();
41381
- if (resumeId !== void 0) {
41522
+ if (resumable && resumeId !== void 0) {
41382
41523
  await this.deps.runtime.syncTranscriptForAcpResume?.(this.deps.opts.cwd, resumeId);
41383
41524
  this.deps.streaming.beginLoadReplay();
41384
41525
  try {
@@ -41391,13 +41532,30 @@ var AcpDriver = class {
41391
41532
  await this.deps.client.stop().catch(() => void 0);
41392
41533
  throw err;
41393
41534
  }
41394
- const conversationId = resumeId !== void 0 ? resumeId : started.sessionId;
41535
+ const conversationId = resumable && resumeId !== void 0 ? resumeId : started.sessionId;
41395
41536
  this.acpSessionId = conversationId;
41396
41537
  this.agentCaps = started.initialize.agentCapabilities;
41397
41538
  this.session = null;
41398
41539
  this.budgetReachedPosted = false;
41399
41540
  return conversationId;
41400
41541
  }
41542
+ /**
41543
+ * Does the agent's own on-disk transcript for `conversationId` exist yet?
41544
+ * `resolveHistoryFile` returns null when the file isn't there (every baton
41545
+ * runtime `existsSync`-checks), which is exactly "the native TUI has run zero
41546
+ * turns on this id" — the only case where `session/load` has nothing to load.
41547
+ * A runtime without the hook can't be a baton runtime (see `gate.ts`), but if
41548
+ * one ever is, assume resumable so behaviour is unchanged.
41549
+ */
41550
+ hasTranscript(conversationId) {
41551
+ const resolve10 = this.deps.runtime.resolveHistoryFile;
41552
+ if (typeof resolve10 !== "function") return true;
41553
+ try {
41554
+ return resolve10.call(this.deps.runtime, this.deps.opts.cwd, conversationId) !== null;
41555
+ } catch {
41556
+ return true;
41557
+ }
41558
+ }
41401
41559
  async stop() {
41402
41560
  await this.deps.client.stop();
41403
41561
  this.acpSessionId = null;
@@ -41732,8 +41890,7 @@ async function runBatonSession(opts) {
41732
41890
  nativeDriver.handlePtyData(raw);
41733
41891
  },
41734
41892
  onExit(code) {
41735
- teardown();
41736
- process.exit(code);
41893
+ void teardown().finally(() => process.exit(code));
41737
41894
  }
41738
41895
  });
41739
41896
  nativeDriver = new NativeTuiDriver({
@@ -41813,19 +41970,18 @@ async function runBatonSession(opts) {
41813
41970
  publish: publishBatonState
41814
41971
  })
41815
41972
  );
41816
- function teardown() {
41973
+ async function teardown() {
41817
41974
  if (torn) return;
41818
41975
  torn = true;
41819
41976
  process.removeListener("SIGINT", onSignal);
41820
41977
  process.removeListener("SIGTERM", onSignal);
41821
41978
  process.removeListener("SIGHUP", onSignal);
41822
41979
  mirror?.stop();
41823
- relay.stop();
41824
41980
  void controller.shutdown();
41981
+ await stopRelayWithGoodbye(relay);
41825
41982
  }
41826
41983
  const onSignal = () => {
41827
- teardown();
41828
- process.exit(0);
41984
+ void teardown().finally(() => process.exit(0));
41829
41985
  };
41830
41986
  process.once("SIGINT", onSignal);
41831
41987
  process.once("SIGTERM", onSignal);
@@ -42212,12 +42368,12 @@ async function start(requestedAgent, presetSession) {
42212
42368
  outputSvc.push(raw);
42213
42369
  streamingEmitter?.push(raw);
42214
42370
  },
42215
- onExit(code) {
42371
+ async onExit(code) {
42216
42372
  process.removeListener("SIGINT", sigintHandler);
42217
42373
  process.removeListener("SIGTERM", sigintHandler);
42218
42374
  process.removeListener("SIGHUP", sigintHandler);
42219
42375
  outputSvc.dispose();
42220
- relay.stop();
42376
+ await stopRelayWithGoodbye(relay);
42221
42377
  void fileWatcher?.stop();
42222
42378
  turnFiles?.stop();
42223
42379
  void beads?.watcher.stop();
@@ -42272,7 +42428,7 @@ async function start(requestedAgent, presetSession) {
42272
42428
  shuttingDown = true;
42273
42429
  agent.kill();
42274
42430
  outputSvc.dispose();
42275
- relay.stop();
42431
+ await stopRelayWithGoodbye(relay);
42276
42432
  void fileWatcher?.stop();
42277
42433
  void beads?.watcher.stop();
42278
42434
  void streamingEmitter?.stop();
@@ -44868,7 +45024,7 @@ function checkChokidar() {
44868
45024
  }
44869
45025
  async function doctor(args2 = []) {
44870
45026
  const json = args2.includes("--json");
44871
- const cliVersion = true ? "2.65.10" : "0.0.0-dev";
45027
+ const cliVersion = true ? "2.65.11" : "0.0.0-dev";
44872
45028
  const apiBase2 = resolveApiBaseUrl();
44873
45029
  const diagnosticId = (0, import_node_crypto13.randomUUID)();
44874
45030
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -45259,7 +45415,7 @@ async function mcpRun(args2) {
45259
45415
  // src/commands/version.ts
45260
45416
  var import_picocolors15 = __toESM(require("picocolors"));
45261
45417
  function version2() {
45262
- const v = true ? "2.65.10" : "unknown";
45418
+ const v = true ? "2.65.11" : "unknown";
45263
45419
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
45264
45420
  }
45265
45421
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.65.10",
3
+ "version": "2.65.11",
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",