codeam-cli 2.65.11 → 2.65.12

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 +36 -0
  2. package/dist/index.js +250 -20
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,42 @@ 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.65.11] — 2026-08-19
8
+
9
+ ### Documentation
10
+
11
+ - **cli:** Record the 2026-08-18 baton hand-off + goodbye-heartbeat invariants
12
+
13
+ ### Fixed
14
+
15
+ - **cli:** Take-control never wedges on "Switching…" again
16
+ - **cli:** Say goodbye before exiting so mobile stops showing ONLINE
17
+ - **cli:** LOCAL_DRIVE mirrors the transcript only — no raw TUI bytes to mobile
18
+
19
+ ### Tests
20
+
21
+ - **cli:** Real baton local integration test (take-control before first turn, handback, goodbye heartbeat)
22
+
23
+ ## [2.65.10] — 2026-08-19
24
+
25
+ ### Fixed
26
+
27
+ - **cli:** Codespace sessions report the user's repo, not the shared wrapper hostname
28
+ - **cli:** Dedupe the native ACP model list and validate model/tier at the wire boundary
29
+ - **cli:** Welcome card greets first-time users correctly and never shows a raw internal path
30
+
31
+ ## [2.65.9] — 2026-08-19
32
+
33
+ ### Fixed
34
+
35
+ - **cli:** Report an ineligible-tier credential to the backend on startup failure
36
+
37
+ ## [2.65.8] — 2026-08-18
38
+
39
+ ### Fixed
40
+
41
+ - **cli:** Baton re-affirms its state on the relay heartbeat so the 1h backend snapshot never expires under a live session
42
+
7
43
  ## [2.65.7] — 2026-08-15
8
44
 
9
45
  ### Fixed
package/dist/index.js CHANGED
@@ -8079,7 +8079,7 @@ function readAnonId() {
8079
8079
  }
8080
8080
  function superProperties() {
8081
8081
  return {
8082
- cliVersion: true ? "2.65.11" : "0.0.0-dev",
8082
+ cliVersion: true ? "2.65.12" : "0.0.0-dev",
8083
8083
  nodeVersion: process.version,
8084
8084
  platform: process.platform,
8085
8085
  arch: process.arch,
@@ -8321,7 +8321,7 @@ var http = __toESM(require("http"));
8321
8321
  // package.json
8322
8322
  var package_default = {
8323
8323
  name: "codeam-cli",
8324
- version: "2.65.11",
8324
+ version: "2.65.12",
8325
8325
  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.",
8326
8326
  type: "commonjs",
8327
8327
  main: "dist/index.js",
@@ -9842,7 +9842,7 @@ var CommandRelayService = class _CommandRelayService {
9842
9842
  // fresh + clear the "CLI update available" banner after a self-update
9843
9843
  // (a codespace that reinstalls @latest reconnects via heartbeat, not
9844
9844
  // pair/reconnect). Older backends ignore the extra field.
9845
- ..."2.65.11" ? { ideVersion: "2.65.11" } : {}
9845
+ ..."2.65.12" ? { ideVersion: "2.65.12" } : {}
9846
9846
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
9847
9847
  }
9848
9848
  /**
@@ -15283,6 +15283,7 @@ function parseHistoryFile(filePath) {
15283
15283
  if (!msg) continue;
15284
15284
  const text = extractText(msg["content"]).trim();
15285
15285
  if (!text) continue;
15286
+ if (type === "user" && isLocalCommandEcho(text)) continue;
15286
15287
  const ts = r["timestamp"];
15287
15288
  const timestamp = typeof ts === "string" ? ts : typeof ts === "number" ? new Date(ts).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
15288
15289
  const uuid = typeof r["uuid"] === "string" ? r["uuid"] : `${Date.now()}-${Math.random()}`;
@@ -15347,6 +15348,175 @@ function listResumableSessions(cwd) {
15347
15348
  out2.sort((a, b) => b.timestamp - a.timestamp);
15348
15349
  return out2;
15349
15350
  }
15351
+ function isLocalCommandEcho(text) {
15352
+ return /^\s*<(command-name|local-command-[a-z]+)>/.test(text);
15353
+ }
15354
+ var CLEAR_COMMAND_ECHO = "<command-name>/clear</command-name>";
15355
+ var SWITCH_PROBE_BYTES = 256 * 1024;
15356
+ function isClearedConversationFile(filePath) {
15357
+ let fd;
15358
+ try {
15359
+ fd = fs17.openSync(filePath, "r");
15360
+ } catch {
15361
+ return false;
15362
+ }
15363
+ let raw;
15364
+ try {
15365
+ const buf = Buffer.alloc(SWITCH_PROBE_BYTES);
15366
+ const n = fs17.readSync(fd, buf, 0, SWITCH_PROBE_BYTES, 0);
15367
+ raw = buf.toString("utf8", 0, n);
15368
+ } catch {
15369
+ return false;
15370
+ } finally {
15371
+ fs17.closeSync(fd);
15372
+ }
15373
+ if (!raw.includes(CLEAR_COMMAND_ECHO) && !raw.includes("SessionStart:clear")) return false;
15374
+ for (const line of raw.split("\n")) {
15375
+ if (!line.trim()) continue;
15376
+ let r;
15377
+ try {
15378
+ r = JSON.parse(line);
15379
+ } catch {
15380
+ continue;
15381
+ }
15382
+ if (r["type"] === "user" && !r["isMeta"]) {
15383
+ const msg = r["message"];
15384
+ if (extractText(msg?.["content"]).trimStart().startsWith(CLEAR_COMMAND_ECHO)) return true;
15385
+ }
15386
+ if (r["type"] === "attachment") {
15387
+ const att = r["attachment"];
15388
+ if (typeof att?.["hookName"] === "string" && att["hookName"].startsWith("SessionStart:clear")) {
15389
+ return true;
15390
+ }
15391
+ }
15392
+ }
15393
+ return false;
15394
+ }
15395
+ function appendedTailHasResumeMarker(filePath, fromOffset) {
15396
+ let fd;
15397
+ try {
15398
+ fd = fs17.openSync(filePath, "r");
15399
+ } catch {
15400
+ return false;
15401
+ }
15402
+ try {
15403
+ const size = fs17.fstatSync(fd).size;
15404
+ const len = Math.min(Math.max(size - fromOffset, 0), SWITCH_PROBE_BYTES);
15405
+ if (len <= 0) return false;
15406
+ const buf = Buffer.alloc(len);
15407
+ const n = fs17.readSync(fd, buf, 0, len, fromOffset);
15408
+ const raw = buf.toString("utf8", 0, n);
15409
+ if (!raw.includes('"last-prompt"')) return false;
15410
+ for (const line of raw.split("\n")) {
15411
+ if (!line.trim()) continue;
15412
+ try {
15413
+ if (JSON.parse(line)["type"] === "last-prompt") return true;
15414
+ } catch {
15415
+ }
15416
+ }
15417
+ return false;
15418
+ } catch {
15419
+ return false;
15420
+ } finally {
15421
+ fs17.closeSync(fd);
15422
+ }
15423
+ }
15424
+ function watchConversationSwitch(cwd, opts, onSwitch, projectsRoot) {
15425
+ const root = projectsRoot ?? path21.join(os15.homedir(), ".claude", "projects");
15426
+ let currentId = opts.currentId;
15427
+ const baseline = /* @__PURE__ */ new Map();
15428
+ const unclassified = /* @__PURE__ */ new Map();
15429
+ let dirWatcher = null;
15430
+ let rootWatcher = null;
15431
+ let closed = false;
15432
+ const fileSize = (file) => {
15433
+ try {
15434
+ return fs17.statSync(file).size;
15435
+ } catch {
15436
+ return null;
15437
+ }
15438
+ };
15439
+ const switchTo = (dir, id, kind) => {
15440
+ const leaving = fileSize(path21.join(dir, `${currentId}.jsonl`));
15441
+ if (leaving !== null) baseline.set(currentId, leaving);
15442
+ baseline.delete(id);
15443
+ unclassified.delete(id);
15444
+ currentId = id;
15445
+ onSwitch(id, { kind });
15446
+ };
15447
+ const scan = (dir) => {
15448
+ if (closed) return;
15449
+ let entries;
15450
+ try {
15451
+ entries = fs17.readdirSync(dir);
15452
+ } catch {
15453
+ return;
15454
+ }
15455
+ for (const name of entries) {
15456
+ if (!name.endsWith(".jsonl")) continue;
15457
+ const id = name.slice(0, -".jsonl".length);
15458
+ if (id === currentId) continue;
15459
+ const file = path21.join(dir, name);
15460
+ const size = fileSize(file);
15461
+ if (size === null) continue;
15462
+ const known = baseline.get(id);
15463
+ if (known !== void 0) {
15464
+ if (size > known && appendedTailHasResumeMarker(file, known)) {
15465
+ switchTo(dir, id, "resumed");
15466
+ return;
15467
+ }
15468
+ continue;
15469
+ }
15470
+ if (unclassified.get(id) === size) continue;
15471
+ unclassified.set(id, size);
15472
+ if (isClearedConversationFile(file)) {
15473
+ switchTo(dir, id, "new");
15474
+ return;
15475
+ }
15476
+ }
15477
+ };
15478
+ const attachDir = (initial) => {
15479
+ const dir = resolveHistoryDir(cwd, root);
15480
+ if (!dir) return false;
15481
+ rootWatcher?.close();
15482
+ rootWatcher = null;
15483
+ if (initial) {
15484
+ try {
15485
+ for (const name of fs17.readdirSync(dir)) {
15486
+ if (!name.endsWith(".jsonl")) continue;
15487
+ const id = name.slice(0, -".jsonl".length);
15488
+ if (id === currentId) continue;
15489
+ const size = fileSize(path21.join(dir, name));
15490
+ if (size !== null) baseline.set(id, size);
15491
+ }
15492
+ } catch {
15493
+ }
15494
+ }
15495
+ try {
15496
+ dirWatcher = fs17.watch(dir, { persistent: false }, () => scan(dir));
15497
+ } catch {
15498
+ return false;
15499
+ }
15500
+ scan(dir);
15501
+ return true;
15502
+ };
15503
+ if (!attachDir(true)) {
15504
+ try {
15505
+ fs17.mkdirSync(root, { recursive: true });
15506
+ rootWatcher = fs17.watch(root, { persistent: false }, () => {
15507
+ if (!closed && !dirWatcher) attachDir(false);
15508
+ });
15509
+ } catch {
15510
+ }
15511
+ }
15512
+ return () => {
15513
+ closed = true;
15514
+ dirWatcher?.close();
15515
+ rootWatcher?.close();
15516
+ dirWatcher = null;
15517
+ rootWatcher = null;
15518
+ };
15519
+ }
15350
15520
 
15351
15521
  // src/agents/claude/runtime.ts
15352
15522
  var ClaudeRuntimeStrategy = class {
@@ -15436,6 +15606,14 @@ var ClaudeRuntimeStrategy = class {
15436
15606
  parseHistoryFile(filePath) {
15437
15607
  return parseHistoryFile(filePath);
15438
15608
  }
15609
+ /**
15610
+ * Baton: follow the native TUI through `/clear` (new conversation id, new
15611
+ * JSONL) and `/resume` (an existing one) — see
15612
+ * {@link history.watchConversationSwitch}.
15613
+ */
15614
+ watchConversationSwitch(cwd, opts, onSwitch) {
15615
+ return watchConversationSwitch(cwd, opts, onSwitch);
15616
+ }
15439
15617
  getCurrentUsage(historyDir) {
15440
15618
  return getCurrentUsage(historyDir);
15441
15619
  }
@@ -18137,25 +18315,27 @@ async function waitForCommandOnPath(cmd, opts = {}) {
18137
18315
  }
18138
18316
  return check();
18139
18317
  }
18140
- function augmentUserLocalBinPaths() {
18141
- const home = import_os4.default.homedir();
18318
+ function augmentUserLocalBinPaths(deps = {}) {
18319
+ const env = deps.env ?? process.env;
18320
+ const home = deps.homedir ?? import_os4.default.homedir();
18321
+ const p2 = deps.pathApi ?? import_path4.default;
18142
18322
  const candidates = [
18143
18323
  // XDG-style per-user bin — npm's default global-prefix bin dir on most
18144
18324
  // Linux setups (`npm config set prefix ~/.local` or an nvm-less
18145
18325
  // per-user npm), and where curl-based agent installers commonly land.
18146
- import_path4.default.join(home, ".local", "bin"),
18326
+ p2.join(home, ".local", "bin"),
18147
18327
  // Common explicit npm global-prefix conventions seen in the wild
18148
18328
  // (`npm config set prefix ~/.npm-global`, and Debian/Fedora's
18149
18329
  // `~/.local/share/npm` layout for `npm config set prefix
18150
18330
  // ~/.local/share/npm`).
18151
- import_path4.default.join(home, ".npm-global", "bin"),
18152
- import_path4.default.join(home, ".local", "share", "npm", "bin")
18331
+ p2.join(home, ".npm-global", "bin"),
18332
+ p2.join(home, ".local", "share", "npm", "bin")
18153
18333
  ];
18154
- const parts = (process.env.PATH ?? "").split(import_path4.default.delimiter).filter((p2) => p2.length > 0);
18334
+ const parts = (env.PATH ?? "").split(p2.delimiter).filter((s) => s.length > 0);
18155
18335
  const existing = new Set(parts);
18156
18336
  const additions = candidates.filter((dir) => !existing.has(dir));
18157
18337
  if (additions.length === 0) return;
18158
- process.env.PATH = [...additions, ...parts].join(import_path4.default.delimiter);
18338
+ env.PATH = [...additions, ...parts].join(p2.delimiter);
18159
18339
  }
18160
18340
  function resolveCursorAgentBinary(deps = {}) {
18161
18341
  const existsSync29 = deps.existsSync ?? import_fs.default.existsSync;
@@ -21786,7 +21966,7 @@ async function autoUpgradeBeforeCriticalCommand() {
21786
21966
  if (process.env.NODE_ENV === "test") return;
21787
21967
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
21788
21968
  if (process.env.CI) return;
21789
- const current2 = true ? "2.65.11" : null;
21969
+ const current2 = true ? "2.65.12" : null;
21790
21970
  if (!current2) return;
21791
21971
  const cache = readCache();
21792
21972
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -21803,7 +21983,7 @@ function checkForUpdates() {
21803
21983
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
21804
21984
  if (process.env.CI) return;
21805
21985
  if (!process.stdout.isTTY) return;
21806
- const current2 = true ? "2.65.11" : null;
21986
+ const current2 = true ? "2.65.12" : null;
21807
21987
  if (!current2) return;
21808
21988
  const cache = readCache();
21809
21989
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
@@ -21823,7 +22003,7 @@ var SELF_UPDATE_INTERVAL_MS = 60 * 60 * 1e3;
21823
22003
  var SELF_UPDATE_VIEW_TIMEOUT_MS = 3e4;
21824
22004
  var SELF_UPDATE_INSTALL_TIMEOUT_MS = 18e4;
21825
22005
  function currentCliVersion() {
21826
- return true ? "2.65.11" : null;
22006
+ return true ? "2.65.12" : null;
21827
22007
  }
21828
22008
  function runCmd(cmd, args2, timeoutMs) {
21829
22009
  return new Promise((resolve10) => {
@@ -41268,6 +41448,21 @@ var BatonController = class _BatonController {
41268
41448
  this._conversationId = conversationId;
41269
41449
  this.setState("LOCAL_DRIVE");
41270
41450
  }
41451
+ /**
41452
+ * The native TUI SWITCHED conversation while driving (Claude `/clear` →
41453
+ * a new id, `/resume` → an existing one): the process and the pairing are
41454
+ * unchanged, but the conversation the user is in has another id. Re-point
41455
+ * the baton at it so the read-only mirror follows that transcript and a
41456
+ * later Take Control resumes THAT conversation instead of the abandoned one. Guarded to LOCAL_DRIVE — only
41457
+ * the terminal can switch its own conversation, and during a hand-off /
41458
+ * MOBILE_DRIVE the native watcher is torn down anyway (`stop()`). Same id →
41459
+ * no-op. Re-publishes LOCAL_DRIVE so the mirror re-arms + mobile learns the id.
41460
+ */
41461
+ switchConversation(conversationId) {
41462
+ if (this._state !== "LOCAL_DRIVE" || this._conversationId === conversationId) return;
41463
+ this._conversationId = conversationId;
41464
+ this.setState("LOCAL_DRIVE");
41465
+ }
41271
41466
  async takeControl() {
41272
41467
  await this.switchDriver(
41273
41468
  "LOCAL_DRIVE",
@@ -41419,18 +41614,25 @@ var NativeTuiDriver = class _NativeTuiDriver {
41419
41614
  static QUICK_DISCOVER_MS = 8e3;
41420
41615
  outputSvc;
41421
41616
  historySvc;
41617
+ /** Unsubscribe for the runtime's conversation-switch watcher (armed per
41618
+ * `start()`, torn down in `stop()`), or null when not armed. */
41619
+ unwatchSwitch = null;
41422
41620
  setKeepAlive;
41423
41621
  keepAliveCtx;
41424
41622
  async start(resumeId) {
41425
41623
  if (resumeId !== void 0) {
41426
41624
  await this.deps.runtime.syncTranscriptForNativeResume?.(this.deps.opts.cwd, resumeId);
41427
41625
  await this.agent.restart(resumeId, false);
41626
+ this.armSwitchWatch(resumeId);
41428
41627
  return resumeId;
41429
41628
  }
41430
41629
  const spawnedAt = this.now();
41431
41630
  await this.agent.spawn();
41432
41631
  const preMinted = this.agent.spawnedSessionId;
41433
- if (preMinted) return preMinted;
41632
+ if (preMinted) {
41633
+ this.armSwitchWatch(preMinted);
41634
+ return preMinted;
41635
+ }
41434
41636
  const discover = this.deps.runtime.discoverSessionId;
41435
41637
  if (!discover) {
41436
41638
  throw new Error("NativeTuiDriver: agent did not expose a session id after spawn");
@@ -41446,7 +41648,26 @@ var NativeTuiDriver = class _NativeTuiDriver {
41446
41648
  });
41447
41649
  return null;
41448
41650
  }
41651
+ /**
41652
+ * Follow the native TUI if it switches conversation mid-session (Claude
41653
+ * `/clear` / `/resume`): the runtime's event-driven watcher reports the id
41654
+ * now being driven, we point the PTY-side history at it and tell the
41655
+ * controller to rebind. Armed once per `start()` (fresh or resume) on the
41656
+ * id the TUI is driving; inert for agents without the hook.
41657
+ */
41658
+ armSwitchWatch(currentId) {
41659
+ this.unwatchSwitch?.();
41660
+ this.unwatchSwitch = null;
41661
+ const watch3 = this.deps.runtime.watchConversationSwitch;
41662
+ if (!watch3) return;
41663
+ this.unwatchSwitch = watch3(this.deps.opts.cwd, { currentId }, (id) => {
41664
+ this.historySvc.setCurrentConversationId(id);
41665
+ this.deps.onConversationSwitch?.(id);
41666
+ });
41667
+ }
41449
41668
  async stop() {
41669
+ this.unwatchSwitch?.();
41670
+ this.unwatchSwitch = null;
41450
41671
  this.agent.kill();
41451
41672
  parkTerminalForReadonly();
41452
41673
  }
@@ -41693,8 +41914,8 @@ var TranscriptMirror = class {
41693
41914
  if (!file) return false;
41694
41915
  this.attached = true;
41695
41916
  this.emit(file);
41696
- const watch2 = this.deps.watch ?? defaultWatch;
41697
- this.unwatch = watch2(file, () => this.emit(file));
41917
+ const watch3 = this.deps.watch ?? defaultWatch;
41918
+ this.unwatch = watch3(file, () => this.emit(file));
41698
41919
  return true;
41699
41920
  }
41700
41921
  clearPoll() {
@@ -41909,10 +42130,17 @@ async function runBatonSession(opts) {
41909
42130
  // calls this once the user's first terminal turn creates the transcript.
41910
42131
  // Safe forward-ref — `controller` is initialised long before `begin()` runs,
41911
42132
  // and onLateBind only fires from inside `begin()`'s spawn.
41912
- onLateBind: (id) => controller.rebindConversation(id)
42133
+ onLateBind: (id) => controller.rebindConversation(id),
42134
+ // The native TUI switched conversation mid-drive (Claude `/clear` mints a
42135
+ // new id + JSONL; `/resume` re-opens an existing one): rebind so the mirror
42136
+ // follows that transcript and a later Take Control resumes THAT
42137
+ // conversation. Owner report 2026-08-18: after `/clear` (+`/rename`) the
42138
+ // mobile went silent — the mirror kept tailing the abandoned file.
42139
+ onConversationSwitch: (id) => controller.switchConversation(id)
41913
42140
  });
41914
42141
  let mirror = null;
41915
42142
  let firstLocalDrive = true;
42143
+ let lastPublished = null;
41916
42144
  const startMirror = (conversationId, fresh) => {
41917
42145
  mirror?.stop();
41918
42146
  mirror = new TranscriptMirror({
@@ -41945,11 +42173,13 @@ async function runBatonSession(opts) {
41945
42173
  publishState: (state, driver, conversationId) => {
41946
42174
  publishBatonState(state, driver, conversationId);
41947
42175
  if (state === "LOCAL_DRIVE" && conversationId) {
41948
- startMirror(conversationId, firstLocalDrive);
42176
+ const switched = lastPublished === "LOCAL_DRIVE";
42177
+ startMirror(conversationId, firstLocalDrive || switched);
41949
42178
  firstLocalDrive = false;
41950
42179
  } else if (state !== "LOCAL_DRIVE") {
41951
42180
  mirror?.stop();
41952
42181
  }
42182
+ lastPublished = state;
41953
42183
  }
41954
42184
  });
41955
42185
  const dispatchActive = (cmd) => controller.activeSessionDriver.dispatch(cmd);
@@ -45024,7 +45254,7 @@ function checkChokidar() {
45024
45254
  }
45025
45255
  async function doctor(args2 = []) {
45026
45256
  const json = args2.includes("--json");
45027
- const cliVersion = true ? "2.65.11" : "0.0.0-dev";
45257
+ const cliVersion = true ? "2.65.12" : "0.0.0-dev";
45028
45258
  const apiBase2 = resolveApiBaseUrl();
45029
45259
  const diagnosticId = (0, import_node_crypto13.randomUUID)();
45030
45260
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -45415,7 +45645,7 @@ async function mcpRun(args2) {
45415
45645
  // src/commands/version.ts
45416
45646
  var import_picocolors15 = __toESM(require("picocolors"));
45417
45647
  function version2() {
45418
- const v = true ? "2.65.11" : "unknown";
45648
+ const v = true ? "2.65.12" : "unknown";
45419
45649
  console.log(`${import_picocolors15.default.bold("codeam-cli")} ${import_picocolors15.default.cyan(v)}`);
45420
45650
  }
45421
45651
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.65.11",
3
+ "version": "2.65.12",
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",