codeam-cli 2.60.50 → 2.60.52

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 +135 -10
  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.60.51] — 2026-07-12
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Recover kimi ACP 'Session is closed' by re-establishing the session + retry (multi-turn)
12
+
13
+ ### Tests
14
+
15
+ - **cli:** Reusable ACP-provision smoke harness (per-agent real handshake auth check; automates CLAUDE.md Step 8)
16
+
17
+ ## [2.60.50] — 2026-07-11
18
+
19
+ ### Fixed
20
+
21
+ - **cli:** Guard kimi ACP login-state slot + real `kimi acp` regression test
22
+
7
23
  ## [2.60.49] — 2026-07-11
8
24
 
9
25
  ### Fixed
package/dist/index.js CHANGED
@@ -478,13 +478,27 @@ function headroomKindFor(agentId) {
478
478
  // ../../packages/shared/src/integrations/registry.ts
479
479
  var INTEGRATION_REGISTRY = {
480
480
  jira: {
481
+ // Registry id kept 'jira' for id-stability (existing LinkedIntegration rows
482
+ // stay valid — zero data migration); only the DISPLAY is "Atlassian". The
483
+ // one mcp-atlassian server serves BOTH Jira and Confluence, so the same
484
+ // entry now requests Confluence scopes too.
481
485
  id: "jira",
482
- name: "Jira",
486
+ name: "Atlassian",
483
487
  icon: "jira",
484
488
  enabled: true,
485
489
  auth: {
486
490
  kind: "oauth_redirect",
487
- scopes: ["read:jira-work", "write:jira-work", "offline_access"]
491
+ // Jira + Confluence 3LO granular scopes. The Confluence pair
492
+ // (read:confluence-content.all / write:confluence-content) is the set
493
+ // mcp-atlassian's own Authentication docs recommend for full read+write
494
+ // Confluence (matches its documented env scope string).
495
+ scopes: [
496
+ "read:jira-work",
497
+ "write:jira-work",
498
+ "read:confluence-content.all",
499
+ "write:confluence-content",
500
+ "offline_access"
501
+ ]
488
502
  },
489
503
  delivery: {
490
504
  mcp: {
@@ -5733,7 +5747,7 @@ function readAnonId() {
5733
5747
  }
5734
5748
  function superProperties() {
5735
5749
  return {
5736
- cliVersion: true ? "2.60.50" : "0.0.0-dev",
5750
+ cliVersion: true ? "2.60.52" : "0.0.0-dev",
5737
5751
  nodeVersion: process.version,
5738
5752
  platform: process.platform,
5739
5753
  arch: process.arch,
@@ -5914,7 +5928,7 @@ var os4 = __toESM(require("os"));
5914
5928
  // package.json
5915
5929
  var package_default = {
5916
5930
  name: "codeam-cli",
5917
- version: "2.60.50",
5931
+ version: "2.60.52",
5918
5932
  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.",
5919
5933
  type: "commonjs",
5920
5934
  main: "dist/index.js",
@@ -7023,7 +7037,7 @@ var CommandRelayService = class {
7023
7037
  // fresh + clear the "CLI update available" banner after a self-update
7024
7038
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
7025
7039
  // pair/reconnect). Older backends ignore the extra field.
7026
- ..."2.60.50" ? { ideVersion: "2.60.50" } : {}
7040
+ ..."2.60.52" ? { ideVersion: "2.60.52" } : {}
7027
7041
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
7028
7042
  }
7029
7043
  /**
@@ -17592,7 +17606,7 @@ async function autoUpgradeBeforeCriticalCommand() {
17592
17606
  if (process.env.NODE_ENV === "test") return;
17593
17607
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17594
17608
  if (process.env.CI) return;
17595
- const current = true ? "2.60.50" : null;
17609
+ const current = true ? "2.60.52" : null;
17596
17610
  if (!current) return;
17597
17611
  const cache = readCache();
17598
17612
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17609,7 +17623,7 @@ function checkForUpdates() {
17609
17623
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17610
17624
  if (process.env.CI) return;
17611
17625
  if (!process.stdout.isTTY) return;
17612
- const current = true ? "2.60.50" : null;
17626
+ const current = true ? "2.60.52" : null;
17613
17627
  if (!current) return;
17614
17628
  const cache = readCache();
17615
17629
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17629,7 +17643,7 @@ var SELF_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
17629
17643
  var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
17630
17644
  var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
17631
17645
  function currentCliVersion() {
17632
- return true ? "2.60.50" : null;
17646
+ return true ? "2.60.52" : null;
17633
17647
  }
17634
17648
  function runCmd(cmd, args2, timeoutMs) {
17635
17649
  return new Promise((resolve7) => {
@@ -26736,6 +26750,7 @@ function createIdleTimeout(idleMs, makeError) {
26736
26750
  var TRANSIENT_ADAPTER_SPAWN_RE = /ETXTBSY|ENOENT/;
26737
26751
  var MAX_START_ATTEMPTS = 5;
26738
26752
  var TRANSPORT_DOWN_DURING_START_RE = /connection (is )?closed|stream (is )?closed|ECONNRESET|EPIPE|write after end|premature close/i;
26753
+ var SESSION_CLOSED_RE = /session is closed/i;
26739
26754
  var AdapterStartCrashError = class extends Error {
26740
26755
  adapterExitedDuringStart = true;
26741
26756
  constructor(message) {
@@ -26763,6 +26778,12 @@ var AcpClient = class {
26763
26778
  connection = null;
26764
26779
  stopping = false;
26765
26780
  sessionId = null;
26781
+ /** Whether the agent advertised `loadSession` on `initialize`. Drives the
26782
+ * post-turn session-closed recovery: an agent that supports resume (kimi,
26783
+ * claude, codex, gemini) is re-established with `session/load` — preserving
26784
+ * the conversation — whereas one without it falls back to a fresh
26785
+ * `session/new`. Set once during {@link startOnce}. */
26786
+ supportsLoadSession = false;
26766
26787
  /** Idle watchdog for the in-flight prompt. The `Client` handlers
26767
26788
  * (`sessionUpdate` / `requestPermission`) reach for this to keep
26768
26789
  * the turn alive while the adapter is demonstrably working. Null
@@ -26946,6 +26967,7 @@ var AcpClient = class {
26946
26967
  "acpClient",
26947
26968
  `initialize \u2190 ok protocolVersion=${initialize.protocolVersion} agentCaps=${JSON.stringify(initialize.agentCapabilities ?? {}).slice(0, 200)}`
26948
26969
  );
26970
+ this.supportsLoadSession = initialize.agentCapabilities?.loadSession === true;
26949
26971
  log.info("acpClient", "newSession \u2192 sending");
26950
26972
  let startupTimer;
26951
26973
  const startupFailure = new Promise((_2, reject) => {
@@ -27033,6 +27055,29 @@ var AcpClient = class {
27033
27055
  throw new Error("AcpClient.prompt called before start()");
27034
27056
  }
27035
27057
  const blocks = typeof input === "string" ? [{ type: "text", text: input }] : input;
27058
+ try {
27059
+ return await this.sendPromptOnce(blocks);
27060
+ } catch (err) {
27061
+ if (this.isSessionClosedError(err)) {
27062
+ log.warn(
27063
+ "acpClient",
27064
+ "session/prompt rejected 'Session is closed' \u2014 re-establishing session and retrying once"
27065
+ );
27066
+ await this.reestablishSession();
27067
+ return await this.sendPromptOnce(blocks);
27068
+ }
27069
+ throw err;
27070
+ }
27071
+ }
27072
+ /**
27073
+ * Send ONE `session/prompt` round-trip with the idle watchdog. Extracted from
27074
+ * {@link runPrompt} so the closed-session recovery can re-invoke it for the
27075
+ * single retry without duplicating the watchdog / tool-ledger setup.
27076
+ */
27077
+ async sendPromptOnce(blocks) {
27078
+ if (!this.connection || !this.sessionId) {
27079
+ throw new Error("AcpClient.sendPromptOnce called before start()");
27080
+ }
27036
27081
  const textLen = blocks.reduce(
27037
27082
  (n, b) => b.type === "text" ? n + b.text.length : n,
27038
27083
  0
@@ -27078,6 +27123,71 @@ var AcpClient = class {
27078
27123
  this.promptIdle = null;
27079
27124
  }
27080
27125
  }
27126
+ /**
27127
+ * True when a `session/prompt` rejection is the "the agent closed its ACP
27128
+ * session between turns" fact — the kimi post-turn/idle close. Keyed on the
27129
+ * `session is closed` phrase (in `message` / `details` / serialised `data`),
27130
+ * NOT on the bare `-32603` code: that generic "Internal error" code is also
27131
+ * used for the adapter's own transient spawn races, so matching it alone would
27132
+ * misfire. See {@link SESSION_CLOSED_RE}.
27133
+ */
27134
+ isSessionClosedError(err) {
27135
+ if (!err || typeof err !== "object") return false;
27136
+ const e = err;
27137
+ const parts = [];
27138
+ if (typeof e.message === "string") parts.push(e.message);
27139
+ if (typeof e.details === "string") parts.push(e.details);
27140
+ if (e.data !== void 0) {
27141
+ try {
27142
+ parts.push(JSON.stringify(e.data));
27143
+ } catch {
27144
+ }
27145
+ }
27146
+ return SESSION_CLOSED_RE.test(parts.join(" "));
27147
+ }
27148
+ /**
27149
+ * Re-establish a session the agent closed out from under us (kimi post-turn),
27150
+ * on the SAME still-running adapter process, so the pending prompt can be
27151
+ * retried. Prefers `session/load` (resume) when the agent advertised
27152
+ * `loadSession` — proven live to re-open kimi's closed session with the
27153
+ * conversation context intact. Calls `connection.loadSession` DIRECTLY — the
27154
+ * public {@link loadSession} skips a same-id load (a Claude relaunch-wedge
27155
+ * guard), which is exactly the load we MUST perform here (the closed id IS the
27156
+ * active id). Falls back to a fresh `session/new` (context lost) only when the
27157
+ * agent can't resume.
27158
+ *
27159
+ * ⚠️ kimi's `session/load` (0.23.6, live-verified) REPLAYS the ENTIRE prior
27160
+ * conversation as `session/update` notifications BEFORE it resolves — the
27161
+ * earlier assumption that it emitted no replay was wrong and caused a P0: the
27162
+ * replayed prior-turn text was published as live chunks and prepended to the
27163
+ * recovered turn's real reply. So the load is bracketed with
27164
+ * `beginLoadReplay()/endLoadReplay()` (wired by the runner to StreamingState)
27165
+ * so those replay updates are swallowed, exactly like the baton's load. The
27166
+ * retried `session/prompt` (after the load resolves) then streams the REAL new
27167
+ * reply cleanly. The `finally` guarantees the guard clears even if the load
27168
+ * throws, so a failed recovery can't wedge streaming off.
27169
+ */
27170
+ async reestablishSession() {
27171
+ if (!this.connection) throw new Error("AcpClient.reestablishSession: no connection");
27172
+ const cwd = this.opts.cwd;
27173
+ const mcpServers = this.opts.mcpServers ?? [];
27174
+ const closedSid = this.sessionId;
27175
+ if (this.supportsLoadSession && closedSid) {
27176
+ log.info("acpClient", `reestablish \u2192 loadSession(resume) sid=${closedSid.slice(0, 8)}`);
27177
+ this.opts.beginLoadReplay?.();
27178
+ try {
27179
+ await this.connection.loadSession({ sessionId: closedSid, cwd, mcpServers });
27180
+ } finally {
27181
+ this.opts.endLoadReplay?.();
27182
+ }
27183
+ log.info("acpClient", "reestablish \u2190 loadSession ok");
27184
+ return;
27185
+ }
27186
+ log.info("acpClient", "reestablish \u2192 newSession (agent has no loadSession capability)");
27187
+ const ns = await this.connection.newSession({ cwd, mcpServers });
27188
+ this.sessionId = ns.sessionId;
27189
+ log.info("acpClient", `reestablish \u2190 newSession ok sid=${ns.sessionId.slice(0, 8)}`);
27190
+ }
27081
27191
  /**
27082
27192
  * Load a previously persisted session by id. Mobile's
27083
27193
  * `resume_session` command flows through here. Requires the
@@ -29966,6 +30076,13 @@ async function runAcpSession(opts) {
29966
30076
  streaming.append(delta);
29967
30077
  }
29968
30078
  },
30079
+ // kimi post-turn "Session is closed" recovery brackets its `session/load`
30080
+ // with these so the conversation history the load REPLAYS as session/update
30081
+ // notifications is swallowed by StreamingState instead of published as live
30082
+ // chunks (which prepended a prior turn's text to the recovered reply — P0).
30083
+ // Same guard the baton uses; the happy path never calls loadSession.
30084
+ beginLoadReplay: () => streaming.beginLoadReplay(),
30085
+ endLoadReplay: () => streaming.endLoadReplay(),
29969
30086
  onRequestPermission: async (request) => {
29970
30087
  if (opts.autoApprovePermissions) {
29971
30088
  const allow = pickAllowOption(request.options);
@@ -31763,6 +31880,14 @@ async function runBatonSession(opts) {
31763
31880
  onSessionUpdate: (notification) => {
31764
31881
  for (const delta of mapSessionUpdate(notification)) streaming.append(delta);
31765
31882
  },
31883
+ // Swallow the ACP session/load history replay during the kimi "Session is
31884
+ // closed" recovery (AcpClient.reestablishSession) — kimi 0.23.6 replays the
31885
+ // whole conversation as session/update, which would otherwise prepend a
31886
+ // prior turn's text to the recovered reply on the baton's mobile stream too
31887
+ // (same leak the plain ACP runner wires at runner.ts). Same `streaming`
31888
+ // instance the baton's own AcpDriver.start bracketing uses.
31889
+ beginLoadReplay: () => streaming.beginLoadReplay(),
31890
+ endLoadReplay: () => streaming.endLoadReplay(),
31766
31891
  onRequestPermission: async (request) => {
31767
31892
  const { event, optionIdByLabel } = mapPermissionRequest(request);
31768
31893
  await publisher.publishAwaitingAnswer(event);
@@ -34846,7 +34971,7 @@ function checkChokidar() {
34846
34971
  }
34847
34972
  async function doctor(args2 = []) {
34848
34973
  const json = args2.includes("--json");
34849
- const cliVersion = true ? "2.60.50" : "0.0.0-dev";
34974
+ const cliVersion = true ? "2.60.52" : "0.0.0-dev";
34850
34975
  const apiBase2 = resolveApiBaseUrl();
34851
34976
  const diagnosticId = (0, import_node_crypto12.randomUUID)();
34852
34977
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -35323,7 +35448,7 @@ async function mcpRun(args2) {
35323
35448
  // src/commands/version.ts
35324
35449
  var import_picocolors15 = __toESM(require("picocolors"));
35325
35450
  function version2() {
35326
- const v = true ? "2.60.50" : "unknown";
35451
+ const v = true ? "2.60.52" : "unknown";
35327
35452
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
35328
35453
  }
35329
35454
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.60.50",
3
+ "version": "2.60.52",
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",