codeam-cli 2.26.6 → 2.26.8

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 +12 -0
  2. package/dist/index.js +201 -49
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ 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.26.7] — 2026-06-03
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Preview no longer pollutes the host terminal + waits for tunnel DNS (#240)
12
+
13
+ ## [2.26.6] — 2026-06-03
14
+
15
+ ### Fixed
16
+
17
+ - **cli:** Preview shutdown is graceful + parser tolerates prose-wrapped JSON (#239)
18
+
7
19
  ## [2.26.5] — 2026-06-03
8
20
 
9
21
  ### Fixed
package/dist/index.js CHANGED
@@ -472,7 +472,7 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
472
472
  // package.json
473
473
  var package_default = {
474
474
  name: "codeam-cli",
475
- version: "2.26.6",
475
+ version: "2.26.8",
476
476
  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.",
477
477
  type: "commonjs",
478
478
  main: "dist/index.js",
@@ -5829,7 +5829,7 @@ function readAnonId() {
5829
5829
  }
5830
5830
  function superProperties() {
5831
5831
  return {
5832
- cliVersion: true ? "2.26.6" : "0.0.0-dev",
5832
+ cliVersion: true ? "2.26.8" : "0.0.0-dev",
5833
5833
  nodeVersion: process.version,
5834
5834
  platform: process.platform,
5835
5835
  arch: process.arch,
@@ -9572,6 +9572,50 @@ function parseHistoryFile(filePath) {
9572
9572
  }
9573
9573
  return out2;
9574
9574
  }
9575
+ function listResumableSessions(cwd) {
9576
+ const dir = resolveHistoryDir(cwd);
9577
+ if (!dir) return [];
9578
+ let entries;
9579
+ try {
9580
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
9581
+ } catch {
9582
+ return [];
9583
+ }
9584
+ const out2 = [];
9585
+ for (const entry of entries) {
9586
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
9587
+ const id = entry.name.slice(0, -".jsonl".length);
9588
+ const filePath = path12.join(dir, entry.name);
9589
+ let timestamp = Date.now();
9590
+ try {
9591
+ timestamp = fs9.statSync(filePath).mtimeMs;
9592
+ } catch {
9593
+ }
9594
+ let summary = "";
9595
+ try {
9596
+ const raw = fs9.readFileSync(filePath, "utf8");
9597
+ for (const line of raw.split("\n")) {
9598
+ if (!line.trim()) continue;
9599
+ try {
9600
+ const record = JSON.parse(line);
9601
+ if (record["type"] === "user") {
9602
+ const msg = record["message"];
9603
+ const text = extractText(msg?.["content"]).trim();
9604
+ if (text) {
9605
+ summary = text.slice(0, 120);
9606
+ break;
9607
+ }
9608
+ }
9609
+ } catch {
9610
+ }
9611
+ }
9612
+ } catch {
9613
+ }
9614
+ if (summary) out2.push({ id, summary, timestamp });
9615
+ }
9616
+ out2.sort((a, b) => b.timestamp - a.timestamp);
9617
+ return out2;
9618
+ }
9575
9619
 
9576
9620
  // src/agents/claude/parsing.ts
9577
9621
  function filterChrome(lines) {
@@ -9933,6 +9977,9 @@ var ClaudeRuntimeStrategy = class {
9933
9977
  getCurrentUsage(historyDir) {
9934
9978
  return getCurrentUsage(historyDir);
9935
9979
  }
9980
+ listResumableSessions(cwd) {
9981
+ return listResumableSessions(cwd);
9982
+ }
9936
9983
  async fetchWeeklyUsage() {
9937
9984
  return fetchClaudeQuota();
9938
9985
  }
@@ -10297,6 +10344,84 @@ function parseHistoryFile2(filePath) {
10297
10344
  }
10298
10345
  return out2;
10299
10346
  }
10347
+ function listResumableSessions2(cwd, homeOverride) {
10348
+ const home = homeOverride ?? import_node_os.default.homedir();
10349
+ const sessionsRoot = import_node_path2.default.join(home, ".codex", "sessions");
10350
+ if (!import_node_fs3.default.existsSync(sessionsRoot)) return [];
10351
+ let resolvedCurrent;
10352
+ try {
10353
+ resolvedCurrent = import_node_fs3.default.realpathSync(cwd);
10354
+ } catch {
10355
+ resolvedCurrent = import_node_path2.default.resolve(cwd);
10356
+ }
10357
+ const out2 = [];
10358
+ const now = /* @__PURE__ */ new Date();
10359
+ for (let dayOffset = 0; dayOffset < 7; dayOffset += 1) {
10360
+ const d3 = new Date(now.getTime() - dayOffset * 24 * 60 * 60 * 1e3);
10361
+ const yyyy = String(d3.getUTCFullYear());
10362
+ const mm = String(d3.getUTCMonth() + 1).padStart(2, "0");
10363
+ const dd = String(d3.getUTCDate()).padStart(2, "0");
10364
+ const dayDir = import_node_path2.default.join(sessionsRoot, yyyy, mm, dd);
10365
+ if (!import_node_fs3.default.existsSync(dayDir)) continue;
10366
+ let dayFiles;
10367
+ try {
10368
+ dayFiles = import_node_fs3.default.readdirSync(dayDir, { withFileTypes: true });
10369
+ } catch {
10370
+ continue;
10371
+ }
10372
+ for (const entry of dayFiles) {
10373
+ if (!entry.isFile()) continue;
10374
+ if (!entry.name.startsWith("rollout-") || !entry.name.endsWith(".jsonl")) {
10375
+ continue;
10376
+ }
10377
+ const filePath = import_node_path2.default.join(dayDir, entry.name);
10378
+ let timestamp = Date.now();
10379
+ try {
10380
+ timestamp = import_node_fs3.default.statSync(filePath).mtimeMs;
10381
+ } catch {
10382
+ }
10383
+ let metaCwd;
10384
+ let metaId;
10385
+ let summary = "";
10386
+ try {
10387
+ const raw = import_node_fs3.default.readFileSync(filePath, "utf8");
10388
+ for (const line of raw.split("\n")) {
10389
+ if (!line.trim()) continue;
10390
+ const rec = parseLine(line);
10391
+ if (!rec) continue;
10392
+ if (rec.type === "session_meta") {
10393
+ const meta = rec.payload;
10394
+ metaCwd = typeof meta?.cwd === "string" ? meta.cwd : void 0;
10395
+ metaId = typeof meta?.id === "string" ? meta.id : void 0;
10396
+ continue;
10397
+ }
10398
+ if (!summary && rec.type === "response_item") {
10399
+ const payload = rec.payload;
10400
+ const msg = payload?.Message;
10401
+ if (msg && msg.role === "user") {
10402
+ const text = extractMessageText(msg.content).trim();
10403
+ if (text) summary = text.slice(0, 120);
10404
+ }
10405
+ }
10406
+ if (metaCwd !== void 0 && summary) break;
10407
+ }
10408
+ } catch {
10409
+ continue;
10410
+ }
10411
+ if (!metaCwd || !metaId || !summary) continue;
10412
+ let resolvedMeta;
10413
+ try {
10414
+ resolvedMeta = import_node_fs3.default.realpathSync(metaCwd);
10415
+ } catch {
10416
+ resolvedMeta = import_node_path2.default.resolve(metaCwd);
10417
+ }
10418
+ if (resolvedMeta !== resolvedCurrent) continue;
10419
+ out2.push({ id: metaId, summary, timestamp });
10420
+ }
10421
+ }
10422
+ out2.sort((a, b) => b.timestamp - a.timestamp);
10423
+ return out2;
10424
+ }
10300
10425
  function getCurrentUsage2(historyDir) {
10301
10426
  if (!import_node_fs3.default.existsSync(historyDir)) return null;
10302
10427
  const files = import_node_fs3.default.readdirSync(historyDir).filter((f) => f.startsWith("rollout-") && f.endsWith(".jsonl")).map((f) => ({ name: f, full: import_node_path2.default.join(historyDir, f) })).map((e) => ({ ...e, mtime: import_node_fs3.default.statSync(e.full).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
@@ -10830,6 +10955,9 @@ var CodexRuntimeStrategy = class {
10830
10955
  getCurrentUsage(historyDir) {
10831
10956
  return getCurrentUsage2(historyDir);
10832
10957
  }
10958
+ listResumableSessions(cwd) {
10959
+ return listResumableSessions2(cwd);
10960
+ }
10833
10961
  /**
10834
10962
  * Codex's quota lives behind the `account/get_account_rate_limits` RPC,
10835
10963
  * not a TUI slash command. Phase 2 ships with this stubbed to null so the
@@ -12731,52 +12859,32 @@ var HistoryService = class _HistoryService {
12731
12859
  return Math.round(totalCost * 100) / 100;
12732
12860
  }
12733
12861
  /**
12734
- * Read session list from disk and POST it to the API.
12735
- * Called once ~2 s after Claude spawns (non-blocking).
12862
+ * Push the active agent's resumable-sessions list to the backend.
12863
+ * Delegates the per-agent JSONL/rollout walk to
12864
+ * `runtime.listResumableSessions(cwd)` so each agent reads its own
12865
+ * on-disk format (Claude's JSONL files vs Codex's date-bucketed
12866
+ * rollouts). Strategies that don't yet expose the helper (Cursor,
12867
+ * Aider) cause this to no-op — the Conversations sheet on mobile
12868
+ * just shows the empty state for those agents until each one's
12869
+ * `listResumableSessions` lands.
12870
+ *
12871
+ * The push body now includes `agentId` so the backend keys by
12872
+ * (pluginId, agentId). Old CLI clients that omit `agentId` continue
12873
+ * to land in the `claude-code` slot via the backend's default.
12874
+ *
12875
+ * Called once ~2 s after the agent spawns (non-blocking).
12736
12876
  */
12737
12877
  async load() {
12738
- const dir = this.projectDir;
12739
- let entries;
12740
- try {
12741
- entries = fs20.readdirSync(dir, { withFileTypes: true });
12742
- } catch {
12878
+ if (!this.runtime.listResumableSessions) {
12743
12879
  return;
12744
12880
  }
12745
- const sessions3 = [];
12746
- for (const entry of entries) {
12747
- if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
12748
- const id = path24.basename(entry.name, ".jsonl");
12749
- const filePath = path24.join(dir, entry.name);
12750
- let mtime = Date.now();
12751
- try {
12752
- mtime = fs20.statSync(filePath).mtimeMs;
12753
- } catch {
12754
- }
12755
- let summary = "";
12756
- try {
12757
- const raw = fs20.readFileSync(filePath, "utf8");
12758
- for (const line of raw.split("\n")) {
12759
- if (!line.trim()) continue;
12760
- try {
12761
- const record = JSON.parse(line);
12762
- if (record["type"] === "user") {
12763
- const msg = record["message"];
12764
- const text = extractText2(msg?.["content"]).trim();
12765
- if (text) {
12766
- summary = text.slice(0, 120);
12767
- break;
12768
- }
12769
- }
12770
- } catch {
12771
- }
12772
- }
12773
- } catch {
12774
- }
12775
- if (summary) sessions3.push({ id, summary, timestamp: mtime });
12776
- }
12881
+ const sessions3 = this.runtime.listResumableSessions(this.cwd);
12777
12882
  if (sessions3.length === 0) return;
12778
- sessions3.sort((a, b) => b.timestamp - a.timestamp);
12779
- await post("/api/sessions/list", { pluginId: this.pluginId, sessions: sessions3 });
12883
+ await post("/api/sessions/list", {
12884
+ pluginId: this.pluginId,
12885
+ agentId: this.runtime.id,
12886
+ sessions: sessions3
12887
+ });
12780
12888
  }
12781
12889
  /**
12782
12890
  * Read a specific session's full conversation and POST it to the API in batches.
@@ -15944,6 +16052,20 @@ var import_path4 = __toESM(require("path"));
15944
16052
  var import_promises2 = require("stream/promises");
15945
16053
  var import_which = __toESM(require("which"));
15946
16054
  var CACHED_BINARY = import_path4.default.join(import_os7.default.homedir(), ".codeam", "bin", "cloudflared");
16055
+ async function waitForCloudflaredReady(url, timeoutMs = 3e4) {
16056
+ const start2 = Date.now();
16057
+ while (Date.now() - start2 < timeoutMs) {
16058
+ try {
16059
+ const res = await fetch(url, { method: "HEAD" });
16060
+ if (res.status < 500) return;
16061
+ } catch {
16062
+ }
16063
+ await new Promise((r) => setTimeout(r, 500));
16064
+ }
16065
+ throw new Error(
16066
+ `Tunnel URL ${url} not reachable after ${timeoutMs}ms (DNS may still be propagating).`
16067
+ );
16068
+ }
15947
16069
  async function resolveCloudflared(opts = {}) {
15948
16070
  try {
15949
16071
  return await (0, import_which.default)("cloudflared");
@@ -16919,10 +17041,12 @@ var previewStartH = (ctx, _cmd, parsed) => {
16919
17041
  const onTunnelChunk = (chunk) => {
16920
17042
  const s = chunk.toString();
16921
17043
  if (!parsedUrl) parsedUrl = parseCloudflaredUrl(s);
17044
+ const trimmed = s.replace(/\n+$/g, "");
17045
+ if (trimmed.length > 0) log.info("preview", `cloudflared: ${trimmed}`);
16922
17046
  };
16923
17047
  tunnel.stderr.on("data", onTunnelChunk);
16924
17048
  tunnel.stdout.on("data", onTunnelChunk);
16925
- const tunnelDeadline = Date.now() + 15e3;
17049
+ const tunnelDeadline = Date.now() + 45e3;
16926
17050
  while (!parsedUrl && Date.now() < tunnelDeadline) {
16927
17051
  await new Promise((r) => setTimeout(r, 250));
16928
17052
  }
@@ -16940,7 +17064,27 @@ var previewStartH = (ctx, _cmd, parsed) => {
16940
17064
  pluginId: ctx.pluginId,
16941
17065
  pluginAuthToken,
16942
17066
  type: "preview_error",
16943
- payload: { stage: "tunnel", message: "cloudflared did not emit a URL within 15s." }
17067
+ payload: { stage: "tunnel", message: "cloudflared did not emit a URL within 45s." }
17068
+ });
17069
+ return;
17070
+ }
17071
+ try {
17072
+ await waitForCloudflaredReady(parsedUrl, 3e4);
17073
+ } catch (e) {
17074
+ try {
17075
+ tunnel.kill("SIGTERM");
17076
+ } catch {
17077
+ }
17078
+ try {
17079
+ devServer.kill("SIGTERM");
17080
+ } catch {
17081
+ }
17082
+ void postPreviewEvent({
17083
+ sessionId: ctx.sessionId,
17084
+ pluginId: ctx.pluginId,
17085
+ pluginAuthToken,
17086
+ type: "preview_error",
17087
+ payload: { stage: "tunnel", message: e.message }
16944
17088
  });
16945
17089
  return;
16946
17090
  }
@@ -16986,8 +17130,16 @@ function runOnce(cmd, args2, cwd, env) {
16986
17130
  const child = (0, import_child_process15.spawn)(cmd, args2, {
16987
17131
  cwd,
16988
17132
  env: { ...process.env, ...env ?? {} },
16989
- stdio: "inherit"
17133
+ stdio: ["ignore", "pipe", "pipe"]
16990
17134
  });
17135
+ const tag = `setup:${cmd}`;
17136
+ const onChunk = (chunk) => {
17137
+ const text = chunk.toString().replace(/\n+$/g, "");
17138
+ if (text.length === 0) return;
17139
+ log.info("preview", `${tag}: ${text}`);
17140
+ };
17141
+ child.stdout?.on("data", onChunk);
17142
+ child.stderr?.on("data", onChunk);
16991
17143
  child.once("exit", (code) => resolve5(code));
16992
17144
  child.once("error", () => resolve5(-1));
16993
17145
  });
@@ -19992,7 +20144,7 @@ function checkChokidar() {
19992
20144
  }
19993
20145
  async function doctor(args2 = []) {
19994
20146
  const json = args2.includes("--json");
19995
- const cliVersion = true ? "2.26.6" : "0.0.0-dev";
20147
+ const cliVersion = true ? "2.26.8" : "0.0.0-dev";
19996
20148
  const apiBase = resolveApiBaseUrl();
19997
20149
  const diagnosticId = (0, import_node_crypto6.randomUUID)();
19998
20150
  log.info("doctor", `run id=${diagnosticId} cli=${cliVersion}`);
@@ -20191,7 +20343,7 @@ async function completion(args2) {
20191
20343
  // src/commands/version.ts
20192
20344
  var import_picocolors13 = __toESM(require("picocolors"));
20193
20345
  function version2() {
20194
- const v = true ? "2.26.6" : "unknown";
20346
+ const v = true ? "2.26.8" : "unknown";
20195
20347
  console.log(`${import_picocolors13.default.bold("codeam-cli")} ${import_picocolors13.default.cyan(v)}`);
20196
20348
  }
20197
20349
 
@@ -20419,7 +20571,7 @@ function checkForUpdates() {
20419
20571
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
20420
20572
  if (process.env.CI) return;
20421
20573
  if (!process.stdout.isTTY) return;
20422
- const current = true ? "2.26.6" : null;
20574
+ const current = true ? "2.26.8" : null;
20423
20575
  if (!current) return;
20424
20576
  const cache = readCache();
20425
20577
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.26.6",
3
+ "version": "2.26.8",
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",