codeam-cli 2.76.33 → 2.76.35

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 +14 -0
  2. package/dist/index.js +193 -33
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ 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.76.34] — 2026-10-21
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Collect old versioned installs on every self-update tick (#983)
12
+ - **cli:** Migrate the beads schema on start and pin bd to 1.3.1 in CLI and Box (#984)
13
+
14
+ ## [2.76.33] — 2026-10-14
15
+
16
+ ### Added
17
+
18
+ - **box:** Bake PHP CLI + Composer into the Box image
19
+ - **cli:** Composer pre-flight + PHP guidance for in-app Preview
20
+
7
21
  ## [2.76.32] — 2026-10-14
8
22
 
9
23
  ### Fixed
package/dist/index.js CHANGED
@@ -10636,7 +10636,7 @@ function readAnonId() {
10636
10636
  }
10637
10637
  function superProperties() {
10638
10638
  return {
10639
- cliVersion: true ? "2.76.33" : "0.0.0-dev",
10639
+ cliVersion: true ? "2.76.35" : "0.0.0-dev",
10640
10640
  nodeVersion: process.version,
10641
10641
  platform: process.platform,
10642
10642
  arch: process.arch,
@@ -10885,7 +10885,7 @@ var http = __toESM(require("http"));
10885
10885
  // package.json
10886
10886
  var package_default = {
10887
10887
  name: "codeam-cli",
10888
- version: "2.76.33",
10888
+ version: "2.76.35",
10889
10889
  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.",
10890
10890
  type: "commonjs",
10891
10891
  main: "dist/index.js",
@@ -10973,7 +10973,7 @@ var package_default = {
10973
10973
  zod: "^4.3.6"
10974
10974
  },
10975
10975
  optionalDependencies: {
10976
- "@beads/bd": "1.0.5"
10976
+ "@beads/bd": "1.3.1"
10977
10977
  },
10978
10978
  devDependencies: {
10979
10979
  "@codeam/shared": "*",
@@ -12261,7 +12261,7 @@ var CommandRelayService = class _CommandRelayService {
12261
12261
  // fresh + clear the "CLI update available" banner after a self-update
12262
12262
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
12263
12263
  // pair/reconnect). Older backends ignore the extra field.
12264
- ..."2.76.33" ? { ideVersion: "2.76.33" } : {}
12264
+ ..."2.76.35" ? { ideVersion: "2.76.35" } : {}
12265
12265
  };
12266
12266
  try {
12267
12267
  const res = await _postJson(`${API_BASE2}/api/plugin/heartbeat`, body, this.pollSecretHeader());
@@ -26033,6 +26033,18 @@ var ONE_M_CREDITS_MESSAGE = "\u{1F504} **Reconnect your Claude subscription to c
26033
26033
  var TURN_FAILURE_MESSAGE = "\u26A0\uFE0F **The agent hit an error and couldn\u2019t finish this turn.** Please send your message again.";
26034
26034
  var IDLE_TURN_FAILURE_MESSAGE = "\u26A0\uFE0F **The agent stopped responding, so this turn was ended.** Your message wasn\u2019t answered \u2014 send it again.";
26035
26035
  var IDLE_TURN_RE = /ACP prompt idle/;
26036
+ var AGENT_FREE_LIMIT_RE = /Free usage limit reached/i;
26037
+ function looksLikeAgentFreeLimit(text) {
26038
+ return AGENT_FREE_LIMIT_RE.test(text);
26039
+ }
26040
+ var AGENT_FREE_LIMIT_MESSAGE = [
26041
+ "\u26A0\uFE0F **OpenCode\u2019s free models have hit their usage limit, so this message wasn\u2019t answered.**",
26042
+ "",
26043
+ "Messages will keep failing until the limit resets. To keep going now:",
26044
+ "\u2022 add your own provider key for opencode in **Profile \u203A Agents** (OpenRouter, Anthropic, OpenAI, \u2026),",
26045
+ "\u2022 switch this session to another agent, or",
26046
+ "\u2022 subscribe to OpenCode Go: [opencode.ai/go](https://opencode.ai/go)."
26047
+ ].join("\n");
26036
26048
  function emptyReplyMessage(agent) {
26037
26049
  const label = isKnownAgentId(agent) ? AGENT_REGISTRY[agent].displayName : agent;
26038
26050
  const binary = isKnownAgentId(agent) ? AGENT_REGISTRY[agent].binaryName : agent;
@@ -26096,6 +26108,9 @@ function failureBubble(opts) {
26096
26108
  return houseAgentLimitMessage(`${opts.detail}
26097
26109
  ${opts.recentStderr}`);
26098
26110
  }
26111
+ if (looksLikeAgentFreeLimit(opts.detail) || looksLikeAgentFreeLimit(opts.recentStderr)) {
26112
+ return AGENT_FREE_LIMIT_MESSAGE;
26113
+ }
26099
26114
  if (looksLikeByoProviderBilling(opts.detail, env) || looksLikeByoProviderBilling(opts.recentStderr, env)) {
26100
26115
  return byoProviderBillingMessage(byoProviderName({ env, agent: opts.agent }));
26101
26116
  }
@@ -27509,6 +27524,43 @@ var import_child_process21 = require("child_process");
27509
27524
  var fs59 = __toESM(require("fs"));
27510
27525
  var os40 = __toESM(require("os"));
27511
27526
  var path67 = __toESM(require("path"));
27527
+
27528
+ // src/beads/schema-migrate.ts
27529
+ var SCHEMA_SKEW_RE = /Error 1146|table not found|refusing to auto-apply \d+ pending schema migration/i;
27530
+ function isSchemaSkew(text) {
27531
+ return SCHEMA_SKEW_RE.test(text);
27532
+ }
27533
+ var APPLIED_RE = /applying (\d+) pending schema migration/i;
27534
+ var REMOTE_GATE_RE = /remote-backed database/i;
27535
+ async function migrateSchema(bd) {
27536
+ let forced = false;
27537
+ let res = await bd.run(["migrate", "schema"]);
27538
+ if (res.code !== 0 && REMOTE_GATE_RE.test(res.stderr + res.stdout)) {
27539
+ log.info("beads", "schema migrate refused for a remote-backed DB \u2014 migrating with --force");
27540
+ forced = true;
27541
+ res = await bd.run(["migrate", "schema", "--force"]);
27542
+ }
27543
+ const text = `${res.stderr}
27544
+ ${res.stdout}`;
27545
+ if (res.code !== 0) {
27546
+ const detail = (res.stderr || res.stdout).trim().slice(0, 200);
27547
+ log.warn("beads", `bd migrate schema failed (code=${res.code}): ${detail}`);
27548
+ return { status: "failed", detail };
27549
+ }
27550
+ const applied = APPLIED_RE.exec(text);
27551
+ if (applied) {
27552
+ const count = Number(applied[1]);
27553
+ log.info(
27554
+ "beads",
27555
+ `bd migrate schema applied ${count} migration(s)${forced ? " (--force)" : ""}`
27556
+ );
27557
+ return { status: "applied", count, forced };
27558
+ }
27559
+ log.trace("beads", "bd migrate schema: already current");
27560
+ return { status: "current" };
27561
+ }
27562
+
27563
+ // src/beads/bd-adapter.ts
27512
27564
  var BD_PACKAGE = "@beads/bd";
27513
27565
  function resolveBundledBdBinary() {
27514
27566
  return _resolveSeam.resolveBundled();
@@ -27586,6 +27638,8 @@ var BdAdapter = class {
27586
27638
  }
27587
27639
  opts;
27588
27640
  resolved;
27641
+ /** Set when a bd call failed on schema skew; read + cleared by `takeSchemaSkew`. */
27642
+ schemaSkewSeen = false;
27589
27643
  /**
27590
27644
  * Resolve the bd binary lazily, caching the result. Order:
27591
27645
  * bundled @beads/bd → PATH. Returns null when neither is available — the
@@ -27641,8 +27695,20 @@ var BdAdapter = class {
27641
27695
  await _adapterSeam.sleep(750 * attempt);
27642
27696
  res = await _spawnSeam.run(binary, args2, { cwd, env });
27643
27697
  }
27698
+ if (res.code !== 0 && isSchemaSkew(res.stderr + res.stdout)) this.schemaSkewSeen = true;
27644
27699
  return res;
27645
27700
  }
27701
+ /**
27702
+ * Whether a bd call since the last check failed because the project DB is
27703
+ * behind this bd's schema (see `schema-migrate.ts`). `listIssues` /
27704
+ * `statusSummary` swallow failures into []/null, so this is how the watcher
27705
+ * learns the cause. Clears the flag.
27706
+ */
27707
+ takeSchemaSkew() {
27708
+ const seen2 = this.schemaSkewSeen;
27709
+ this.schemaSkewSeen = false;
27710
+ return seen2;
27711
+ }
27646
27712
  /**
27647
27713
  * `bd ready --json` → typed issue array. `bd list --json` shares the shape,
27648
27714
  * so `listIssues` reuses the same parser. Bad JSON / non-zero exit → [].
@@ -28208,7 +28274,8 @@ async function provisionBeads(opts = {}) {
28208
28274
  prefix: null,
28209
28275
  initialized: false,
28210
28276
  exportEnabled: false,
28211
- agentsWired: []
28277
+ agentsWired: [],
28278
+ schemaMigrate: null
28212
28279
  };
28213
28280
  if (!bd.isAvailable()) {
28214
28281
  log.info("beads", "bd binary missing \u2014 running OS installer fallback");
@@ -28290,6 +28357,7 @@ async function provisionBeads(opts = {}) {
28290
28357
  log.warn("beads", "shared dolt sql-server not up \u2014 beads disabled this run");
28291
28358
  return result;
28292
28359
  }
28360
+ result.schemaMigrate = await migrateSchemaSafely(bd);
28293
28361
  try {
28294
28362
  if (!await projectDbReachable(bd)) {
28295
28363
  log.info(
@@ -28305,6 +28373,8 @@ async function provisionBeads(opts = {}) {
28305
28373
  "beads",
28306
28374
  `bd bootstrap failed (code=${boot.code}): ${boot.stderr.slice(0, 200)} \u2014 prefix DB may be absent`
28307
28375
  );
28376
+ } else {
28377
+ result.schemaMigrate = await migrateSchemaSafely(bd);
28308
28378
  }
28309
28379
  }
28310
28380
  if (!await projectDbReachable(bd)) {
@@ -28356,10 +28426,25 @@ async function provisionBeads(opts = {}) {
28356
28426
  result.agentsWired = await setupAgents(bd, opts.agents ?? []);
28357
28427
  log.info(
28358
28428
  "beads",
28359
- `provision done dolt=${result.doltAvailable} server=${result.serverUp} prefix=${result.prefix} initialized=${result.initialized} export=${result.exportEnabled} agentsWired=[${result.agentsWired.join(",")}]`
28429
+ `provision done dolt=${result.doltAvailable} server=${result.serverUp} prefix=${result.prefix} initialized=${result.initialized} schema=${describeSchemaMigrate(result.schemaMigrate)} export=${result.exportEnabled} agentsWired=[${result.agentsWired.join(",")}]`
28360
28430
  );
28361
28431
  return result;
28362
28432
  }
28433
+ async function migrateSchemaSafely(bd) {
28434
+ try {
28435
+ return await migrateSchema(bd);
28436
+ } catch (err) {
28437
+ log.warn("beads", "bd migrate schema threw (non-fatal)", err);
28438
+ return { status: "failed", detail: err.message };
28439
+ }
28440
+ }
28441
+ function describeSchemaMigrate(outcome) {
28442
+ if (!outcome) return "skipped";
28443
+ if (outcome.status === "applied") {
28444
+ return `applied:${outcome.count}${outcome.forced ? ":forced" : ""}`;
28445
+ }
28446
+ return outcome.status;
28447
+ }
28363
28448
  async function projectDbReachable(bd) {
28364
28449
  const ping = await bd.run(["ping"]);
28365
28450
  return ping.code === 0;
@@ -28455,6 +28540,8 @@ var BeadsWatcher = class {
28455
28540
  debounceTimer = null;
28456
28541
  stopped = false;
28457
28542
  lastPushedHash = null;
28543
+ /** One schema-heal attempt per watcher — a failing migrate must not loop. */
28544
+ schemaHealTried = false;
28458
28545
  bd;
28459
28546
  feedPath;
28460
28547
  apiBase;
@@ -28523,10 +28610,15 @@ var BeadsWatcher = class {
28523
28610
  async syncNow() {
28524
28611
  if (this.stopped) return;
28525
28612
  const { projectKey, projectLabel } = deriveProjectIdentity(this.opts.cwd);
28526
- const [issues, summary] = await Promise.all([
28527
- this.bd.listIssues(projectKey),
28528
- this.bd.statusSummary()
28529
- ]);
28613
+ let [issues, summary] = await this.readSnapshot(projectKey);
28614
+ if (!this.schemaHealTried && this.bd.takeSchemaSkew()) {
28615
+ this.schemaHealTried = true;
28616
+ log.info("beads", "snapshot hit schema skew \u2014 running bd migrate schema once");
28617
+ const outcome = await migrateSchema(this.bd);
28618
+ if (outcome.status !== "failed") {
28619
+ [issues, summary] = await this.readSnapshot(projectKey);
28620
+ }
28621
+ }
28530
28622
  const payload = {
28531
28623
  sessionId: this.opts.sessionId,
28532
28624
  pluginId: this.opts.pluginId,
@@ -28565,6 +28657,9 @@ var BeadsWatcher = class {
28565
28657
  log.warn("beads", "ingest POST error", err);
28566
28658
  }
28567
28659
  }
28660
+ readSnapshot(projectKey) {
28661
+ return Promise.all([this.bd.listIssues(projectKey), this.bd.statusSummary()]);
28662
+ }
28568
28663
  headers() {
28569
28664
  return {
28570
28665
  "Content-Type": "application/json",
@@ -31765,6 +31860,7 @@ function isLocalSession(env = process.env) {
31765
31860
 
31766
31861
  // src/agents/acp/adapters.ts
31767
31862
  var require_ = require;
31863
+ var OPENCODE_FATAL_TURN_STDERR_RE = /message="stream error".*error\.error="[^"]*(Free usage limit reached|insufficient[_ ]quota|invalid[_ ]api[_ ]key|incorrect api key|unauthorized)/i;
31768
31864
  function claudeBinaryWaiter(opts = {}) {
31769
31865
  return waitForClaudeNativeBinary(opts).then((p2) => p2 !== null);
31770
31866
  }
@@ -31924,8 +32020,12 @@ var REGISTRY = {
31924
32020
  // the ACP client spawns the adapter with `env: { ...process.env, ...extraEnv }`.
31925
32021
  opencode: () => ({
31926
32022
  command: "opencode",
31927
- args: ["acp"],
32023
+ // `--print-logs --log-level ERROR`: opencode writes its provider errors only
32024
+ // to its own log file, so without these flags they never reach the adapter
32025
+ // stderr we read (see OPENCODE_FATAL_TURN_STDERR_RE).
32026
+ args: ["acp", "--print-logs", "--log-level", "ERROR"],
31928
32027
  requiresAgentBinary: "opencode",
32028
+ fatalTurnStderr: OPENCODE_FATAL_TURN_STDERR_RE,
31929
32029
  waitForBinary: async (o) => {
31930
32030
  if (isLocalSession()) await ensureOpencodeInstalled();
31931
32031
  augmentOpencodePath();
@@ -37052,6 +37152,9 @@ var _acpStartSeam = {
37052
37152
  var PROTOCOL_VERSION3 = 1;
37053
37153
  var FATAL_STARTUP_RE = /IneligibleTierError|UNSUPPORTED_CLIENT|no longer supported for Gemini Code Assist|not eligible for Gemini Code Assist|Error authenticating|ProjectIdRequiredError/i;
37054
37154
  var NEWSESSION_TIMEOUT_MS = 12e4;
37155
+ function agentErrorOf(line) {
37156
+ return /error\.error="([^"]+)"/.exec(line)?.[1] ?? line;
37157
+ }
37055
37158
  var MCP_STARTUP_TIMEOUT_MS = NEWSESSION_TIMEOUT_MS - 3e4;
37056
37159
  var AGENT_ENV_WITHHELD = ["PREVIEW_TUNNEL_TOKEN", "PREVIEW_TUNNEL_HOSTNAME"];
37057
37160
  var LOADSESSION_TIMEOUT_MS = 6e4;
@@ -37157,6 +37260,12 @@ var AcpClient = class {
37157
37260
  * engages; with it, the crash rejects immediately → retry respawns once the
37158
37261
  * install settles. Broader than {@link startupFailureReject} (newSession-only). */
37159
37262
  startAbortReject = null;
37263
+ /** The adapter's {@link AdapterSpec.fatalTurnStderr}, set at spawn. */
37264
+ fatalTurnStderr = null;
37265
+ /** Set while a `session/prompt` is in flight; the stderr watcher calls it on
37266
+ * a {@link fatalTurnStderr} line to end the turn now (opencode keeps a turn
37267
+ * open forever after a terminal provider error — see the adapter spec). */
37268
+ turnFailureReject = null;
37160
37269
  /**
37161
37270
  * Spawn the adapter + handshake, retrying a TRANSIENT in-adapter spawn race.
37162
37271
  *
@@ -37245,6 +37354,7 @@ var AcpClient = class {
37245
37354
  "acpClient",
37246
37355
  `provider routing ${formatProviderRouting(describeProviderRouting(childEnv, { cwd }), adapter.command)}`
37247
37356
  );
37357
+ this.fatalTurnStderr = adapter.fatalTurnStderr ?? null;
37248
37358
  const child = (0, import_node_child_process28.spawn)(adapter.command, adapter.args, {
37249
37359
  cwd,
37250
37360
  env: childEnv,
@@ -37270,16 +37380,7 @@ var AcpClient = class {
37270
37380
  child.stderr?.on("data", (chunk) => {
37271
37381
  for (const line of chunk.split(/\r?\n/)) {
37272
37382
  const trimmed = line.trim();
37273
- if (trimmed) {
37274
- log.info("acpAdapter", trimmed);
37275
- this.opts.onStderr?.(trimmed);
37276
- this.recentStderr.push(trimmed);
37277
- if (this.recentStderr.length > 12) this.recentStderr.shift();
37278
- if (this.startupFailureReject && FATAL_STARTUP_RE.test(trimmed)) {
37279
- this.startupFailureReject(new Error(`AGENT_STARTUP_FAILED: ${trimmed}`));
37280
- this.startupFailureReject = null;
37281
- }
37282
- }
37383
+ if (trimmed) this.onAdapterStderrLine(trimmed);
37283
37384
  }
37284
37385
  });
37285
37386
  child.on("exit", (code, signal) => {
@@ -37479,9 +37580,16 @@ var AcpClient = class {
37479
37580
  PROMPT_ACTIVE_IDLE_TIMEOUT_MS
37480
37581
  );
37481
37582
  this.promptIdle = idle;
37583
+ let fatalTripped = false;
37584
+ const fatal = new Promise((_2, reject) => {
37585
+ this.turnFailureReject = (err) => {
37586
+ fatalTripped = true;
37587
+ reject(err);
37588
+ };
37589
+ });
37482
37590
  this.pendingToolCalls.clear();
37483
37591
  try {
37484
- const result = await Promise.race([send, idle.promise]);
37592
+ const result = await Promise.race([send, idle.promise, fatal]);
37485
37593
  await new Promise((resolve13) => setImmediate(resolve13));
37486
37594
  log.info(
37487
37595
  "acpClient",
@@ -37495,15 +37603,32 @@ var AcpClient = class {
37495
37603
  "acpClient",
37496
37604
  `prompt \u2190 failed elapsedMs=${Date.now() - t0} err=${err instanceof Error ? err.message : String(err)}`
37497
37605
  );
37498
- if (idleTripped) await this.cancelAbandonedTurn(sessionId, send);
37606
+ if (idleTripped || fatalTripped) await this.cancelAbandonedTurn(sessionId, send);
37499
37607
  throw err;
37500
37608
  } finally {
37501
37609
  idle.clear();
37502
37610
  this.promptIdle = null;
37611
+ this.turnFailureReject = null;
37612
+ }
37613
+ }
37614
+ /** One non-empty adapter stderr line: logged, kept in the ring, and checked
37615
+ * for the fatal startup / fatal turn errors that end a stuck RPC early. */
37616
+ onAdapterStderrLine(trimmed) {
37617
+ log.info("acpAdapter", trimmed);
37618
+ this.opts.onStderr?.(trimmed);
37619
+ this.recentStderr.push(trimmed);
37620
+ if (this.recentStderr.length > 12) this.recentStderr.shift();
37621
+ if (this.startupFailureReject && FATAL_STARTUP_RE.test(trimmed)) {
37622
+ this.startupFailureReject(new Error(`AGENT_STARTUP_FAILED: ${trimmed}`));
37623
+ this.startupFailureReject = null;
37624
+ }
37625
+ if (this.turnFailureReject && this.fatalTurnStderr?.test(trimmed)) {
37626
+ this.turnFailureReject(new Error(`AGENT_TURN_FAILED: ${agentErrorOf(trimmed)}`));
37627
+ this.turnFailureReject = null;
37503
37628
  }
37504
37629
  }
37505
37630
  /**
37506
- * Cancel a turn the idle watchdog abandoned and wait, bounded, for the
37631
+ * Cancel a turn the idle watchdog (or a fatal stderr error) abandoned and wait, bounded, for the
37507
37632
  * adapter to answer its `session/prompt` — the proof the turn is over before
37508
37633
  * the next prompt is sent. Best-effort: a dead adapter can't hold the queue
37509
37634
  * longer than {@link ABANDONED_TURN_SETTLE_MS}.
@@ -43598,7 +43723,7 @@ async function runStartTaskTurn(ctx, payload, blocks, promptText) {
43598
43723
  reportTurn2(
43599
43724
  ctx,
43600
43725
  "failed",
43601
- bubble === AUTH_FAILURE_MESSAGE ? "AUTH_FAILED" : bubble === ONE_M_CREDITS_MESSAGE ? "ONE_M_CREDITS" : failed.partialReplyKept ? "TURN_ERROR_PARTIAL" : "TURN_ERROR"
43726
+ bubble === AUTH_FAILURE_MESSAGE ? "AUTH_FAILED" : bubble === ONE_M_CREDITS_MESSAGE ? "ONE_M_CREDITS" : bubble === AGENT_FREE_LIMIT_MESSAGE ? "AGENT_FREE_LIMIT" : failed.partialReplyKept ? "TURN_ERROR_PARTIAL" : "TURN_ERROR"
43602
43727
  );
43603
43728
  if (bubble === AUTH_FAILURE_MESSAGE || bubble === ONE_M_CREDITS_MESSAGE) {
43604
43729
  void reportCredentialInvalid(opts);
@@ -51542,7 +51667,7 @@ async function autoUpgradeBeforeCriticalCommand() {
51542
51667
  if (process.env.NODE_ENV === "test") return;
51543
51668
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
51544
51669
  if (process.env.CI) return;
51545
- const current2 = true ? "2.76.33" : null;
51670
+ const current2 = true ? "2.76.35" : null;
51546
51671
  if (!current2) return;
51547
51672
  const cache = readCache();
51548
51673
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -51559,7 +51684,7 @@ function checkForUpdates() {
51559
51684
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
51560
51685
  if (process.env.CI) return;
51561
51686
  if (!process.stdout.isTTY) return;
51562
- const current2 = true ? "2.76.33" : null;
51687
+ const current2 = true ? "2.76.35" : null;
51563
51688
  if (!current2) return;
51564
51689
  const cache = readCache();
51565
51690
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -51735,14 +51860,37 @@ function pidAlive3(pid) {
51735
51860
  return err.code === "EPERM";
51736
51861
  }
51737
51862
  }
51863
+ var CLK_TCK = 100;
51864
+ function processStartMs(pid) {
51865
+ try {
51866
+ const stat3 = fs96.readFileSync(`/proc/${pid}/stat`, "utf8");
51867
+ const ticks = Number(stat3.slice(stat3.lastIndexOf(")") + 2).split(" ")[19]);
51868
+ const btime = /^btime (\d+)$/m.exec(fs96.readFileSync("/proc/stat", "utf8"))?.[1];
51869
+ if (!Number.isFinite(ticks) || !btime) return null;
51870
+ return Number(btime) * 1e3 + ticks * 1e3 / CLK_TCK;
51871
+ } catch {
51872
+ return null;
51873
+ }
51874
+ }
51875
+ var PID_REUSE_SLACK_MS = 2e3;
51738
51876
  function livePids(versionDir) {
51877
+ const dir = path109.join(versionDir, ".pids");
51739
51878
  let names;
51740
51879
  try {
51741
- names = fs96.readdirSync(path109.join(versionDir, ".pids"));
51880
+ names = fs96.readdirSync(dir);
51742
51881
  } catch {
51743
51882
  return [];
51744
51883
  }
51745
- return names.map(Number).filter((pid) => Number.isInteger(pid) && pid > 0 && pidAlive3(pid));
51884
+ return names.map(Number).filter((pid) => {
51885
+ if (!Number.isInteger(pid) || pid <= 0 || !pidAlive3(pid)) return false;
51886
+ const started = processStartMs(pid);
51887
+ if (started === null) return true;
51888
+ try {
51889
+ return started <= fs96.statSync(path109.join(dir, String(pid))).mtimeMs + PID_REUSE_SLACK_MS;
51890
+ } catch {
51891
+ return true;
51892
+ }
51893
+ });
51746
51894
  }
51747
51895
  function registerRunningVersion(root = cliVersionsRoot(), entry = process.argv[1]) {
51748
51896
  const dir = versionDirContaining(root, entry);
@@ -51842,7 +51990,7 @@ var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
51842
51990
  var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
51843
51991
  var SELF_UPDATE_LS_TIMEOUT_MS = 15e3;
51844
51992
  function currentCliVersion() {
51845
- return true ? "2.76.33" : null;
51993
+ return true ? "2.76.35" : null;
51846
51994
  }
51847
51995
  async function installedVersion(deps) {
51848
51996
  const ls = await deps.run(
@@ -51927,6 +52075,7 @@ async function runSelfUpdateWith(deps) {
51927
52075
  }
51928
52076
  if (compareSemver(latest, current2) <= 0) {
51929
52077
  log.info("host-agent", `self-update: running ${current2}, npm latest ${latest} \u2014 up to date`);
52078
+ if (versionedModeSupported(deps.platform)) collectOldVersions(root);
51930
52079
  return { status: "current" };
51931
52080
  }
51932
52081
  if (!versionedModeSupported(deps.platform)) {
@@ -51943,6 +52092,17 @@ async function runSelfUpdateWith(deps) {
51943
52092
  return { status: "skipped" };
51944
52093
  }
51945
52094
  }
52095
+ function collectOldVersions(root) {
52096
+ const release3 = acquireInstallLock(root);
52097
+ if (!release3) return [];
52098
+ try {
52099
+ const removed = gcVersions(root);
52100
+ if (removed.length) log.info("host-agent", `self-update: gc removed ${removed.join(", ")}`);
52101
+ return removed;
52102
+ } finally {
52103
+ release3();
52104
+ }
52105
+ }
51946
52106
  var VERIFY_TIMEOUT_MS = 3e4;
51947
52107
  async function installVersioned(deps, root, latest, running2) {
51948
52108
  const node = deps.nodePath ?? process.execPath;
@@ -52053,7 +52213,7 @@ function signalHostReady(send = process.send?.bind(process)) {
52053
52213
  }
52054
52214
  function defaultDeps() {
52055
52215
  return {
52056
- ownVersion: true ? "2.76.33" : null,
52216
+ ownVersion: true ? "2.76.35" : null,
52057
52217
  root: cliVersionsRoot(),
52058
52218
  args: process.argv.slice(3),
52059
52219
  execPath: process.execPath,
@@ -54591,7 +54751,7 @@ function checkChokidar() {
54591
54751
  }
54592
54752
  async function doctor(args2 = []) {
54593
54753
  const json = args2.includes("--json");
54594
- const cliVersion = true ? "2.76.33" : "0.0.0-dev";
54754
+ const cliVersion = true ? "2.76.35" : "0.0.0-dev";
54595
54755
  const apiBase2 = resolveApiBaseUrl();
54596
54756
  const diagnosticId = (0, import_node_crypto17.randomUUID)();
54597
54757
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -55127,7 +55287,7 @@ async function mcpWarm(argv = [], opts = {}) {
55127
55287
  // src/commands/version.ts
55128
55288
  var import_picocolors15 = __toESM(require("picocolors"));
55129
55289
  function version2() {
55130
- const v = true ? "2.76.33" : "unknown";
55290
+ const v = true ? "2.76.35" : "unknown";
55131
55291
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
55132
55292
  }
55133
55293
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.76.33",
3
+ "version": "2.76.35",
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",
@@ -88,7 +88,7 @@
88
88
  "zod": "^4.3.6"
89
89
  },
90
90
  "optionalDependencies": {
91
- "@beads/bd": "1.0.5"
91
+ "@beads/bd": "1.3.1"
92
92
  },
93
93
  "devDependencies": {
94
94
  "@codeam/shared": "*",