codeam-cli 2.61.17 → 2.61.19

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 +15 -0
  2. package/dist/index.js +216 -30
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ 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.18] — 2026-07-18
8
+
9
+ ### Added
10
+
11
+ - **cli:** List_models reports the in-use model (currentModelId)
12
+ - **cli:** Native ACP model + mode list/switch (single source of truth, no hardcoded/hacky)
13
+
14
+ ## [2.61.17] — 2026-07-18
15
+
16
+ ### Fixed
17
+
18
+ - **cli:** Reuse bounded adapter-resolve retry in baton + codespace-gate paths; honest fail-then-succeed retry test
19
+ - **cli:** Make buildNpmInstallInvocation platform-injectable → deterministic cross-OS tests (fixes red Windows CI)
20
+ - **cli:** Never wipe a streamed transcript on a turn error (false 'couldn't finish')
21
+
7
22
  ## [2.61.16] — 2026-07-17
8
23
 
9
24
  ### Fixed
package/dist/index.js CHANGED
@@ -277,6 +277,10 @@ function getContextWindow(model) {
277
277
  if (!model) return DEFAULT_CONTEXT_WINDOW;
278
278
  return longestPrefixMatch(MODEL_CONTEXT_WINDOW, model) ?? DEFAULT_CONTEXT_WINDOW;
279
279
  }
280
+ function tryGetContextWindow(model) {
281
+ if (!model) return void 0;
282
+ return longestPrefixMatch(MODEL_CONTEXT_WINDOW, model);
283
+ }
280
284
 
281
285
  // ../../packages/shared/src/agents/registry.ts
282
286
  var AGENT_REGISTRY = {
@@ -6024,7 +6028,7 @@ function readAnonId() {
6024
6028
  }
6025
6029
  function superProperties() {
6026
6030
  return {
6027
- cliVersion: true ? "2.61.17" : "0.0.0-dev",
6031
+ cliVersion: true ? "2.61.19" : "0.0.0-dev",
6028
6032
  nodeVersion: process.version,
6029
6033
  platform: process.platform,
6030
6034
  arch: process.arch,
@@ -6205,7 +6209,7 @@ var os4 = __toESM(require("os"));
6205
6209
  // package.json
6206
6210
  var package_default = {
6207
6211
  name: "codeam-cli",
6208
- version: "2.61.17",
6212
+ version: "2.61.19",
6209
6213
  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
6214
  type: "commonjs",
6211
6215
  main: "dist/index.js",
@@ -7348,7 +7352,7 @@ var CommandRelayService = class _CommandRelayService {
7348
7352
  // fresh + clear the "CLI update available" banner after a self-update
7349
7353
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
7350
7354
  // pair/reconnect). Older backends ignore the extra field.
7351
- ..."2.61.17" ? { ideVersion: "2.61.17" } : {}
7355
+ ..."2.61.19" ? { ideVersion: "2.61.19" } : {}
7352
7356
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
7353
7357
  }
7354
7358
  /**
@@ -17949,7 +17953,7 @@ async function autoUpgradeBeforeCriticalCommand() {
17949
17953
  if (process.env.NODE_ENV === "test") return;
17950
17954
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17951
17955
  if (process.env.CI) return;
17952
- const current = true ? "2.61.17" : null;
17956
+ const current = true ? "2.61.19" : null;
17953
17957
  if (!current) return;
17954
17958
  const cache = readCache();
17955
17959
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17966,7 +17970,7 @@ function checkForUpdates() {
17966
17970
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17967
17971
  if (process.env.CI) return;
17968
17972
  if (!process.stdout.isTTY) return;
17969
- const current = true ? "2.61.17" : null;
17973
+ const current = true ? "2.61.19" : null;
17970
17974
  if (!current) return;
17971
17975
  const cache = readCache();
17972
17976
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17986,7 +17990,7 @@ var SELF_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
17986
17990
  var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
17987
17991
  var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
17988
17992
  function currentCliVersion() {
17989
- return true ? "2.61.17" : null;
17993
+ return true ? "2.61.19" : null;
17990
17994
  }
17991
17995
  function runCmd(cmd, args2, timeoutMs) {
17992
17996
  return new Promise((resolve8) => {
@@ -29192,6 +29196,34 @@ var AcpClient = class {
29192
29196
  connection = null;
29193
29197
  stopping = false;
29194
29198
  sessionId = null;
29199
+ /** The id of the NATIVE ACP `category:'model'` `SessionConfigSelect` option
29200
+ * captured from `newSession`/`loadSession` `configOptions`. It's the SINGLE
29201
+ * source of truth for model listing/switching: `session/set_config_option`
29202
+ * targets this id, and `undefined` here means the agent exposes no native
29203
+ * model selector → {@link getAvailableModels} is empty and {@link setModel}
29204
+ * throws (mobile then hides the picker). */
29205
+ modelConfigId = void 0;
29206
+ /** The models the native model config option advertises — mapped from its
29207
+ * `options` (id = option `value`, label = option `name`). Empty when the
29208
+ * agent has no model config option. NO hardcoded fallback. */
29209
+ availableModels = [];
29210
+ /** The native model select's current value — seeded from the config option's
29211
+ * `currentValue` and updated on a successful {@link setModel}. Exposed via
29212
+ * {@link getCurrentModelId} so `list_models` can report the in-use model to
29213
+ * mobile (the model line under the composer). `undefined` when no model
29214
+ * config option is advertised. */
29215
+ currentModelId = void 0;
29216
+ /** The operating/permission MODES the native ACP `SessionModeState` advertises
29217
+ * (a DIFFERENT axis than models) — mapped from `newSession`/`loadSession`
29218
+ * `modes.availableModes` (id = mode id, label = mode name, description passthrough).
29219
+ * Empty when the agent advertises no modes → {@link getAvailableModes} is empty
29220
+ * and {@link setMode} throws (mobile then hides the mode picker). */
29221
+ availableModes = [];
29222
+ /** The native `SessionModeState.currentModeId` — seeded from `modes.currentModeId`
29223
+ * and updated on a successful {@link setMode}. Exposed via {@link getCurrentModeId}
29224
+ * so `list_modes` can report the in-use mode to mobile. `undefined` when the agent
29225
+ * advertises no modes. */
29226
+ currentModeId = void 0;
29195
29227
  /** Whether the agent advertised `loadSession` on `initialize`. Drives the
29196
29228
  * post-turn session-closed recovery: an agent that supports resume (kimi,
29197
29229
  * claude, codex, gemini) is re-established with `session/load` — preserving
@@ -29412,16 +29444,18 @@ var AcpClient = class {
29412
29444
  }
29413
29445
  this.startAbortReject = null;
29414
29446
  this.sessionId = newSession.sessionId;
29415
- const newSessionMeta = newSession;
29447
+ this.captureModelConfig(newSession.configOptions ?? []);
29448
+ this.captureModes(newSession.modes ?? null);
29449
+ const tier = newSession.currentServiceTier;
29416
29450
  log.info(
29417
29451
  "acpClient",
29418
- `newSession \u2190 ok sessionId=${newSession.sessionId.slice(0, 8)} model=${newSessionMeta.currentModelId ?? "?"} tier=${newSessionMeta.currentServiceTier ?? "?"}`
29452
+ `newSession \u2190 ok sessionId=${newSession.sessionId.slice(0, 8)} model=${this.currentModelId ?? "?"} models=${this.availableModels.length} tier=${tier ?? "?"}`
29419
29453
  );
29420
29454
  return {
29421
29455
  sessionId: newSession.sessionId,
29422
29456
  initialize,
29423
- model: newSessionMeta.currentModelId,
29424
- tier: newSessionMeta.currentServiceTier
29457
+ model: this.currentModelId,
29458
+ tier
29425
29459
  };
29426
29460
  } catch (err) {
29427
29461
  this.cleanupAfterFailedStart(child);
@@ -29628,8 +29662,9 @@ var AcpClient = class {
29628
29662
  }
29629
29663
  log.info("acpClient", `loadSession \u2192 sessionId=${sessionId.slice(0, 8)}`);
29630
29664
  this.opts.beginLoadReplay?.();
29665
+ let loaded;
29631
29666
  try {
29632
- await this.connection.loadSession({
29667
+ loaded = await this.connection.loadSession({
29633
29668
  sessionId,
29634
29669
  cwd: this.opts.cwd,
29635
29670
  mcpServers: this.opts.mcpServers ?? []
@@ -29638,6 +29673,8 @@ var AcpClient = class {
29638
29673
  this.opts.endLoadReplay?.();
29639
29674
  }
29640
29675
  this.sessionId = sessionId;
29676
+ if (loaded?.configOptions) this.captureModelConfig(loaded.configOptions);
29677
+ if (loaded?.modes !== void 0) this.captureModes(loaded.modes ?? null);
29641
29678
  log.info("acpClient", `loadSession \u2190 ok sessionId=${sessionId.slice(0, 8)}`);
29642
29679
  }
29643
29680
  /**
@@ -29666,24 +29703,135 @@ var AcpClient = class {
29666
29703
  }
29667
29704
  }
29668
29705
  /**
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.
29706
+ * Switch the active session's model via the NATIVE ACP
29707
+ * `session/set_config_option` RPC, targeting the `category:'model'`
29708
+ * `SessionConfigSelect` option captured on newSession/loadSession. This is
29709
+ * the single source of truth — there is no PTY `/model` fallback and no
29710
+ * non-standard `session/set_model` extension.
29711
+ *
29712
+ * Throws a clear error when the agent exposes no model config option, so
29713
+ * `change_model` acks `failed` and mobile surfaces "model picker not
29714
+ * supported on this agent".
29674
29715
  */
29675
29716
  async setModel(modelId) {
29676
29717
  if (!this.connection || !this.sessionId) {
29677
29718
  throw new Error("AcpClient.setModel called before start()");
29678
29719
  }
29679
- log.info("acpClient", `setModel \u2192 ${modelId}`);
29680
- const rawConn = this.connection;
29681
- await rawConn.connection.sendRequest("session/set_model", {
29720
+ if (!this.modelConfigId) {
29721
+ throw new Error(
29722
+ "model selection not available: this agent exposes no ACP model config option"
29723
+ );
29724
+ }
29725
+ log.info("acpClient", `setModel \u2192 ${modelId} (configId=${this.modelConfigId})`);
29726
+ await this.connection.setSessionConfigOption({
29682
29727
  sessionId: this.sessionId,
29683
- modelId
29728
+ configId: this.modelConfigId,
29729
+ value: modelId
29684
29730
  });
29731
+ this.currentModelId = modelId;
29685
29732
  log.info("acpClient", `setModel \u2190 ok modelId=${modelId}`);
29686
29733
  }
29734
+ /**
29735
+ * The models the agent natively advertises via its `category:'model'` config
29736
+ * option (mapped: id = option value, label = option name, contextWindow
29737
+ * derived from the id). Empty array when the agent has no model config option
29738
+ * — mobile then hides the model selector. NO hardcoded fallback.
29739
+ */
29740
+ getAvailableModels() {
29741
+ return this.availableModels;
29742
+ }
29743
+ /** The native model select's current value (or undefined if none advertised). */
29744
+ getCurrentModelId() {
29745
+ return this.currentModelId;
29746
+ }
29747
+ /**
29748
+ * Parse the native model config option out of a `newSession`/`loadSession`
29749
+ * `configOptions` array and populate {@link modelConfigId},
29750
+ * {@link availableModels}, and {@link currentModelId}. The list itself is
29751
+ * native (the option's `options`); `contextWindow` is a display attribute
29752
+ * derived per-id via {@link getContextWindow}. When no `category:'model'`
29753
+ * select option is present, everything resets to empty/undefined — no
29754
+ * hardcoded list, no guessed model.
29755
+ */
29756
+ captureModelConfig(configOptions) {
29757
+ const modelOption = configOptions.find(
29758
+ (o) => o.category === "model" && o.type === "select"
29759
+ );
29760
+ if (!modelOption || modelOption.type !== "select") {
29761
+ this.modelConfigId = void 0;
29762
+ this.availableModels = [];
29763
+ this.currentModelId = void 0;
29764
+ return;
29765
+ }
29766
+ this.modelConfigId = modelOption.id;
29767
+ this.currentModelId = modelOption.currentValue;
29768
+ this.availableModels = flattenSelectOptions(modelOption.options).map((opt) => ({
29769
+ id: opt.value,
29770
+ label: opt.name,
29771
+ // Only when it's a real catalog match — native ids are often opaque
29772
+ // aliases ("default"/"opus") or proxied (MiniMax house agent), for which a
29773
+ // default 200K is a fake; undefined → the UI omits the context sub-label.
29774
+ contextWindow: tryGetContextWindow(opt.value)
29775
+ }));
29776
+ }
29777
+ /**
29778
+ * Switch the active session's operating/permission MODE via the NATIVE ACP
29779
+ * `session/set_mode` RPC (the standard {@link SessionModeState} axis — DISTINCT
29780
+ * from the model config option). This is the single source of truth — there is
29781
+ * no PTY fallback.
29782
+ *
29783
+ * Throws a clear error when the agent advertises no modes, so `set_mode` acks
29784
+ * `failed` and mobile surfaces "mode switching not supported on this agent".
29785
+ */
29786
+ async setMode(modeId) {
29787
+ if (!this.connection || !this.sessionId) {
29788
+ throw new Error("AcpClient.setMode called before start()");
29789
+ }
29790
+ if (this.availableModes.length === 0) {
29791
+ throw new Error(
29792
+ "mode selection not available: this agent advertises no ACP session modes"
29793
+ );
29794
+ }
29795
+ log.info("acpClient", `setMode \u2192 ${modeId}`);
29796
+ await this.connection.setSessionMode({
29797
+ sessionId: this.sessionId,
29798
+ modeId
29799
+ });
29800
+ this.currentModeId = modeId;
29801
+ log.info("acpClient", `setMode \u2190 ok modeId=${modeId}`);
29802
+ }
29803
+ /**
29804
+ * The operating/permission modes the agent natively advertises via its ACP
29805
+ * `SessionModeState` (mapped: id = mode id, label = mode name, description
29806
+ * passthrough). Empty array when the agent advertises no modes — mobile then
29807
+ * hides the mode selector. NO hardcoded fallback.
29808
+ */
29809
+ getAvailableModes() {
29810
+ return this.availableModes;
29811
+ }
29812
+ /** The native current mode id (or undefined if the agent advertises no modes). */
29813
+ getCurrentModeId() {
29814
+ return this.currentModeId;
29815
+ }
29816
+ /**
29817
+ * Parse the native ACP {@link SessionModeState} out of a `newSession`/`loadSession`
29818
+ * response `modes` field and populate {@link availableModes} + {@link currentModeId}.
29819
+ * When absent (`null`/undefined — the agent advertises no modes) everything resets
29820
+ * to empty/undefined — no hardcoded list, no guessed mode.
29821
+ */
29822
+ captureModes(modes) {
29823
+ if (!modes) {
29824
+ this.availableModes = [];
29825
+ this.currentModeId = void 0;
29826
+ return;
29827
+ }
29828
+ this.currentModeId = modes.currentModeId;
29829
+ this.availableModes = (modes.availableModes ?? []).map((mode) => ({
29830
+ id: mode.id,
29831
+ label: mode.name,
29832
+ description: mode.description ?? void 0
29833
+ }));
29834
+ }
29687
29835
  /**
29688
29836
  * Cancel the in-flight prompt turn. Notification — no response.
29689
29837
  * Safe to call when nothing is in flight (the adapter no-ops).
@@ -29836,6 +29984,17 @@ var AcpClient = class {
29836
29984
  };
29837
29985
  }
29838
29986
  };
29987
+ function flattenSelectOptions(options) {
29988
+ const out2 = [];
29989
+ for (const entry of options) {
29990
+ if ("group" in entry) {
29991
+ out2.push(...entry.options);
29992
+ } else {
29993
+ out2.push(entry);
29994
+ }
29995
+ }
29996
+ return out2;
29997
+ }
29839
29998
  function applyLineRange(content, line, limit) {
29840
29999
  if (line === null && limit === null) return { content };
29841
30000
  const lines = content.split("\n");
@@ -31792,8 +31951,38 @@ async function getConversationH(ctx) {
31792
31951
  return;
31793
31952
  }
31794
31953
  async function listModelsH(ctx) {
31795
- const { cmd, relay, models } = ctx;
31796
- await relay.sendResult(cmd.id, "completed", { models });
31954
+ const { cmd, relay, client: client3 } = ctx;
31955
+ await relay.sendResult(cmd.id, "completed", {
31956
+ models: client3.getAvailableModels(),
31957
+ currentModelId: client3.getCurrentModelId()
31958
+ });
31959
+ return;
31960
+ }
31961
+ async function listModesH(ctx) {
31962
+ const { cmd, relay, client: client3 } = ctx;
31963
+ await relay.sendResult(cmd.id, "completed", {
31964
+ modes: client3.getAvailableModes(),
31965
+ currentModeId: client3.getCurrentModeId()
31966
+ });
31967
+ return;
31968
+ }
31969
+ async function setModeH(ctx) {
31970
+ const { cmd, client: client3, relay, opts } = ctx;
31971
+ const payload = cmd.payload;
31972
+ const modeId = payload?.modeId?.trim();
31973
+ if (!modeId) {
31974
+ await relay.sendResult(cmd.id, "failed", { error: "modeId required" });
31975
+ return;
31976
+ }
31977
+ try {
31978
+ await client3.setMode(modeId);
31979
+ await relay.sendResult(cmd.id, "completed", { modeId });
31980
+ } catch (err) {
31981
+ log.warn("acpRunner", `set_mode failed: ${describeError(err)}`);
31982
+ await relay.sendResult(cmd.id, "failed", {
31983
+ error: `Mode switching not supported on ${opts.agent} via ACP: ${describeError(err)}`
31984
+ });
31985
+ }
31797
31986
  return;
31798
31987
  }
31799
31988
  async function ackEmptyH(ctx) {
@@ -32049,6 +32238,8 @@ var ACP_COMMAND_HANDLERS = {
32049
32238
  escape_key: stopTaskH,
32050
32239
  get_conversation: getConversationH,
32051
32240
  list_models: listModelsH,
32241
+ list_modes: listModesH,
32242
+ set_mode: setModeH,
32052
32243
  set_keep_alive: ackEmptyH,
32053
32244
  get_context: ackEmptyH,
32054
32245
  select_option: selectOptionH,
@@ -32795,7 +32986,6 @@ async function runAcpSession(opts) {
32795
32986
  done: true
32796
32987
  });
32797
32988
  const runtime = createInteractiveAgentStrategy(opts.agent, createOsStrategy());
32798
- const models = await runtime.listModels();
32799
32989
  const history = new AcpHistory(publisher, {
32800
32990
  agent: opts.agent,
32801
32991
  acpSessionId,
@@ -32871,7 +33061,6 @@ async function runAcpSession(opts) {
32871
33061
  client3,
32872
33062
  relay,
32873
33063
  acpSessionId,
32874
- models,
32875
33064
  streaming,
32876
33065
  opts,
32877
33066
  history,
@@ -32927,12 +33116,11 @@ async function runAcpSession(opts) {
32927
33116
  await new Promise(() => {
32928
33117
  });
32929
33118
  }
32930
- async function handleCommand(cmd, client3, relay, acpSessionId, models, streaming, opts, history, jsonlHistory, agentCaps, turnFiles, getBeads, publisher, recentStderr, budgetRecovery, budgetReachedFlag, onActiveSessionChanged) {
33119
+ async function handleCommand(cmd, client3, relay, acpSessionId, streaming, opts, history, jsonlHistory, agentCaps, turnFiles, getBeads, publisher, recentStderr, budgetRecovery, budgetReachedFlag, onActiveSessionChanged) {
32931
33120
  const session = {
32932
33121
  client: client3,
32933
33122
  relay,
32934
33123
  acpSessionId,
32935
- models,
32936
33124
  streaming,
32937
33125
  opts,
32938
33126
  history,
@@ -34268,7 +34456,6 @@ var AcpDriver = class {
34268
34456
  async ensureSession(relay, acpSessionId, agentCaps) {
34269
34457
  if (this.session) return this.session;
34270
34458
  const { opts, publisher, streaming, runtime, recentStderr } = this.deps;
34271
- const models = await runtime.listModels();
34272
34459
  const jsonlHistory = new HistoryService(runtime, opts.pluginId, opts.cwd, {
34273
34460
  pluginAuthToken: opts.pluginAuthToken
34274
34461
  });
@@ -34308,7 +34495,6 @@ var AcpDriver = class {
34308
34495
  client: this.deps.client,
34309
34496
  relay,
34310
34497
  acpSessionId,
34311
- models,
34312
34498
  streaming,
34313
34499
  opts,
34314
34500
  history,
@@ -37633,7 +37819,7 @@ function checkChokidar() {
37633
37819
  }
37634
37820
  async function doctor(args2 = []) {
37635
37821
  const json = args2.includes("--json");
37636
- const cliVersion = true ? "2.61.17" : "0.0.0-dev";
37822
+ const cliVersion = true ? "2.61.19" : "0.0.0-dev";
37637
37823
  const apiBase2 = resolveApiBaseUrl();
37638
37824
  const diagnosticId = (0, import_node_crypto12.randomUUID)();
37639
37825
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -38144,7 +38330,7 @@ async function mcpRun(args2) {
38144
38330
  // src/commands/version.ts
38145
38331
  var import_picocolors15 = __toESM(require("picocolors"));
38146
38332
  function version2() {
38147
- const v = true ? "2.61.17" : "unknown";
38333
+ const v = true ? "2.61.19" : "unknown";
38148
38334
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
38149
38335
  }
38150
38336
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.61.17",
3
+ "version": "2.61.19",
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",