codeam-cli 2.73.2 → 2.73.4

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 +16 -0
  2. package/dist/index.js +140 -36
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ 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.73.3] — 2026-09-02
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Debounce MCP reprovisions so a run of changes costs ONE respawn (#684)
12
+
13
+ ## [2.73.2] — 2026-09-02
14
+
15
+ ### CI
16
+
17
+ - **release:** Publish the release notes to #changelog and Discussions (#680)
18
+
19
+ ### Fixed
20
+
21
+ - **cli:** An MCP reprovision burst must not kill the session (#682)
22
+
7
23
  ## [2.73.1] — 2026-09-02
8
24
 
9
25
  ### Added
package/dist/index.js CHANGED
@@ -8097,7 +8097,7 @@ function readAnonId() {
8097
8097
  }
8098
8098
  function superProperties() {
8099
8099
  return {
8100
- cliVersion: true ? "2.73.2" : "0.0.0-dev",
8100
+ cliVersion: true ? "2.73.4" : "0.0.0-dev",
8101
8101
  nodeVersion: process.version,
8102
8102
  platform: process.platform,
8103
8103
  arch: process.arch,
@@ -8339,7 +8339,7 @@ var http = __toESM(require("http"));
8339
8339
  // package.json
8340
8340
  var package_default = {
8341
8341
  name: "codeam-cli",
8342
- version: "2.73.2",
8342
+ version: "2.73.4",
8343
8343
  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.",
8344
8344
  type: "commonjs",
8345
8345
  main: "dist/index.js",
@@ -9932,7 +9932,7 @@ var CommandRelayService = class _CommandRelayService {
9932
9932
  // fresh + clear the "CLI update available" banner after a self-update
9933
9933
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
9934
9934
  // pair/reconnect). Older backends ignore the extra field.
9935
- ..."2.73.2" ? { ideVersion: "2.73.2" } : {}
9935
+ ..."2.73.4" ? { ideVersion: "2.73.4" } : {}
9936
9936
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
9937
9937
  }
9938
9938
  /**
@@ -22208,7 +22208,7 @@ async function autoUpgradeBeforeCriticalCommand() {
22208
22208
  if (process.env.NODE_ENV === "test") return;
22209
22209
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
22210
22210
  if (process.env.CI) return;
22211
- const current2 = true ? "2.73.2" : null;
22211
+ const current2 = true ? "2.73.4" : null;
22212
22212
  if (!current2) return;
22213
22213
  const cache = readCache();
22214
22214
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -22225,7 +22225,7 @@ function checkForUpdates() {
22225
22225
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
22226
22226
  if (process.env.CI) return;
22227
22227
  if (!process.stdout.isTTY) return;
22228
- const current2 = true ? "2.73.2" : null;
22228
+ const current2 = true ? "2.73.4" : null;
22229
22229
  if (!current2) return;
22230
22230
  const cache = readCache();
22231
22231
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -22248,7 +22248,7 @@ var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
22248
22248
  var SELF_UPDATE_LS_TIMEOUT_MS = 15e3;
22249
22249
  var SUDO_PREFLIGHT_TIMEOUT_MS = 5e3;
22250
22250
  function currentCliVersion() {
22251
- return true ? "2.73.2" : null;
22251
+ return true ? "2.73.4" : null;
22252
22252
  }
22253
22253
  async function installedVersion(deps) {
22254
22254
  const ls = await deps.run(
@@ -35677,6 +35677,7 @@ var _acpStartSeam = {
35677
35677
  var PROTOCOL_VERSION3 = 1;
35678
35678
  var FATAL_STARTUP_RE = /IneligibleTierError|UNSUPPORTED_CLIENT|no longer supported for Gemini Code Assist|not eligible for Gemini Code Assist|Error authenticating|ProjectIdRequiredError/i;
35679
35679
  var NEWSESSION_TIMEOUT_MS = 12e4;
35680
+ var MCP_STARTUP_TIMEOUT_MS = NEWSESSION_TIMEOUT_MS - 3e4;
35680
35681
  var LOADSESSION_TIMEOUT_MS = 6e4;
35681
35682
  var PROMPT_IDLE_TIMEOUT_MS = 9e4;
35682
35683
  var PROMPT_ACTIVE_IDLE_TIMEOUT_MS = 6e5;
@@ -35684,6 +35685,7 @@ var CLIENT_CAPABILITIES = {
35684
35685
  fs: { readTextFile: true, writeTextFile: true },
35685
35686
  terminal: false
35686
35687
  };
35688
+ var MCP_REPROVISION_DEBOUNCE_MS = 1e4;
35687
35689
  var AcpClient = class {
35688
35690
  constructor(opts) {
35689
35691
  this.opts = opts;
@@ -35756,8 +35758,9 @@ var AcpClient = class {
35756
35758
  /** True once a start has fully succeeded (handshake + session). Distinguishes
35757
35759
  * "never started" (a programming error) from "the adapter died" (recoverable). */
35758
35760
  startedOnce = false;
35759
- mcpReprovisionQueued = false;
35760
35761
  mcpReprovisionPending = null;
35762
+ mcpDebounceTimer = null;
35763
+ mcpDebounceWaiters = [];
35761
35764
  /** Last few adapter stderr lines — so a startup failure surfaces the REAL
35762
35765
  * cause (e.g. gemini's `IneligibleTierError`) instead of a bare timeout. */
35763
35766
  recentStderr = [];
@@ -35853,7 +35856,15 @@ var AcpClient = class {
35853
35856
  // extraEnv (e.g. CLAUDE_CODE_DISABLE_1M_CONTEXT=1 on an on-demand
35854
35857
  // re-spawn) layers over process.env; PATH stays last so the augmented
35855
35858
  // PATH always wins.
35856
- env: { ...process.env, ...this.opts.extraEnv ?? {}, PATH: augmentedPath },
35859
+ //
35860
+ // MCP_TIMEOUT comes FIRST so the caller can still override it, and it is
35861
+ // load-bearing — see MCP_STARTUP_TIMEOUT_MS.
35862
+ env: {
35863
+ ...process.env,
35864
+ MCP_TIMEOUT: String(MCP_STARTUP_TIMEOUT_MS),
35865
+ ...this.opts.extraEnv ?? {},
35866
+ PATH: augmentedPath
35867
+ },
35857
35868
  stdio: ["pipe", "pipe", "pipe"]
35858
35869
  });
35859
35870
  this.child = child;
@@ -36268,19 +36279,30 @@ var AcpClient = class {
36268
36279
  this.opts.mcpServers = servers;
36269
36280
  if (!this.connection || !this.sessionId) return "deferred";
36270
36281
  this.mcpReprovisionPending = servers;
36271
- if (this.mcpReprovisionQueued) return "deferred";
36272
- this.mcpReprovisionQueued = true;
36273
- const run = this.promptChain.then(() => {
36274
- this.mcpReprovisionQueued = false;
36282
+ if (this.mcpDebounceTimer) clearTimeout(this.mcpDebounceTimer);
36283
+ const settled = new Promise((resolve11) => {
36284
+ this.mcpDebounceWaiters.push(resolve11);
36285
+ });
36286
+ this.mcpDebounceTimer = setTimeout(() => {
36287
+ this.mcpDebounceTimer = null;
36275
36288
  const latest = this.mcpReprovisionPending ?? servers;
36276
36289
  this.mcpReprovisionPending = null;
36277
- return this.runReprovisionMcp(latest);
36278
- });
36279
- this.promptChain = run.then(
36280
- () => void 0,
36281
- () => void 0
36282
- );
36283
- return run;
36290
+ const waiters = this.mcpDebounceWaiters;
36291
+ this.mcpDebounceWaiters = [];
36292
+ const run = this.promptChain.then(() => this.runReprovisionMcp(latest));
36293
+ this.promptChain = run.then(
36294
+ () => void 0,
36295
+ () => void 0
36296
+ );
36297
+ run.then(
36298
+ (result) => waiters.forEach((w3) => w3(result)),
36299
+ // `runReprovisionMcp` already swallows its own failures into
36300
+ // 'deferred'; this is the belt for anything the chain itself throws.
36301
+ () => waiters.forEach((w3) => w3("deferred"))
36302
+ );
36303
+ }, this.opts.mcpReprovisionDebounceMs ?? MCP_REPROVISION_DEBOUNCE_MS);
36304
+ this.mcpDebounceTimer.unref?.();
36305
+ return settled;
36284
36306
  }
36285
36307
  /** The actual respawn. Only ever called from {@link reprovisionMcp}'s queued
36286
36308
  * slot, so it can assume no turn and no other respawn is running. */
@@ -36480,6 +36502,14 @@ var AcpClient = class {
36480
36502
  */
36481
36503
  async stop() {
36482
36504
  this.stopping = true;
36505
+ if (this.mcpDebounceTimer) {
36506
+ clearTimeout(this.mcpDebounceTimer);
36507
+ this.mcpDebounceTimer = null;
36508
+ this.mcpReprovisionPending = null;
36509
+ const waiters = this.mcpDebounceWaiters;
36510
+ this.mcpDebounceWaiters = [];
36511
+ waiters.forEach((w3) => w3("deferred"));
36512
+ }
36483
36513
  const child = this.child;
36484
36514
  if (!child) return;
36485
36515
  this.child = null;
@@ -43681,12 +43711,22 @@ var TranscriptMirror = class {
43681
43711
  unwatch = null;
43682
43712
  pollHandle = null;
43683
43713
  attached = false;
43714
+ /** True while `start()`'s own synchronous attach attempt is running, so
43715
+ * `tryAttach` can tell "the file was already there" from "it appeared while
43716
+ * we waited". The distinction is the whole basis for whether the first
43717
+ * batch is history or live content. */
43718
+ attachingAtStart = false;
43684
43719
  pollIntervalMs;
43685
43720
  waitTimeoutMs;
43686
43721
  setIntervalFn;
43687
43722
  clearIntervalFn;
43688
43723
  start() {
43689
- if (this.tryAttach()) return;
43724
+ this.attachingAtStart = true;
43725
+ try {
43726
+ if (this.tryAttach()) return;
43727
+ } finally {
43728
+ this.attachingAtStart = false;
43729
+ }
43690
43730
  let waited = 0;
43691
43731
  this.pollHandle = this.setIntervalFn(() => {
43692
43732
  waited += this.pollIntervalMs;
@@ -43709,7 +43749,7 @@ var TranscriptMirror = class {
43709
43749
  const file = this.deps.runtime.resolveHistoryFile?.(this.deps.cwd, this.deps.conversationId);
43710
43750
  if (!file) return false;
43711
43751
  this.attached = true;
43712
- this.emit(file);
43752
+ this.emit(file, this.attachingAtStart);
43713
43753
  const watch4 = this.deps.watch ?? defaultWatch;
43714
43754
  this.unwatch = watch4(file, () => this.emit(file));
43715
43755
  return true;
@@ -43720,7 +43760,7 @@ var TranscriptMirror = class {
43720
43760
  this.pollHandle = null;
43721
43761
  }
43722
43762
  }
43723
- emit(file) {
43763
+ emit(file, preexisting = false) {
43724
43764
  let all;
43725
43765
  try {
43726
43766
  all = this.deps.runtime.parseHistoryFile(file);
@@ -43730,7 +43770,7 @@ var TranscriptMirror = class {
43730
43770
  if (all.length <= this.emitted) return;
43731
43771
  const delta = all.slice(this.emitted);
43732
43772
  this.emitted = all.length;
43733
- this.deps.onNewMessages(delta);
43773
+ this.deps.onNewMessages(delta, { preexisting });
43734
43774
  }
43735
43775
  };
43736
43776
  function defaultWatch(file, onChange) {
@@ -43782,10 +43822,9 @@ async function publishMirroredTurnsLive(publisher, messages) {
43782
43822
  }
43783
43823
  }
43784
43824
  function makeMirrorOnNewMessages(deps) {
43785
- let isFirstEmit = true;
43786
43825
  let publishChain = Promise.resolve();
43787
43826
  const conversation = [];
43788
- return (messages) => {
43827
+ return (messages, meta) => {
43789
43828
  const relevant = messages.filter((m) => m.role !== "system");
43790
43829
  if (relevant.length === 0) return;
43791
43830
  const mapped = relevant.map((m) => ({
@@ -43800,7 +43839,7 @@ function makeMirrorOnNewMessages(deps) {
43800
43839
  sessionId: deps.conversationId,
43801
43840
  messages: conversation.slice()
43802
43841
  });
43803
- if (deps.fresh || !isFirstEmit) {
43842
+ if (meta?.preexisting !== true) {
43804
43843
  publishChain = publishChain.then(() => publishMirroredTurnsLive(deps.publisher, relevant)).catch((err) => {
43805
43844
  log.warn(
43806
43845
  "wireBaton",
@@ -43808,7 +43847,6 @@ function makeMirrorOnNewMessages(deps) {
43808
43847
  );
43809
43848
  });
43810
43849
  }
43811
- isFirstEmit = false;
43812
43850
  };
43813
43851
  }
43814
43852
  function makeSerializedBatonPoster(post2) {
@@ -43935,9 +43973,8 @@ async function runBatonSession(opts) {
43935
43973
  onConversationSwitch: (id) => controller.switchConversation(id)
43936
43974
  });
43937
43975
  let mirror = null;
43938
- let firstLocalDrive = true;
43939
43976
  let lastPublished = null;
43940
- const startMirror = (conversationId, fresh) => {
43977
+ const startMirror = (conversationId) => {
43941
43978
  mirror?.stop();
43942
43979
  mirror = new TranscriptMirror({
43943
43980
  runtime,
@@ -43946,8 +43983,7 @@ async function runBatonSession(opts) {
43946
43983
  onNewMessages: makeMirrorOnNewMessages({
43947
43984
  publisher,
43948
43985
  agentId: opts.agent,
43949
- conversationId,
43950
- fresh
43986
+ conversationId
43951
43987
  })
43952
43988
  });
43953
43989
  mirror.start();
@@ -43969,9 +44005,7 @@ async function runBatonSession(opts) {
43969
44005
  publishState: (state, driver, conversationId) => {
43970
44006
  publishBatonState(state, driver, conversationId);
43971
44007
  if (state === "LOCAL_DRIVE" && conversationId) {
43972
- const switched = lastPublished === "LOCAL_DRIVE";
43973
- startMirror(conversationId, firstLocalDrive || switched);
43974
- firstLocalDrive = false;
44008
+ startMirror(conversationId);
43975
44009
  } else if (state !== "LOCAL_DRIVE") {
43976
44010
  mirror?.stop();
43977
44011
  }
@@ -47065,7 +47099,7 @@ function checkChokidar() {
47065
47099
  }
47066
47100
  async function doctor(args2 = []) {
47067
47101
  const json = args2.includes("--json");
47068
- const cliVersion = true ? "2.73.2" : "0.0.0-dev";
47102
+ const cliVersion = true ? "2.73.4" : "0.0.0-dev";
47069
47103
  const apiBase2 = resolveApiBaseUrl();
47070
47104
  const diagnosticId = (0, import_node_crypto13.randomUUID)();
47071
47105
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -47453,10 +47487,77 @@ async function mcpRun(args2) {
47453
47487
  await proxy.start();
47454
47488
  }
47455
47489
 
47490
+ // src/integrations/mcp-warm.ts
47491
+ var import_node_child_process35 = require("child_process");
47492
+ function specOf(delivery) {
47493
+ if (!delivery.command) return null;
47494
+ const spec = (delivery.args ?? []).find((a) => !a.startsWith("-"));
47495
+ return spec ?? null;
47496
+ }
47497
+ function warmTargets() {
47498
+ const bySpec = /* @__PURE__ */ new Map();
47499
+ for (const integration of getEnabledIntegrations()) {
47500
+ const delivery = integration.delivery?.mcp;
47501
+ if (!delivery) continue;
47502
+ const spec = specOf(delivery);
47503
+ if (!spec) continue;
47504
+ if (!bySpec.has(spec)) {
47505
+ bySpec.set(spec, { id: integration.id, launcher: delivery.command, spec });
47506
+ }
47507
+ }
47508
+ return [...bySpec.values()];
47509
+ }
47510
+ function warmCommand(target) {
47511
+ if (target.launcher === "npx") {
47512
+ return { command: "npx", args: ["-y", "--package", target.spec, "-c", "true"] };
47513
+ }
47514
+ if (target.launcher === "uvx") {
47515
+ return { command: "uv", args: ["tool", "install", target.spec] };
47516
+ }
47517
+ return null;
47518
+ }
47519
+ async function mcpWarm(argv = []) {
47520
+ const timeoutMs = 3e5;
47521
+ const targets = warmTargets();
47522
+ const only = argv.filter((a) => !a.startsWith("-"));
47523
+ const selected = only.length > 0 ? targets.filter((t2) => only.includes(t2.id)) : targets;
47524
+ const result = { warmed: [], skipped: [], failed: [] };
47525
+ console.log(`[codeam mcp-warm] ${selected.length} MCP package(s) to pre-fetch`);
47526
+ for (const target of selected) {
47527
+ const cmd = warmCommand(target);
47528
+ if (!cmd) {
47529
+ result.skipped.push(`${target.spec} (no warm recipe for '${target.launcher}')`);
47530
+ continue;
47531
+ }
47532
+ const started = Date.now();
47533
+ const run = (0, import_node_child_process35.spawnSync)(cmd.command, cmd.args, {
47534
+ stdio: ["ignore", "ignore", "pipe"],
47535
+ timeout: timeoutMs,
47536
+ encoding: "utf8"
47537
+ });
47538
+ const secs = ((Date.now() - started) / 1e3).toFixed(1);
47539
+ if (run.status === 0) {
47540
+ result.warmed.push(target.spec);
47541
+ console.log(` OK ${target.spec} (${secs}s)`);
47542
+ } else {
47543
+ const reason = run.error?.message ?? (run.stderr ?? "").trim().split("\n").slice(-1)[0] ?? `exit ${String(run.status)}`;
47544
+ result.failed.push({ spec: target.spec, reason });
47545
+ console.log(` MISS ${target.spec} (${secs}s) \u2014 fetched at runtime instead: ${reason}`);
47546
+ }
47547
+ }
47548
+ console.log(
47549
+ `[codeam mcp-warm] warmed=${result.warmed.length} failed=${result.failed.length} skipped=${result.skipped.length}`
47550
+ );
47551
+ log.info(
47552
+ "mcpWarm",
47553
+ `warmed=${result.warmed.length} failed=${result.failed.length} skipped=${result.skipped.length}`
47554
+ );
47555
+ }
47556
+
47456
47557
  // src/commands/version.ts
47457
47558
  var import_picocolors15 = __toESM(require("picocolors"));
47458
47559
  function version2() {
47459
- const v = true ? "2.73.2" : "unknown";
47560
+ const v = true ? "2.73.4" : "unknown";
47460
47561
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
47461
47562
  }
47462
47563
 
@@ -47657,6 +47758,9 @@ async function main() {
47657
47758
  if (command === "mcp-run") {
47658
47759
  return mcpRun(args);
47659
47760
  }
47761
+ if (command === "mcp-warm") {
47762
+ return mcpWarm(args);
47763
+ }
47660
47764
  if (command === "deploy") {
47661
47765
  if (args[0] === "ls" || args[0] === "list") return deployList();
47662
47766
  if (args[0] === "stop" || args[0] === "remove") return deployStop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.73.2",
3
+ "version": "2.73.4",
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",