codeam-cli 2.10.7 → 2.10.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ 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.10.7] — 2026-05-11
8
+
9
+ ### Fixed
10
+
11
+ - **cli:** Don't auto-detect + upload stale JSONL on boot
12
+
7
13
  ## [2.10.6] — 2026-05-11
8
14
 
9
15
  ### Added
package/dist/index.js CHANGED
@@ -1477,7 +1477,7 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
1477
1477
  // package.json
1478
1478
  var package_default = {
1479
1479
  name: "codeam-cli",
1480
- version: "2.10.7",
1480
+ version: "2.10.8",
1481
1481
  description: "Remote control Claude Code (and other AI coding agents) from your mobile phone. Pair your device, send prompts, stream responses in real-time, and approve commands \u2014 from anywhere.",
1482
1482
  type: "commonjs",
1483
1483
  main: "dist/index.js",
@@ -1848,6 +1848,13 @@ var CommandRelayService = class {
1848
1848
  /** Polling backoff state (only used on the fallback). */
1849
1849
  pollTimer = null;
1850
1850
  pollFailures = 0;
1851
+ // Successive polls that returned no commands. Drives an idle
1852
+ // backoff so a CLI sitting on the polling fallback (SSE was
1853
+ // unavailable) doesn't keep hammering the API every 2 s when
1854
+ // there's nothing to do. Reset to 0 whenever a poll DELIVERS a
1855
+ // command, so the first real work after a quiet period still
1856
+ // reaches the CLI quickly.
1857
+ pollEmptyStreak = 0;
1851
1858
  /** Reconnect backoff state for the SSE stream. */
1852
1859
  sseFailures = 0;
1853
1860
  sseReconnectTimer = null;
@@ -1976,7 +1983,9 @@ var CommandRelayService = class {
1976
1983
  if (!this._running) return;
1977
1984
  await this.pollOnce();
1978
1985
  if (this._running) {
1979
- const delay = computePollDelay({ baseMs: 2e3, failures: this.pollFailures });
1986
+ const idleExp = Math.min(this.pollEmptyStreak, 4);
1987
+ const effectiveFailures = Math.max(this.pollFailures, idleExp);
1988
+ const delay = computePollDelay({ baseMs: 2e3, failures: effectiveFailures });
1980
1989
  this.pollTimer = setTimeout(() => this.pollLoop(), delay);
1981
1990
  }
1982
1991
  }
@@ -1985,7 +1994,11 @@ var CommandRelayService = class {
1985
1994
  const data = await _getJson(`${API_BASE2}/api/commands/pending?pluginId=${this.pluginId}`);
1986
1995
  const commands = data?.data;
1987
1996
  this.pollFailures = 0;
1988
- if (!Array.isArray(commands) || commands.length === 0) return;
1997
+ if (!Array.isArray(commands) || commands.length === 0) {
1998
+ this.pollEmptyStreak += 1;
1999
+ return;
2000
+ }
2001
+ this.pollEmptyStreak = 0;
1989
2002
  log.trace("relay", `poll received ${commands.length} command(s)`);
1990
2003
  await this.dispatchCommands(commands);
1991
2004
  } catch (err) {
@@ -5504,19 +5517,8 @@ var OutputService = class _OutputService {
5504
5517
  tryExtractSessionId(text) {
5505
5518
  if (!this.onSessionIdDetected) return;
5506
5519
  const printable = text.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, "");
5507
- const patterns = [
5508
- /Resuming session[:\s]+([a-f0-9-]{36})/i,
5509
- /Session[:\s]+([a-f0-9-]{36})/i,
5510
- /Conversation[:\s]+([a-f0-9-]{36})/i,
5511
- /Session\s+ID[:\s]+([a-f0-9-]{36})/i
5512
- ];
5513
- for (const pattern of patterns) {
5514
- const match = printable.match(pattern);
5515
- if (match) {
5516
- this.onSessionIdDetected(match[1]);
5517
- return;
5518
- }
5519
- }
5520
+ const match = printable.match(/Resuming session[:\s]+([a-f0-9-]{36})/i);
5521
+ if (match) this.onSessionIdDetected(match[1]);
5520
5522
  }
5521
5523
  tryDetectRateLimit(text) {
5522
5524
  if (!this.onRateLimitDetected) return;
@@ -5652,10 +5654,11 @@ function post(endpoint, body) {
5652
5654
  req.end();
5653
5655
  });
5654
5656
  }
5655
- var HistoryService = class {
5656
- constructor(pluginId, cwd) {
5657
+ var HistoryService = class _HistoryService {
5658
+ constructor(pluginId, cwd, options) {
5657
5659
  this.pluginId = pluginId;
5658
5660
  this.cwd = cwd;
5661
+ this.bootTimeMs = options?.bootTimeMs ?? Date.now();
5659
5662
  }
5660
5663
  pluginId;
5661
5664
  cwd;
@@ -5671,6 +5674,25 @@ var HistoryService = class {
5671
5674
  * resume re-uploads the full transcript on first call.
5672
5675
  */
5673
5676
  lastUploadedUuid = /* @__PURE__ */ new Map();
5677
+ /**
5678
+ * Captured at construction time so every JSONL discovery (detect,
5679
+ * usage stats) can ignore files that already existed in the
5680
+ * project's `~/.claude/projects/<cwd>/` dir *before* this CLI
5681
+ * run started. Without this filter, an old conversation — or
5682
+ * a *parallel* Claude session actively writing to the same
5683
+ * project — wins the mtime sort and we publish its content
5684
+ * to the mobile app as if it were the fresh pair's chat.
5685
+ */
5686
+ bootTimeMs;
5687
+ /**
5688
+ * Small grace window subtracted from `bootTimeMs` when filtering
5689
+ * by `birthtime`. Covers clock skew + filesystem timestamp
5690
+ * rounding (HFS+ floors to 1 s; some Linux filesystems round to
5691
+ * the nearest second). 5 s is comfortably wider than any
5692
+ * filesystem rounding while still excluding everything from a
5693
+ * previous pair / previous Claude run.
5694
+ */
5695
+ static BIRTHTIME_GRACE_MS = 5e3;
5674
5696
  /** Store rate limit reset info detected from Claude Code output */
5675
5697
  setRateLimitReset(reset) {
5676
5698
  this._rateLimitReset = reset;
@@ -5725,37 +5747,50 @@ var HistoryService = class {
5725
5747
  }
5726
5748
  return null;
5727
5749
  }
5728
- /** Detect the active conversation by finding the most recently modified JSONL file */
5750
+ /**
5751
+ * Detect the active conversation by finding the most recently
5752
+ * modified JSONL file that was **created during this CLI run**.
5753
+ * The birthtime filter is critical: without it, an old
5754
+ * conversation in the same project dir — or a parallel Claude
5755
+ * session actively writing to a sibling JSONL — wins the mtime
5756
+ * sort, and we publish that other run's content to mobile as if
5757
+ * it were the fresh pair's chat. With the filter, only files
5758
+ * Claude created on or after `bootTimeMs` are eligible, so a
5759
+ * fresh pair stays empty until the user actually types a turn.
5760
+ */
5729
5761
  detectCurrentConversation() {
5730
5762
  const dir = this.projectDir;
5763
+ const cutoff = this.bootTimeMs - _HistoryService.BIRTHTIME_GRACE_MS;
5731
5764
  try {
5732
5765
  const files = fs5.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => {
5733
5766
  try {
5734
- return { name: e.name, mtime: fs5.statSync(path8.join(dir, e.name)).mtimeMs };
5767
+ const stat3 = fs5.statSync(path8.join(dir, e.name));
5768
+ return { name: e.name, mtime: stat3.mtimeMs, birthtime: stat3.birthtimeMs };
5735
5769
  } catch {
5736
- return { name: e.name, mtime: 0 };
5770
+ return { name: e.name, mtime: 0, birthtime: 0 };
5737
5771
  }
5738
- }).sort((a, b) => b.mtime - a.mtime);
5772
+ }).filter((f) => f.birthtime >= cutoff).sort((a, b) => b.mtime - a.mtime);
5739
5773
  if (files.length > 0) {
5740
5774
  this.currentConversationId = path8.basename(files[0].name, ".jsonl");
5741
5775
  }
5742
5776
  } catch {
5743
5777
  }
5744
5778
  }
5745
- /** Extract conversation ID from Claude output (e.g., from session resume messages) */
5779
+ /**
5780
+ * Extract conversation ID from Claude output. Limited to the
5781
+ * unambiguous "Resuming session: <uuid>" pattern — the older
5782
+ * generic `Session: <uuid>` / `Conversation: <uuid>` patterns
5783
+ * were too greedy and matched any incidental UUID-bearing line
5784
+ * Claude printed (debug logs, status info, etc.), causing the
5785
+ * CLI to "detect" the wrong conversation on a fresh pair.
5786
+ * Resume is the only flow that legitimately needs to bind via
5787
+ * output text; everything else sets `currentConversationId`
5788
+ * via `setCurrentConversationId()` or the birthtime-filtered
5789
+ * `detectCurrentConversation()`.
5790
+ */
5746
5791
  tryExtractConversationIdFromOutput(output) {
5747
- const patterns = [
5748
- /Resuming session[:\s]+([a-f0-9-]{36})/i,
5749
- /session[:\s]+([a-f0-9-]{36})/i,
5750
- /conversation[:\s]+([a-f0-9-]{36})/i
5751
- ];
5752
- for (const pattern of patterns) {
5753
- const match = output.match(pattern);
5754
- if (match) {
5755
- this.currentConversationId = match[1];
5756
- return;
5757
- }
5758
- }
5792
+ const match = output.match(/Resuming session[:\s]+([a-f0-9-]{36})/i);
5793
+ if (match) this.currentConversationId = match[1];
5759
5794
  }
5760
5795
  /**
5761
5796
  * Read the most recently modified JSONL session file and extract the
@@ -5767,6 +5802,7 @@ var HistoryService = class {
5767
5802
  */
5768
5803
  getCurrentUsage() {
5769
5804
  const dir = this.projectDir;
5805
+ const cutoff = this.bootTimeMs - _HistoryService.BIRTHTIME_GRACE_MS;
5770
5806
  let entries;
5771
5807
  try {
5772
5808
  entries = fs5.readdirSync(dir, { withFileTypes: true });
@@ -5775,11 +5811,12 @@ var HistoryService = class {
5775
5811
  }
5776
5812
  const files = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => {
5777
5813
  try {
5778
- return { name: e.name, mtime: fs5.statSync(path8.join(dir, e.name)).mtimeMs };
5814
+ const stat3 = fs5.statSync(path8.join(dir, e.name));
5815
+ return { name: e.name, mtime: stat3.mtimeMs, birthtime: stat3.birthtimeMs };
5779
5816
  } catch {
5780
- return { name: e.name, mtime: 0 };
5817
+ return { name: e.name, mtime: 0, birthtime: 0 };
5781
5818
  }
5782
- }).sort((a, b) => b.mtime - a.mtime);
5819
+ }).filter((f) => f.birthtime >= cutoff).sort((a, b) => b.mtime - a.mtime);
5783
5820
  if (files.length === 0) return null;
5784
5821
  const targetFile = this.currentConversationId ? `${this.currentConversationId}.jsonl` : files[0].name;
5785
5822
  if (!files.some((f) => f.name === targetFile)) return null;
@@ -9315,7 +9352,7 @@ async function stopWorkspaceFromLocal(target) {
9315
9352
  // src/commands/version.ts
9316
9353
  var import_picocolors11 = __toESM(require("picocolors"));
9317
9354
  function version() {
9318
- const v = true ? "2.10.7" : "unknown";
9355
+ const v = true ? "2.10.8" : "unknown";
9319
9356
  console.log(`${import_picocolors11.default.bold("codeam-cli")} ${import_picocolors11.default.cyan(v)}`);
9320
9357
  }
9321
9358
 
@@ -9450,7 +9487,7 @@ function checkForUpdates() {
9450
9487
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
9451
9488
  if (process.env.CI) return;
9452
9489
  if (!process.stdout.isTTY) return;
9453
- const current = true ? "2.10.7" : null;
9490
+ const current = true ? "2.10.8" : null;
9454
9491
  if (!current) return;
9455
9492
  const cache = readCache();
9456
9493
  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.10.7",
3
+ "version": "2.10.8",
4
4
  "description": "Remote control Claude Code (and other AI coding agents) from your mobile phone. Pair your device, send prompts, stream responses in real-time, and approve commands — from anywhere.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",