codeam-cli 2.61.7 → 2.61.9

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 +21 -0
  2. package/dist/index.js +118 -37
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ 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.8] — 2026-07-16
8
+
9
+ ### Added
10
+
11
+ - **cli:** Agent-agnostic RECENT list + auto-resume latest conversation (ACP session/list)
12
+
13
+ ### Fixed
14
+
15
+ - **cli:** Wrap public loadSession() with the load-replay guard (keystone)
16
+
17
+ ### Tests
18
+
19
+ - **cli:** Harness invariant — no unguarded connection.loadSession
20
+
21
+ ## [2.61.7] — 2026-07-16
22
+
23
+ ### Fixed
24
+
25
+ - **cli:** Auto-resume the user's session on host-agent restart (no churn)
26
+ - **cli:** Engines-compat test uses CommonJS require (was import.meta → tsc fail)
27
+
7
28
  ## [2.61.5] — 2026-07-16
8
29
 
9
30
  ### Added
package/dist/index.js CHANGED
@@ -5975,7 +5975,7 @@ function readAnonId() {
5975
5975
  }
5976
5976
  function superProperties() {
5977
5977
  return {
5978
- cliVersion: true ? "2.61.7" : "0.0.0-dev",
5978
+ cliVersion: true ? "2.61.9" : "0.0.0-dev",
5979
5979
  nodeVersion: process.version,
5980
5980
  platform: process.platform,
5981
5981
  arch: process.arch,
@@ -6156,7 +6156,7 @@ var os4 = __toESM(require("os"));
6156
6156
  // package.json
6157
6157
  var package_default = {
6158
6158
  name: "codeam-cli",
6159
- version: "2.61.7",
6159
+ version: "2.61.9",
6160
6160
  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.",
6161
6161
  type: "commonjs",
6162
6162
  main: "dist/index.js",
@@ -7299,7 +7299,7 @@ var CommandRelayService = class _CommandRelayService {
7299
7299
  // fresh + clear the "CLI update available" banner after a self-update
7300
7300
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
7301
7301
  // pair/reconnect). Older backends ignore the extra field.
7302
- ..."2.61.7" ? { ideVersion: "2.61.7" } : {}
7302
+ ..."2.61.9" ? { ideVersion: "2.61.9" } : {}
7303
7303
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
7304
7304
  }
7305
7305
  /**
@@ -17900,7 +17900,7 @@ async function autoUpgradeBeforeCriticalCommand() {
17900
17900
  if (process.env.NODE_ENV === "test") return;
17901
17901
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17902
17902
  if (process.env.CI) return;
17903
- const current = true ? "2.61.7" : null;
17903
+ const current = true ? "2.61.9" : null;
17904
17904
  if (!current) return;
17905
17905
  const cache = readCache();
17906
17906
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17917,7 +17917,7 @@ function checkForUpdates() {
17917
17917
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
17918
17918
  if (process.env.CI) return;
17919
17919
  if (!process.stdout.isTTY) return;
17920
- const current = true ? "2.61.7" : null;
17920
+ const current = true ? "2.61.9" : null;
17921
17921
  if (!current) return;
17922
17922
  const cache = readCache();
17923
17923
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -17937,7 +17937,7 @@ var SELF_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
17937
17937
  var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
17938
17938
  var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
17939
17939
  function currentCliVersion() {
17940
- return true ? "2.61.7" : null;
17940
+ return true ? "2.61.9" : null;
17941
17941
  }
17942
17942
  function runCmd(cmd, args2, timeoutMs) {
17943
17943
  return new Promise((resolve8) => {
@@ -18256,7 +18256,10 @@ var defaultSpawner = (env, cwd, args2 = []) => (0, import_node_child_process23.s
18256
18256
  });
18257
18257
  var defaultResumeSpawner = (env, cwd) => (0, import_node_child_process23.spawn)(process.execPath, [process.argv[1]], {
18258
18258
  cwd,
18259
- env: { ...process.env, ...env, CODEAM_AUTO_APPROVE: "1" },
18259
+ // CODEAM_AUTO_APPROVE=1 ACP path (baton off). CODEAM_RESUME_LATEST=1
18260
+ // continue the user's most-recent conversation instead of opening an empty
18261
+ // one (runAcpSession loads it via the ACP session/list RPC).
18262
+ env: { ...process.env, ...env, CODEAM_AUTO_APPROVE: "1", CODEAM_RESUME_LATEST: "1" },
18260
18263
  stdio: ["ignore", "pipe", "pipe"],
18261
18264
  detached: false
18262
18265
  });
@@ -22595,16 +22598,23 @@ function buildNpmInstallInvocation(opts) {
22595
22598
  path59.dirname(execPath),
22596
22599
  process.platform === "win32" ? "npm.cmd" : "npm"
22597
22600
  );
22598
- const command2 = exists2(siblingNpm) ? siblingNpm : "npm";
22599
- const args2 = prefix ? ["install", "-g", "--prefix", prefix, "codeam-cli@latest"] : ["install", "-g", "codeam-cli@latest"];
22600
- return { command: command2, args: args2 };
22601
+ const npmCommand = exists2(siblingNpm) ? siblingNpm : "npm";
22602
+ const npmArgs = prefix ? ["install", "-g", "--prefix", prefix, "codeam-cli@latest"] : ["install", "-g", "codeam-cli@latest"];
22603
+ if (opts?.sudo) {
22604
+ return { command: "sudo", args: ["-n", npmCommand, ...npmArgs] };
22605
+ }
22606
+ return { command: npmCommand, args: npmArgs };
22607
+ }
22608
+ function isPermissionError(stderr) {
22609
+ return /EACCES|permission denied|errno[\s"']*-13|operation not permitted/i.test(stderr);
22601
22610
  }
22602
22611
  async function runNpmInstallLatest() {
22603
22612
  let lastError = "";
22604
- const invocation = buildNpmInstallInvocation();
22605
- const useShell = process.platform === "win32";
22606
- const command2 = useShell && invocation.command.includes(" ") ? `"${invocation.command}"` : invocation.command;
22613
+ let useSudo = false;
22607
22614
  for (let attempt = 1; attempt <= CLI_UPDATE_MAX_ATTEMPTS; attempt++) {
22615
+ const invocation = buildNpmInstallInvocation({ sudo: useSudo });
22616
+ const useShell = process.platform === "win32";
22617
+ const command2 = useShell && invocation.command.includes(" ") ? `"${invocation.command}"` : invocation.command;
22608
22618
  const result = await new Promise((resolve8) => {
22609
22619
  (0, import_child_process24.execFile)(
22610
22620
  command2,
@@ -22621,6 +22631,10 @@ async function runNpmInstallLatest() {
22621
22631
  });
22622
22632
  if (result.ok) return { ok: true };
22623
22633
  lastError = result.error ?? "unknown";
22634
+ if (!useSudo && process.platform !== "win32" && isPermissionError(lastError)) {
22635
+ useSudo = true;
22636
+ continue;
22637
+ }
22624
22638
  if (attempt < CLI_UPDATE_MAX_ATTEMPTS) {
22625
22639
  await new Promise((r) => setTimeout(r, 1e3 * attempt));
22626
22640
  }
@@ -29068,6 +29082,7 @@ var AcpClient = class {
29068
29082
  * the conversation — whereas one without it falls back to a fresh
29069
29083
  * `session/new`. Set once during {@link startOnce}. */
29070
29084
  supportsLoadSession = false;
29085
+ supportsListSessions = false;
29071
29086
  /** Idle watchdog for the in-flight prompt. The `Client` handlers
29072
29087
  * (`sessionUpdate` / `requestPermission`) reach for this to keep
29073
29088
  * the turn alive while the adapter is demonstrably working. Null
@@ -29252,6 +29267,7 @@ var AcpClient = class {
29252
29267
  `initialize \u2190 ok protocolVersion=${initialize.protocolVersion} agentCaps=${JSON.stringify(initialize.agentCapabilities ?? {}).slice(0, 200)}`
29253
29268
  );
29254
29269
  this.supportsLoadSession = initialize.agentCapabilities?.loadSession === true;
29270
+ this.supportsListSessions = !!initialize.agentCapabilities?.sessionCapabilities?.list;
29255
29271
  log.info("acpClient", "newSession \u2192 sending");
29256
29272
  let startupTimer;
29257
29273
  const startupFailure = new Promise((_2, reject) => {
@@ -29495,14 +29511,44 @@ var AcpClient = class {
29495
29511
  return;
29496
29512
  }
29497
29513
  log.info("acpClient", `loadSession \u2192 sessionId=${sessionId.slice(0, 8)}`);
29498
- await this.connection.loadSession({
29499
- sessionId,
29500
- cwd: this.opts.cwd,
29501
- mcpServers: this.opts.mcpServers ?? []
29502
- });
29514
+ this.opts.beginLoadReplay?.();
29515
+ try {
29516
+ await this.connection.loadSession({
29517
+ sessionId,
29518
+ cwd: this.opts.cwd,
29519
+ mcpServers: this.opts.mcpServers ?? []
29520
+ });
29521
+ } finally {
29522
+ this.opts.endLoadReplay?.();
29523
+ }
29503
29524
  this.sessionId = sessionId;
29504
29525
  log.info("acpClient", `loadSession \u2190 ok sessionId=${sessionId.slice(0, 8)}`);
29505
29526
  }
29527
+ /**
29528
+ * Enumerate the workspace's conversations via the ACP `session/list` RPC.
29529
+ * Agent-agnostic — works for ANY adapter that advertises
29530
+ * `sessionCapabilities.list` (claude/codex/gemini alike), so the mobile RECENT
29531
+ * list no longer depends on per-agent JSONL scanning. Maps SessionInfo (id +
29532
+ * agent-authored title + updatedAt) to the backend's RECENT-list shape.
29533
+ *
29534
+ * Returns null when the agent doesn't advertise `list` (caller falls back or
29535
+ * skips). Best-effort — never throws out to the caller.
29536
+ */
29537
+ async listSessions() {
29538
+ if (!this.connection || !this.supportsListSessions) return null;
29539
+ try {
29540
+ const res = await this.connection.listSessions({ cwd: this.opts.cwd });
29541
+ return (res.sessions ?? []).map((s) => ({
29542
+ id: s.sessionId,
29543
+ summary: s.title ?? "",
29544
+ // updatedAt is an ISO string; fall back to now on absent/unparseable.
29545
+ timestamp: s.updatedAt ? Date.parse(s.updatedAt) || Date.now() : Date.now()
29546
+ }));
29547
+ } catch (err) {
29548
+ log.trace("acpClient", "listSessions failed (best-effort)", err);
29549
+ return null;
29550
+ }
29551
+ }
29506
29552
  /**
29507
29553
  * Switch the active session's model via the non-standard
29508
29554
  * `session/set_model` RPC. The standard ACP SDK doesn't expose
@@ -32330,17 +32376,17 @@ var AcpHistory = class {
32330
32376
  async flush() {
32331
32377
  if (this.summary === null || this.messages.length === 0) return;
32332
32378
  const timestamp = Date.now();
32379
+ const current = { id: this.opts.acpSessionId, summary: this.summary, timestamp };
32380
+ let sessions3 = [current];
32381
+ const listed = this.opts.listSessions ? await this.opts.listSessions() : null;
32382
+ if (listed && listed.length > 0) {
32383
+ sessions3 = listed.map(
32384
+ (s) => s.id === this.opts.acpSessionId && !s.summary ? { ...s, summary: this.summary } : s
32385
+ );
32386
+ if (!sessions3.some((s) => s.id === this.opts.acpSessionId)) sessions3.unshift(current);
32387
+ }
32333
32388
  await Promise.all([
32334
- this.publisher.pushSessionList({
32335
- agentId: this.opts.agent,
32336
- sessions: [
32337
- {
32338
- id: this.opts.acpSessionId,
32339
- summary: this.summary,
32340
- timestamp
32341
- }
32342
- ]
32343
- }),
32389
+ this.publisher.pushSessionList({ agentId: this.opts.agent, sessions: sessions3 }),
32344
32390
  this.publisher.pushConversation({
32345
32391
  agentId: this.opts.agent,
32346
32392
  sessionId: this.opts.acpSessionId,
@@ -32386,6 +32432,10 @@ function computeAdapterExtraEnv(params) {
32386
32432
  );
32387
32433
  return env;
32388
32434
  }
32435
+ function pickLatestResumableConversation(sessions3, currentId) {
32436
+ const prior = (sessions3 ?? []).filter((s) => s.id !== currentId).sort((a, b) => b.timestamp - a.timestamp)[0];
32437
+ return prior?.id ?? null;
32438
+ }
32389
32439
  async function runAcpSession(opts) {
32390
32440
  const publisher = new AcpPublisher({
32391
32441
  sessionId: opts.sessionId,
@@ -32534,16 +32584,24 @@ async function runAcpSession(opts) {
32534
32584
  });
32535
32585
  return;
32536
32586
  }
32537
- const {
32538
- sessionId: acpSessionId,
32539
- initialize,
32540
- model: handshakeModel,
32541
- tier: handshakeTier
32542
- } = handshake;
32587
+ let acpSessionId = handshake.sessionId;
32588
+ const { initialize, model: handshakeModel, tier: handshakeTier } = handshake;
32543
32589
  log.trace(
32544
32590
  "acpRunner",
32545
32591
  `adapter handshake ok protocolVersion=${initialize.protocolVersion} sessionId=${acpSessionId.slice(0, 8)}`
32546
32592
  );
32593
+ if (process.env.CODEAM_RESUME_LATEST === "1") {
32594
+ try {
32595
+ const priorId = pickLatestResumableConversation(await client3.listSessions(), acpSessionId);
32596
+ if (priorId) {
32597
+ await client3.loadSession(priorId);
32598
+ acpSessionId = priorId;
32599
+ log.info("acpRunner", `auto-resumed latest conversation ${priorId.slice(0, 8)}`);
32600
+ }
32601
+ } catch (err) {
32602
+ log.trace("acpRunner", "auto-resume-latest failed (best-effort)", err);
32603
+ }
32604
+ }
32547
32605
  showSuccess(`${opts.agent} online (ACP) \u2014 awaiting prompts from mobile.`);
32548
32606
  showRelayNotice();
32549
32607
  void publisher.publishOutput({
@@ -32565,10 +32623,33 @@ async function runAcpSession(opts) {
32565
32623
  });
32566
32624
  const runtime = createInteractiveAgentStrategy(opts.agent, createOsStrategy());
32567
32625
  const models = await runtime.listModels();
32568
- const history = new AcpHistory(publisher, { agent: opts.agent, acpSessionId });
32626
+ const history = new AcpHistory(publisher, {
32627
+ agent: opts.agent,
32628
+ acpSessionId,
32629
+ // Agent-agnostic RECENT list: enumerate via the ACP session/list RPC (any
32630
+ // adapter advertising sessionCapabilities.list). Returns null on unsupported
32631
+ // agents → flush() falls back to the current session only.
32632
+ listSessions: () => client3.listSessions()
32633
+ });
32569
32634
  const jsonlHistory = new HistoryService(runtime, opts.pluginId, opts.cwd, {
32570
32635
  pluginAuthToken: opts.pluginAuthToken
32571
32636
  });
32637
+ void (async () => {
32638
+ try {
32639
+ const listed = await client3.listSessions();
32640
+ if (listed && listed.length > 0) {
32641
+ await publisher.pushSessionList({ agentId: opts.agent, sessions: listed });
32642
+ log.info("acpRunner", `session-start: pushed ${listed.length} conversation(s) to RECENT`);
32643
+ }
32644
+ const uploaded = await jsonlHistory.uploadConversationIfChanged(acpSessionId);
32645
+ log.info(
32646
+ "acpRunner",
32647
+ `session-start: transcript upload ${acpSessionId.slice(0, 8)} \u2192 ${uploaded}`
32648
+ );
32649
+ } catch (err) {
32650
+ log.warn("acpRunner", `session-start push failed (best-effort): ${describeError(err)}`);
32651
+ }
32652
+ })();
32572
32653
  const onboardingWelcomeDone = maybeSendOnboardingWelcome({
32573
32654
  streaming,
32574
32655
  history,
@@ -37340,7 +37421,7 @@ function checkChokidar() {
37340
37421
  }
37341
37422
  async function doctor(args2 = []) {
37342
37423
  const json = args2.includes("--json");
37343
- const cliVersion = true ? "2.61.7" : "0.0.0-dev";
37424
+ const cliVersion = true ? "2.61.9" : "0.0.0-dev";
37344
37425
  const apiBase2 = resolveApiBaseUrl();
37345
37426
  const diagnosticId = (0, import_node_crypto12.randomUUID)();
37346
37427
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -37851,7 +37932,7 @@ async function mcpRun(args2) {
37851
37932
  // src/commands/version.ts
37852
37933
  var import_picocolors15 = __toESM(require("picocolors"));
37853
37934
  function version2() {
37854
- const v = true ? "2.61.7" : "unknown";
37935
+ const v = true ? "2.61.9" : "unknown";
37855
37936
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
37856
37937
  }
37857
37938
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.61.7",
3
+ "version": "2.61.9",
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",