codeam-cli 2.61.17 → 2.61.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ 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.61.17] — 2026-07-18
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Reuse bounded adapter-resolve retry in baton + codespace-gate paths; honest fail-then-succeed retry test
12
+ - **cli:** Make buildNpmInstallInvocation platform-injectable → deterministic cross-OS tests (fixes red Windows CI)
13
+ - **cli:** Never wipe a streamed transcript on a turn error (false 'couldn't finish')
14
+
7
15
  ## [2.61.16] — 2026-07-17
8
16
 
9
17
  ### Fixed
package/dist/index.js CHANGED
@@ -6024,7 +6024,7 @@ function readAnonId() {
6024
6024
  }
6025
6025
  function superProperties() {
6026
6026
  return {
6027
- cliVersion: true ? "2.61.17" : "0.0.0-dev",
6027
+ cliVersion: true ? "2.61.18" : "0.0.0-dev",
6028
6028
  nodeVersion: process.version,
6029
6029
  platform: process.platform,
6030
6030
  arch: process.arch,
@@ -6205,7 +6205,7 @@ var os4 = __toESM(require("os"));
6205
6205
  // package.json
6206
6206
  var package_default = {
6207
6207
  name: "codeam-cli",
6208
- version: "2.61.17",
6208
+ version: "2.61.18",
6209
6209
  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.",
6210
6210
  type: "commonjs",
6211
6211
  main: "dist/index.js",
@@ -7348,7 +7348,7 @@ var CommandRelayService = class _CommandRelayService {
7348
7348
  // fresh + clear the "CLI update available" banner after a self-update
7349
7349
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
7350
7350
  // pair/reconnect). Older backends ignore the extra field.
7351
- ..."2.61.17" ? { ideVersion: "2.61.17" } : {}
7351
+ ..."2.61.18" ? { ideVersion: "2.61.18" } : {}
7352
7352
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
7353
7353
  }
7354
7354
  /**
@@ -17949,7 +17949,7 @@ async function autoUpgradeBeforeCriticalCommand() {
17949
17949
  if (process.env.NODE_ENV === "test") return;
17950
17950
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17951
17951
  if (process.env.CI) return;
17952
- const current = true ? "2.61.17" : null;
17952
+ const current = true ? "2.61.18" : null;
17953
17953
  if (!current) return;
17954
17954
  const cache = readCache();
17955
17955
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17966,7 +17966,7 @@ function checkForUpdates() {
17966
17966
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17967
17967
  if (process.env.CI) return;
17968
17968
  if (!process.stdout.isTTY) return;
17969
- const current = true ? "2.61.17" : null;
17969
+ const current = true ? "2.61.18" : null;
17970
17970
  if (!current) return;
17971
17971
  const cache = readCache();
17972
17972
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17986,7 +17986,7 @@ var SELF_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
17986
17986
  var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
17987
17987
  var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
17988
17988
  function currentCliVersion() {
17989
- return true ? "2.61.17" : null;
17989
+ return true ? "2.61.18" : null;
17990
17990
  }
17991
17991
  function runCmd(cmd, args2, timeoutMs) {
17992
17992
  return new Promise((resolve8) => {
@@ -29192,6 +29192,34 @@ var AcpClient = class {
29192
29192
  connection = null;
29193
29193
  stopping = false;
29194
29194
  sessionId = null;
29195
+ /** The id of the NATIVE ACP `category:'model'` `SessionConfigSelect` option
29196
+ * captured from `newSession`/`loadSession` `configOptions`. It's the SINGLE
29197
+ * source of truth for model listing/switching: `session/set_config_option`
29198
+ * targets this id, and `undefined` here means the agent exposes no native
29199
+ * model selector → {@link getAvailableModels} is empty and {@link setModel}
29200
+ * throws (mobile then hides the picker). */
29201
+ modelConfigId = void 0;
29202
+ /** The models the native model config option advertises — mapped from its
29203
+ * `options` (id = option `value`, label = option `name`). Empty when the
29204
+ * agent has no model config option. NO hardcoded fallback. */
29205
+ availableModels = [];
29206
+ /** The native model select's current value — seeded from the config option's
29207
+ * `currentValue` and updated on a successful {@link setModel}. Exposed via
29208
+ * {@link getCurrentModelId} so `list_models` can report the in-use model to
29209
+ * mobile (the model line under the composer). `undefined` when no model
29210
+ * config option is advertised. */
29211
+ currentModelId = void 0;
29212
+ /** The operating/permission MODES the native ACP `SessionModeState` advertises
29213
+ * (a DIFFERENT axis than models) — mapped from `newSession`/`loadSession`
29214
+ * `modes.availableModes` (id = mode id, label = mode name, description passthrough).
29215
+ * Empty when the agent advertises no modes → {@link getAvailableModes} is empty
29216
+ * and {@link setMode} throws (mobile then hides the mode picker). */
29217
+ availableModes = [];
29218
+ /** The native `SessionModeState.currentModeId` — seeded from `modes.currentModeId`
29219
+ * and updated on a successful {@link setMode}. Exposed via {@link getCurrentModeId}
29220
+ * so `list_modes` can report the in-use mode to mobile. `undefined` when the agent
29221
+ * advertises no modes. */
29222
+ currentModeId = void 0;
29195
29223
  /** Whether the agent advertised `loadSession` on `initialize`. Drives the
29196
29224
  * post-turn session-closed recovery: an agent that supports resume (kimi,
29197
29225
  * claude, codex, gemini) is re-established with `session/load` — preserving
@@ -29412,16 +29440,18 @@ var AcpClient = class {
29412
29440
  }
29413
29441
  this.startAbortReject = null;
29414
29442
  this.sessionId = newSession.sessionId;
29415
- const newSessionMeta = newSession;
29443
+ this.captureModelConfig(newSession.configOptions ?? []);
29444
+ this.captureModes(newSession.modes ?? null);
29445
+ const tier = newSession.currentServiceTier;
29416
29446
  log.info(
29417
29447
  "acpClient",
29418
- `newSession \u2190 ok sessionId=${newSession.sessionId.slice(0, 8)} model=${newSessionMeta.currentModelId ?? "?"} tier=${newSessionMeta.currentServiceTier ?? "?"}`
29448
+ `newSession \u2190 ok sessionId=${newSession.sessionId.slice(0, 8)} model=${this.currentModelId ?? "?"} models=${this.availableModels.length} tier=${tier ?? "?"}`
29419
29449
  );
29420
29450
  return {
29421
29451
  sessionId: newSession.sessionId,
29422
29452
  initialize,
29423
- model: newSessionMeta.currentModelId,
29424
- tier: newSessionMeta.currentServiceTier
29453
+ model: this.currentModelId,
29454
+ tier
29425
29455
  };
29426
29456
  } catch (err) {
29427
29457
  this.cleanupAfterFailedStart(child);
@@ -29628,8 +29658,9 @@ var AcpClient = class {
29628
29658
  }
29629
29659
  log.info("acpClient", `loadSession \u2192 sessionId=${sessionId.slice(0, 8)}`);
29630
29660
  this.opts.beginLoadReplay?.();
29661
+ let loaded;
29631
29662
  try {
29632
- await this.connection.loadSession({
29663
+ loaded = await this.connection.loadSession({
29633
29664
  sessionId,
29634
29665
  cwd: this.opts.cwd,
29635
29666
  mcpServers: this.opts.mcpServers ?? []
@@ -29638,6 +29669,8 @@ var AcpClient = class {
29638
29669
  this.opts.endLoadReplay?.();
29639
29670
  }
29640
29671
  this.sessionId = sessionId;
29672
+ if (loaded?.configOptions) this.captureModelConfig(loaded.configOptions);
29673
+ if (loaded?.modes !== void 0) this.captureModes(loaded.modes ?? null);
29641
29674
  log.info("acpClient", `loadSession \u2190 ok sessionId=${sessionId.slice(0, 8)}`);
29642
29675
  }
29643
29676
  /**
@@ -29666,24 +29699,132 @@ var AcpClient = class {
29666
29699
  }
29667
29700
  }
29668
29701
  /**
29669
- * Switch the active session's model via the non-standard
29670
- * `session/set_model` RPC. The standard ACP SDK doesn't expose
29671
- * this claude-agent-acp and codex-acp implement it as an
29672
- * extension. Adapters without it reject and `change_model`
29673
- * surfaces a clean "not supported" affordance on mobile.
29702
+ * Switch the active session's model via the NATIVE ACP
29703
+ * `session/set_config_option` RPC, targeting the `category:'model'`
29704
+ * `SessionConfigSelect` option captured on newSession/loadSession. This is
29705
+ * the single source of truth — there is no PTY `/model` fallback and no
29706
+ * non-standard `session/set_model` extension.
29707
+ *
29708
+ * Throws a clear error when the agent exposes no model config option, so
29709
+ * `change_model` acks `failed` and mobile surfaces "model picker not
29710
+ * supported on this agent".
29674
29711
  */
29675
29712
  async setModel(modelId) {
29676
29713
  if (!this.connection || !this.sessionId) {
29677
29714
  throw new Error("AcpClient.setModel called before start()");
29678
29715
  }
29679
- log.info("acpClient", `setModel \u2192 ${modelId}`);
29680
- const rawConn = this.connection;
29681
- await rawConn.connection.sendRequest("session/set_model", {
29716
+ if (!this.modelConfigId) {
29717
+ throw new Error(
29718
+ "model selection not available: this agent exposes no ACP model config option"
29719
+ );
29720
+ }
29721
+ log.info("acpClient", `setModel \u2192 ${modelId} (configId=${this.modelConfigId})`);
29722
+ await this.connection.setSessionConfigOption({
29682
29723
  sessionId: this.sessionId,
29683
- modelId
29724
+ configId: this.modelConfigId,
29725
+ value: modelId
29684
29726
  });
29727
+ this.currentModelId = modelId;
29685
29728
  log.info("acpClient", `setModel \u2190 ok modelId=${modelId}`);
29686
29729
  }
29730
+ /**
29731
+ * The models the agent natively advertises via its `category:'model'` config
29732
+ * option (mapped: id = option value, label = option name, contextWindow
29733
+ * derived from the id). Empty array when the agent has no model config option
29734
+ * — mobile then hides the model selector. NO hardcoded fallback.
29735
+ */
29736
+ getAvailableModels() {
29737
+ return this.availableModels;
29738
+ }
29739
+ /** The native model select's current value (or undefined if none advertised). */
29740
+ getCurrentModelId() {
29741
+ return this.currentModelId;
29742
+ }
29743
+ /**
29744
+ * Parse the native model config option out of a `newSession`/`loadSession`
29745
+ * `configOptions` array and populate {@link modelConfigId},
29746
+ * {@link availableModels}, and {@link currentModelId}. The list itself is
29747
+ * native (the option's `options`); `contextWindow` is a display attribute
29748
+ * derived per-id via {@link getContextWindow}. When no `category:'model'`
29749
+ * select option is present, everything resets to empty/undefined — no
29750
+ * hardcoded list, no guessed model.
29751
+ */
29752
+ captureModelConfig(configOptions) {
29753
+ const modelOption = configOptions.find(
29754
+ (o) => o.category === "model" && o.type === "select"
29755
+ );
29756
+ if (!modelOption || modelOption.type !== "select") {
29757
+ this.modelConfigId = void 0;
29758
+ this.availableModels = [];
29759
+ this.currentModelId = void 0;
29760
+ return;
29761
+ }
29762
+ this.modelConfigId = modelOption.id;
29763
+ this.currentModelId = modelOption.currentValue;
29764
+ this.availableModels = flattenSelectOptions(modelOption.options).map((opt) => ({
29765
+ id: opt.value,
29766
+ label: opt.name,
29767
+ contextWindow: getContextWindow(opt.value)
29768
+ }));
29769
+ }
29770
+ /**
29771
+ * Switch the active session's operating/permission MODE via the NATIVE ACP
29772
+ * `session/set_mode` RPC (the standard {@link SessionModeState} axis — DISTINCT
29773
+ * from the model config option). This is the single source of truth — there is
29774
+ * no PTY fallback.
29775
+ *
29776
+ * Throws a clear error when the agent advertises no modes, so `set_mode` acks
29777
+ * `failed` and mobile surfaces "mode switching not supported on this agent".
29778
+ */
29779
+ async setMode(modeId) {
29780
+ if (!this.connection || !this.sessionId) {
29781
+ throw new Error("AcpClient.setMode called before start()");
29782
+ }
29783
+ if (this.availableModes.length === 0) {
29784
+ throw new Error(
29785
+ "mode selection not available: this agent advertises no ACP session modes"
29786
+ );
29787
+ }
29788
+ log.info("acpClient", `setMode \u2192 ${modeId}`);
29789
+ await this.connection.setSessionMode({
29790
+ sessionId: this.sessionId,
29791
+ modeId
29792
+ });
29793
+ this.currentModeId = modeId;
29794
+ log.info("acpClient", `setMode \u2190 ok modeId=${modeId}`);
29795
+ }
29796
+ /**
29797
+ * The operating/permission modes the agent natively advertises via its ACP
29798
+ * `SessionModeState` (mapped: id = mode id, label = mode name, description
29799
+ * passthrough). Empty array when the agent advertises no modes — mobile then
29800
+ * hides the mode selector. NO hardcoded fallback.
29801
+ */
29802
+ getAvailableModes() {
29803
+ return this.availableModes;
29804
+ }
29805
+ /** The native current mode id (or undefined if the agent advertises no modes). */
29806
+ getCurrentModeId() {
29807
+ return this.currentModeId;
29808
+ }
29809
+ /**
29810
+ * Parse the native ACP {@link SessionModeState} out of a `newSession`/`loadSession`
29811
+ * response `modes` field and populate {@link availableModes} + {@link currentModeId}.
29812
+ * When absent (`null`/undefined — the agent advertises no modes) everything resets
29813
+ * to empty/undefined — no hardcoded list, no guessed mode.
29814
+ */
29815
+ captureModes(modes) {
29816
+ if (!modes) {
29817
+ this.availableModes = [];
29818
+ this.currentModeId = void 0;
29819
+ return;
29820
+ }
29821
+ this.currentModeId = modes.currentModeId;
29822
+ this.availableModes = (modes.availableModes ?? []).map((mode) => ({
29823
+ id: mode.id,
29824
+ label: mode.name,
29825
+ description: mode.description ?? void 0
29826
+ }));
29827
+ }
29687
29828
  /**
29688
29829
  * Cancel the in-flight prompt turn. Notification — no response.
29689
29830
  * Safe to call when nothing is in flight (the adapter no-ops).
@@ -29836,6 +29977,17 @@ var AcpClient = class {
29836
29977
  };
29837
29978
  }
29838
29979
  };
29980
+ function flattenSelectOptions(options) {
29981
+ const out2 = [];
29982
+ for (const entry of options) {
29983
+ if ("group" in entry) {
29984
+ out2.push(...entry.options);
29985
+ } else {
29986
+ out2.push(entry);
29987
+ }
29988
+ }
29989
+ return out2;
29990
+ }
29839
29991
  function applyLineRange(content, line, limit) {
29840
29992
  if (line === null && limit === null) return { content };
29841
29993
  const lines = content.split("\n");
@@ -31792,8 +31944,38 @@ async function getConversationH(ctx) {
31792
31944
  return;
31793
31945
  }
31794
31946
  async function listModelsH(ctx) {
31795
- const { cmd, relay, models } = ctx;
31796
- await relay.sendResult(cmd.id, "completed", { models });
31947
+ const { cmd, relay, client: client3 } = ctx;
31948
+ await relay.sendResult(cmd.id, "completed", {
31949
+ models: client3.getAvailableModels(),
31950
+ currentModelId: client3.getCurrentModelId()
31951
+ });
31952
+ return;
31953
+ }
31954
+ async function listModesH(ctx) {
31955
+ const { cmd, relay, client: client3 } = ctx;
31956
+ await relay.sendResult(cmd.id, "completed", {
31957
+ modes: client3.getAvailableModes(),
31958
+ currentModeId: client3.getCurrentModeId()
31959
+ });
31960
+ return;
31961
+ }
31962
+ async function setModeH(ctx) {
31963
+ const { cmd, client: client3, relay, opts } = ctx;
31964
+ const payload = cmd.payload;
31965
+ const modeId = payload?.modeId?.trim();
31966
+ if (!modeId) {
31967
+ await relay.sendResult(cmd.id, "failed", { error: "modeId required" });
31968
+ return;
31969
+ }
31970
+ try {
31971
+ await client3.setMode(modeId);
31972
+ await relay.sendResult(cmd.id, "completed", { modeId });
31973
+ } catch (err) {
31974
+ log.warn("acpRunner", `set_mode failed: ${describeError(err)}`);
31975
+ await relay.sendResult(cmd.id, "failed", {
31976
+ error: `Mode switching not supported on ${opts.agent} via ACP: ${describeError(err)}`
31977
+ });
31978
+ }
31797
31979
  return;
31798
31980
  }
31799
31981
  async function ackEmptyH(ctx) {
@@ -32049,6 +32231,8 @@ var ACP_COMMAND_HANDLERS = {
32049
32231
  escape_key: stopTaskH,
32050
32232
  get_conversation: getConversationH,
32051
32233
  list_models: listModelsH,
32234
+ list_modes: listModesH,
32235
+ set_mode: setModeH,
32052
32236
  set_keep_alive: ackEmptyH,
32053
32237
  get_context: ackEmptyH,
32054
32238
  select_option: selectOptionH,
@@ -32795,7 +32979,6 @@ async function runAcpSession(opts) {
32795
32979
  done: true
32796
32980
  });
32797
32981
  const runtime = createInteractiveAgentStrategy(opts.agent, createOsStrategy());
32798
- const models = await runtime.listModels();
32799
32982
  const history = new AcpHistory(publisher, {
32800
32983
  agent: opts.agent,
32801
32984
  acpSessionId,
@@ -32871,7 +33054,6 @@ async function runAcpSession(opts) {
32871
33054
  client3,
32872
33055
  relay,
32873
33056
  acpSessionId,
32874
- models,
32875
33057
  streaming,
32876
33058
  opts,
32877
33059
  history,
@@ -32927,12 +33109,11 @@ async function runAcpSession(opts) {
32927
33109
  await new Promise(() => {
32928
33110
  });
32929
33111
  }
32930
- async function handleCommand(cmd, client3, relay, acpSessionId, models, streaming, opts, history, jsonlHistory, agentCaps, turnFiles, getBeads, publisher, recentStderr, budgetRecovery, budgetReachedFlag, onActiveSessionChanged) {
33112
+ async function handleCommand(cmd, client3, relay, acpSessionId, streaming, opts, history, jsonlHistory, agentCaps, turnFiles, getBeads, publisher, recentStderr, budgetRecovery, budgetReachedFlag, onActiveSessionChanged) {
32931
33113
  const session = {
32932
33114
  client: client3,
32933
33115
  relay,
32934
33116
  acpSessionId,
32935
- models,
32936
33117
  streaming,
32937
33118
  opts,
32938
33119
  history,
@@ -34268,7 +34449,6 @@ var AcpDriver = class {
34268
34449
  async ensureSession(relay, acpSessionId, agentCaps) {
34269
34450
  if (this.session) return this.session;
34270
34451
  const { opts, publisher, streaming, runtime, recentStderr } = this.deps;
34271
- const models = await runtime.listModels();
34272
34452
  const jsonlHistory = new HistoryService(runtime, opts.pluginId, opts.cwd, {
34273
34453
  pluginAuthToken: opts.pluginAuthToken
34274
34454
  });
@@ -34308,7 +34488,6 @@ var AcpDriver = class {
34308
34488
  client: this.deps.client,
34309
34489
  relay,
34310
34490
  acpSessionId,
34311
- models,
34312
34491
  streaming,
34313
34492
  opts,
34314
34493
  history,
@@ -37633,7 +37812,7 @@ function checkChokidar() {
37633
37812
  }
37634
37813
  async function doctor(args2 = []) {
37635
37814
  const json = args2.includes("--json");
37636
- const cliVersion = true ? "2.61.17" : "0.0.0-dev";
37815
+ const cliVersion = true ? "2.61.18" : "0.0.0-dev";
37637
37816
  const apiBase2 = resolveApiBaseUrl();
37638
37817
  const diagnosticId = (0, import_node_crypto12.randomUUID)();
37639
37818
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -38144,7 +38323,7 @@ async function mcpRun(args2) {
38144
38323
  // src/commands/version.ts
38145
38324
  var import_picocolors15 = __toESM(require("picocolors"));
38146
38325
  function version2() {
38147
- const v = true ? "2.61.17" : "unknown";
38326
+ const v = true ? "2.61.18" : "unknown";
38148
38327
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
38149
38328
  }
38150
38329
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.61.17",
3
+ "version": "2.61.18",
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",