codeam-cli 2.17.0 → 2.17.2

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 +19 -0
  2. package/dist/index.js +485 -446
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ 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.17.0] — 2026-05-22
8
+
9
+ ### Added
10
+
11
+ - Detect OpenAI Codex in VS Code + JetBrains plugins (#46)
12
+
13
+ ### CI
14
+
15
+ - **vsc-plugin:** VS Code E2E workflow — open panel + pair-backend probe (#45)
16
+
17
+ ### Fixed
18
+
19
+ - **cli:** Windows EPERM #43 + cross-OS CI smoke matrix + agent-creds tests
20
+ ## [2.16.1] — 2026-05-21
21
+
22
+ ### Fixed
23
+
24
+ - **cli:** Codeam link — auto-install, multi-probe creds, file-watcher login (#42)
25
+
7
26
  ## [2.16.2] — 2026-05-22
8
27
 
9
28
  ### Chore
package/dist/index.js CHANGED
@@ -424,7 +424,7 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
424
424
  // package.json
425
425
  var package_default = {
426
426
  name: "codeam-cli",
427
- version: "2.17.0",
427
+ version: "2.17.2",
428
428
  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.",
429
429
  type: "commonjs",
430
430
  main: "dist/index.js",
@@ -1537,9 +1537,253 @@ var WindowsConPtyStrategy = class _WindowsConPtyStrategy {
1537
1537
  };
1538
1538
  };
1539
1539
 
1540
+ // src/services/claude-resolver.ts
1541
+ var path6 = __toESM(require("path"));
1542
+ init_types();
1543
+ function buildClaudeLaunch(extraArgs = []) {
1544
+ const found = findInPath("claude") ?? findInPath("claude-code");
1545
+ if (!found) return null;
1546
+ if (process.platform === "win32") {
1547
+ const ext = path6.extname(found).toLowerCase();
1548
+ if (ext === ".cmd" || ext === ".bat") {
1549
+ return { cmd: "cmd.exe", args: ["/c", found, ...extraArgs] };
1550
+ }
1551
+ if (ext === ".ps1") {
1552
+ return {
1553
+ cmd: "powershell.exe",
1554
+ args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", found, ...extraArgs]
1555
+ };
1556
+ }
1557
+ }
1558
+ return { cmd: found, args: extraArgs };
1559
+ }
1560
+
1561
+ // src/services/agent.service.ts
1562
+ var AgentService = class {
1563
+ constructor(runtime, opts) {
1564
+ this.runtime = runtime;
1565
+ this.opts = opts;
1566
+ this.strategyOpts = {
1567
+ onData: (d3) => {
1568
+ if (!this.claudeReady && d3.length > 0) {
1569
+ this.claudeReady = true;
1570
+ setTimeout(() => this.drainPending(), 250);
1571
+ }
1572
+ (opts.onData ?? (() => {
1573
+ }))(d3);
1574
+ },
1575
+ onExit: opts.onExit
1576
+ };
1577
+ }
1578
+ runtime;
1579
+ opts;
1580
+ // Strategy is selected lazily inside spawn() so we can fall back from
1581
+ // ConPTY → legacy pipe at runtime if the native binding fails to load.
1582
+ // Methods called before spawn() (e.g. early kill/SIGINT) no-op safely.
1583
+ strategy = null;
1584
+ strategyOpts;
1585
+ /**
1586
+ * Set once the PTY emits its FIRST batch of output — proxy for
1587
+ * "Claude has rendered its input box and is ready to read keystrokes."
1588
+ * Before this, remote `sendCommand`s are buffered (`pendingInputs`)
1589
+ * and replayed in order on first data. Without this guard, the very
1590
+ * first prompt right after `codeam pair` on Windows lands while
1591
+ * Claude's React Ink tree is still mounting — the input bytes are
1592
+ * accepted by the PTY but never make it to the input field, and
1593
+ * the prompt silently vanishes.
1594
+ */
1595
+ claudeReady = false;
1596
+ pendingInputs = [];
1597
+ drainPending() {
1598
+ if (!this.strategy || this.pendingInputs.length === 0) return;
1599
+ const s = this.strategy;
1600
+ log.trace("claude", `drain pending=${this.pendingInputs.length}`);
1601
+ let offset = 0;
1602
+ for (const text of this.pendingInputs) {
1603
+ setTimeout(() => s.write(text), offset);
1604
+ setTimeout(() => s.write("\r"), offset + 50);
1605
+ offset += 200;
1606
+ }
1607
+ this.pendingInputs.length = 0;
1608
+ }
1609
+ async spawn() {
1610
+ let launch;
1611
+ try {
1612
+ launch = await this.runtime.prepareLaunch();
1613
+ } catch (err) {
1614
+ const msg = err instanceof Error ? err.message : String(err);
1615
+ console.error(
1616
+ `
1617
+ \u2717 ${this.runtime.meta.displayName} could not be launched.
1618
+ ${msg}
1619
+ `
1620
+ );
1621
+ process.exit(1);
1622
+ }
1623
+ if (process.platform === "win32") {
1624
+ log.trace("claude", `spawn (win32) cmd=${launch.cmd} args=${launch.args.join(" ")}`);
1625
+ const conpty = WindowsConPtyStrategy.tryCreate(this.strategyOpts);
1626
+ if (conpty) {
1627
+ try {
1628
+ conpty.spawn(launch.cmd, this.opts.cwd, launch.args);
1629
+ this.strategy = conpty;
1630
+ log.trace("claude", "ConPTY spawn ok");
1631
+ return;
1632
+ } catch (err) {
1633
+ const msg = err instanceof Error ? err.message : String(err);
1634
+ try {
1635
+ conpty.dispose();
1636
+ } catch {
1637
+ }
1638
+ console.error(`
1639
+ \u26A0 ConPTY launch failed (${msg.split("\n")[0]})`);
1640
+ console.error(" Falling back to pipe mode (limited interactivity)\u2026\n");
1641
+ }
1642
+ } else {
1643
+ console.error(
1644
+ '\n \u26A0 Windows: node-pty unavailable, falling back to pipe mode.\n Claude may exit with "no stdin data" / "--print" errors.\n Reinstall the CLI to fetch the prebuilt ConPTY binary, or run inside WSL.\n'
1645
+ );
1646
+ }
1647
+ const pipe = new WindowsPtyStrategy(this.strategyOpts);
1648
+ pipe.spawn(launch.cmd, this.opts.cwd, launch.args);
1649
+ this.strategy = pipe;
1650
+ return;
1651
+ }
1652
+ const unix = new UnixPtyStrategy(this.strategyOpts);
1653
+ unix.spawn(launch.cmd, this.opts.cwd, launch.args);
1654
+ this.strategy = unix;
1655
+ }
1656
+ /**
1657
+ * Send a command to Claude's stdin (remote control from mobile).
1658
+ *
1659
+ * Why two separate writes with a delay?
1660
+ * Same batching problem as selectOption: all bytes arriving in one write()
1661
+ * call are processed by readline in one synchronous run. React Ink batches
1662
+ * the resulting state updates, so when '\r' fires the input's value is still
1663
+ * the pre-batch (empty/previous) state → Enter submits nothing and the text
1664
+ * stays visible-but-unsubmitted in the input field.
1665
+ *
1666
+ * Sending '\r' in a separate write() ~50 ms later guarantees it arrives on
1667
+ * a fresh event-loop tick, after React has flushed the text into input state.
1668
+ *
1669
+ * The delay scales with line count: Smart Composer outputs are ~500–1500
1670
+ * chars with embedded `\n`s (Task / Context / Steps blocks). Ink's input
1671
+ * field re-flows multi-line content per render and the 50 ms baseline that
1672
+ * worked for single-line prompts isn't enough headroom — the text lands in
1673
+ * the field but `\r` submits stale state and the prompt sits there until
1674
+ * the user hits Enter manually. Adding ~40 ms per extra line (capped at
1675
+ * 300 ms) keeps short prompts snappy while giving multi-line composer
1676
+ * outputs the time they need to settle.
1677
+ */
1678
+ sendCommand(text) {
1679
+ if (!this.strategy) {
1680
+ log.trace("claude", "sendCommand dropped (no strategy)");
1681
+ return;
1682
+ }
1683
+ if (!this.claudeReady) {
1684
+ log.trace("claude", `sendCommand buffered (not ready) text=${text.length}B`);
1685
+ this.pendingInputs.push(text);
1686
+ return;
1687
+ }
1688
+ const s = this.strategy;
1689
+ log.trace("claude", `sendCommand text=${text.length}B`);
1690
+ s.write(text);
1691
+ const lineCount = text.split("\n").length;
1692
+ const delay = Math.min(300, 50 + (lineCount - 1) * 40);
1693
+ setTimeout(() => s.write("\r"), delay);
1694
+ }
1695
+ /**
1696
+ * Navigate a React Ink selector to the given 0-based target index and confirm.
1697
+ *
1698
+ * `fromIndex` is the current highlighted position (defaults to 0 for
1699
+ * numbered selectors which always start at the first option). For list-style
1700
+ * selectors (e.g. /mcp), the CLI sends `currentIndex` in the select_prompt
1701
+ * chunk so the client can pass it back here as `fromIndex`, enabling both
1702
+ * up-arrow and down-arrow navigation without always rewinding to position 0.
1703
+ *
1704
+ * Why not sendCommand(arrows + Enter) in one write()?
1705
+ * All bytes arrive as one chunk → readline fires all keypress events in the
1706
+ * same synchronous run → React Ink batches the state updates → each arrow
1707
+ * sees selectedIndex=0 → final state is still 0 or 1 → wrong option selected.
1708
+ *
1709
+ * Fix: send each arrow in a separate write(), ARROW_MS apart, so React has
1710
+ * time to process and re-render between each keystroke. Enter is sent
1711
+ * ENTER_MS after the last arrow.
1712
+ */
1713
+ selectOption(targetIndex, fromIndex = 0) {
1714
+ if (!this.strategy) return;
1715
+ const s = this.strategy;
1716
+ const delta = targetIndex - fromIndex;
1717
+ const steps = Math.abs(delta);
1718
+ const arrow = delta >= 0 ? "\x1B[B" : "\x1B[A";
1719
+ const ARROW_MS = 80;
1720
+ const ENTER_MS = 200;
1721
+ if (steps === 0) {
1722
+ s.write("\r");
1723
+ return;
1724
+ }
1725
+ for (let i = 0; i < steps; i++) {
1726
+ setTimeout(() => {
1727
+ s.write(arrow);
1728
+ }, i * ARROW_MS);
1729
+ }
1730
+ setTimeout(() => {
1731
+ s.write("\r");
1732
+ }, steps * ARROW_MS + ENTER_MS);
1733
+ }
1734
+ /**
1735
+ * Write raw bytes to the PTY without any auto-appended `\r` or delay.
1736
+ * Use this when the caller already owns the full input (e.g. the
1737
+ * `ptyInput` returned by `RuntimeStrategy.changeModelInstruction()`
1738
+ * already contains the trailing `\r`).
1739
+ */
1740
+ sendRawPtyInput(text) {
1741
+ if (!this.strategy) {
1742
+ log.trace("claude", "sendRawPtyInput dropped (no strategy)");
1743
+ return;
1744
+ }
1745
+ log.trace("claude", `sendRawPtyInput len=${text.length}`);
1746
+ this.strategy.write(text);
1747
+ }
1748
+ /** Send Escape key to Claude (cancels interactive prompts). */
1749
+ sendEscape() {
1750
+ this.strategy?.write("\x1B");
1751
+ }
1752
+ /** Send Ctrl+C to Claude. */
1753
+ interrupt() {
1754
+ this.strategy?.write("");
1755
+ }
1756
+ kill() {
1757
+ this.strategy?.kill();
1758
+ }
1759
+ /**
1760
+ * Kill the current Claude process and relaunch it resuming the given session.
1761
+ * Pass auto=true to add --dangerously-skip-permissions (no confirmation prompts).
1762
+ *
1763
+ * For agents that use CLI flags (Claude: --resume <id>), `resumeLaunchArgs`
1764
+ * returns a non-empty array and we pass those directly to the binary.
1765
+ * For agents that use a post-spawn PTY instruction (e.g. Codex), `resumeLaunchArgs`
1766
+ * returns [] and `postSpawnInstruction` types the resume command into the PTY.
1767
+ */
1768
+ restart(sessionId, auto = false) {
1769
+ if (!this.strategy) return;
1770
+ const resumeArgs = auto ? this.runtime.resumeLaunchArgs(sessionId) : ["--resume", sessionId];
1771
+ const launch = buildClaudeLaunch(resumeArgs);
1772
+ if (!launch) return;
1773
+ this.strategy.kill();
1774
+ this.strategy.spawn(launch.cmd, this.opts.cwd, launch.args);
1775
+ if (resumeArgs.length === 0 && this.runtime.postSpawnInstruction) {
1776
+ const { ptyInput } = this.runtime.postSpawnInstruction(sessionId);
1777
+ setTimeout(() => {
1778
+ this.strategy?.write(ptyInput);
1779
+ }, 500);
1780
+ }
1781
+ }
1782
+ };
1783
+
1540
1784
  // src/services/claude-installer.ts
1541
1785
  var import_child_process4 = require("child_process");
1542
- var path6 = __toESM(require("path"));
1786
+ var path7 = __toESM(require("path"));
1543
1787
  var os5 = __toESM(require("os"));
1544
1788
 
1545
1789
  // ../../node_modules/@clack/prompts/dist/index.mjs
@@ -3408,385 +3652,128 @@ ${h.value}` : h.value;
3408
3652
  return { message(h, f) {
3409
3653
  m(a[0], h, f);
3410
3654
  }, group(h) {
3411
- const f = { header: h, value: "", full: "" };
3412
- return a.push(f), { message(v, T2) {
3413
- m(f, v, T2);
3414
- }, error(v) {
3415
- S2(f, { status: "error", message: v });
3416
- }, success(v) {
3417
- S2(f, { status: "success", message: v });
3418
- } };
3419
- }, error(h, f) {
3420
- $2(true), O2.error(h, { output: i, secondarySymbol: r, spacing: 1 }), f?.showLog !== false && p2(), a.splice(1, a.length - 1), a[0].value = "", a[0].full = "";
3421
- }, success(h, f) {
3422
- $2(true), O2.success(h, { output: i, secondarySymbol: r, spacing: 1 }), f?.showLog === true && p2(), a.splice(1, a.length - 1), a[0].value = "", a[0].full = "";
3423
- } };
3424
- };
3425
- var Ot = (e) => new at({ validate: e.validate, placeholder: e.placeholder, defaultValue: e.defaultValue, initialValue: e.initialValue, output: e.output, signal: e.signal, input: e.input, render() {
3426
- const i = e?.withGuide ?? u.withGuide, s = `${`${i ? `${(0, import_node_util2.styleText)("gray", d2)}
3427
- ` : ""}${V2(this.state)} `}${e.message}
3428
- `, r = e.placeholder ? (0, import_node_util2.styleText)("inverse", e.placeholder[0]) + (0, import_node_util2.styleText)("dim", e.placeholder.slice(1)) : (0, import_node_util2.styleText)(["inverse", "hidden"], "_"), u2 = this.userInput ? this.userInputWithCursor : r, n = this.value ?? "";
3429
- switch (this.state) {
3430
- case "error": {
3431
- const o = this.error ? ` ${(0, import_node_util2.styleText)("yellow", this.error)}` : "", c2 = i ? `${(0, import_node_util2.styleText)("yellow", d2)} ` : "", a = i ? (0, import_node_util2.styleText)("yellow", E2) : "";
3432
- return `${s.trim()}
3433
- ${c2}${u2}
3434
- ${a}${o}
3435
- `;
3436
- }
3437
- case "submit": {
3438
- const o = n ? ` ${(0, import_node_util2.styleText)("dim", n)}` : "", c2 = i ? (0, import_node_util2.styleText)("gray", d2) : "";
3439
- return `${s}${c2}${o}`;
3440
- }
3441
- case "cancel": {
3442
- const o = n ? ` ${(0, import_node_util2.styleText)(["strikethrough", "dim"], n)}` : "", c2 = i ? (0, import_node_util2.styleText)("gray", d2) : "";
3443
- return `${s}${c2}${o}${n.trim() ? `
3444
- ${c2}` : ""}`;
3445
- }
3446
- default: {
3447
- const o = i ? `${(0, import_node_util2.styleText)("cyan", d2)} ` : "", c2 = i ? (0, import_node_util2.styleText)("cyan", E2) : "";
3448
- return `${s}${o}${u2}
3449
- ${c2}
3450
- `;
3451
- }
3452
- }
3453
- } }).prompt();
3454
-
3455
- // src/services/claude-installer.ts
3456
- init_types();
3457
- function probeInstallDirs() {
3458
- const home = os5.homedir();
3459
- if (process.platform === "win32") {
3460
- return [
3461
- path6.join(home, ".claude", "local"),
3462
- path6.join(home, "AppData", "Local", "AnthropicClaude"),
3463
- path6.join(home, "AppData", "Local", "Programs", "AnthropicClaude")
3464
- ];
3465
- }
3466
- return [
3467
- path6.join(home, ".local", "bin"),
3468
- path6.join(home, ".claude", "local"),
3469
- "/usr/local/bin"
3470
- ];
3471
- }
3472
- function isAvailable() {
3473
- return findInPath("claude") !== null || findInPath("claude-code") !== null;
3474
- }
3475
- function augmentPath() {
3476
- const dirs = probeInstallDirs();
3477
- const sep2 = path6.delimiter;
3478
- const current = process.env.PATH ?? "";
3479
- const existing = new Set(current.split(sep2).filter(Boolean));
3480
- const additions = dirs.filter((d3) => !existing.has(d3));
3481
- if (additions.length === 0) return;
3482
- process.env.PATH = additions.join(sep2) + sep2 + current;
3483
- }
3484
- function runInstaller() {
3485
- const isWindows = process.platform === "win32";
3486
- const cmd = isWindows ? "powershell.exe" : "bash";
3487
- const args2 = isWindows ? [
3488
- "-NoProfile",
3489
- "-ExecutionPolicy",
3490
- "Bypass",
3491
- "-Command",
3492
- "irm https://claude.ai/install.ps1 | iex"
3493
- ] : ["-c", "curl -fsSL https://claude.ai/install.sh | bash"];
3494
- return new Promise((resolve3) => {
3495
- const proc = (0, import_child_process4.spawn)(cmd, args2, { stdio: "inherit" });
3496
- proc.on("error", (err) => {
3497
- console.error(`
3498
- \u2717 Installer failed to launch: ${err.message}`);
3499
- resolve3(false);
3500
- });
3501
- proc.on("exit", (code) => {
3502
- resolve3(code === 0);
3503
- });
3504
- });
3505
- }
3506
- async function ensureClaudeInstalled() {
3507
- if (isAvailable()) return true;
3508
- const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
3509
- if (isInteractive) {
3510
- const proceed = await ot2({
3511
- message: "Claude Code is not installed on this machine. Install it now using the official installer?",
3512
- initialValue: true
3513
- });
3514
- if (q(proceed) || proceed === false) return false;
3515
- } else {
3516
- console.log("\n Claude Code not found \u2014 running the official installer...\n");
3517
- }
3518
- console.log();
3519
- const ok = await runInstaller();
3520
- if (!ok) {
3521
- console.error("\n \u2717 Claude Code installation failed. See the installer output above.");
3522
- return false;
3523
- }
3524
- augmentPath();
3525
- if (!isAvailable()) {
3526
- console.error(
3527
- "\n \u26A0 Claude Code installed but the binary is still not on PATH for this process.\n This usually means the installer registered a new directory that only takes\n effect in a fresh shell. Restart your terminal and run `codeam pair` again."
3528
- );
3529
- return false;
3530
- }
3531
- return true;
3532
- }
3533
-
3534
- // src/services/claude-resolver.ts
3535
- var path7 = __toESM(require("path"));
3536
- init_types();
3537
- function buildClaudeLaunch(extraArgs = []) {
3538
- const found = findInPath("claude") ?? findInPath("claude-code");
3539
- if (!found) return null;
3540
- if (process.platform === "win32") {
3541
- const ext = path7.extname(found).toLowerCase();
3542
- if (ext === ".cmd" || ext === ".bat") {
3543
- return { cmd: "cmd.exe", args: ["/c", found, ...extraArgs] };
3544
- }
3545
- if (ext === ".ps1") {
3546
- return {
3547
- cmd: "powershell.exe",
3548
- args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", found, ...extraArgs]
3549
- };
3550
- }
3551
- }
3552
- return { cmd: found, args: extraArgs };
3553
- }
3554
-
3555
- // src/services/agent.service.ts
3556
- var AgentService = class {
3557
- constructor(runtime, opts) {
3558
- this.runtime = runtime;
3559
- this.opts = opts;
3560
- this.strategyOpts = {
3561
- onData: (d3) => {
3562
- if (!this.claudeReady && d3.length > 0) {
3563
- this.claudeReady = true;
3564
- setTimeout(() => this.drainPending(), 250);
3565
- }
3566
- (opts.onData ?? (() => {
3567
- }))(d3);
3568
- },
3569
- onExit: opts.onExit
3570
- };
3571
- }
3572
- runtime;
3573
- opts;
3574
- // Strategy is selected lazily inside spawn() so we can fall back from
3575
- // ConPTY → legacy pipe at runtime if the native binding fails to load.
3576
- // Methods called before spawn() (e.g. early kill/SIGINT) no-op safely.
3577
- strategy = null;
3578
- strategyOpts;
3579
- /**
3580
- * Set once the PTY emits its FIRST batch of output — proxy for
3581
- * "Claude has rendered its input box and is ready to read keystrokes."
3582
- * Before this, remote `sendCommand`s are buffered (`pendingInputs`)
3583
- * and replayed in order on first data. Without this guard, the very
3584
- * first prompt right after `codeam pair` on Windows lands while
3585
- * Claude's React Ink tree is still mounting — the input bytes are
3586
- * accepted by the PTY but never make it to the input field, and
3587
- * the prompt silently vanishes.
3588
- */
3589
- claudeReady = false;
3590
- pendingInputs = [];
3591
- drainPending() {
3592
- if (!this.strategy || this.pendingInputs.length === 0) return;
3593
- const s = this.strategy;
3594
- log.trace("claude", `drain pending=${this.pendingInputs.length}`);
3595
- let offset = 0;
3596
- for (const text of this.pendingInputs) {
3597
- setTimeout(() => s.write(text), offset);
3598
- setTimeout(() => s.write("\r"), offset + 50);
3599
- offset += 200;
3600
- }
3601
- this.pendingInputs.length = 0;
3602
- }
3603
- async spawn() {
3604
- let launch;
3605
- try {
3606
- launch = await this.runtime.prepareLaunch();
3607
- } catch {
3608
- const installed = await ensureClaudeInstalled();
3609
- if (installed) {
3610
- try {
3611
- launch = await this.runtime.prepareLaunch();
3612
- } catch {
3613
- launch = null;
3614
- }
3615
- } else {
3616
- launch = null;
3617
- }
3618
- if (!launch) {
3619
- const cmd = process.platform === "win32" ? "irm https://claude.ai/install.ps1 | iex" : "curl -fsSL https://claude.ai/install.sh | bash";
3620
- console.error(
3621
- `
3622
- \u2717 claude is required to continue. Install it manually with:
3623
- ${cmd}
3624
- Then restart your terminal and run \`codeam pair\` again.
3625
- `
3626
- );
3627
- process.exit(1);
3628
- }
3629
- }
3630
- if (process.platform === "win32") {
3631
- log.trace("claude", `spawn (win32) cmd=${launch.cmd} args=${launch.args.join(" ")}`);
3632
- const conpty = WindowsConPtyStrategy.tryCreate(this.strategyOpts);
3633
- if (conpty) {
3634
- try {
3635
- conpty.spawn(launch.cmd, this.opts.cwd, launch.args);
3636
- this.strategy = conpty;
3637
- log.trace("claude", "ConPTY spawn ok");
3638
- return;
3639
- } catch (err) {
3640
- const msg = err instanceof Error ? err.message : String(err);
3641
- try {
3642
- conpty.dispose();
3643
- } catch {
3644
- }
3645
- console.error(`
3646
- \u26A0 ConPTY launch failed (${msg.split("\n")[0]})`);
3647
- console.error(" Falling back to pipe mode (limited interactivity)\u2026\n");
3648
- }
3649
- } else {
3650
- console.error(
3651
- '\n \u26A0 Windows: node-pty unavailable, falling back to pipe mode.\n Claude may exit with "no stdin data" / "--print" errors.\n Reinstall the CLI to fetch the prebuilt ConPTY binary, or run inside WSL.\n'
3652
- );
3653
- }
3654
- const pipe = new WindowsPtyStrategy(this.strategyOpts);
3655
- pipe.spawn(launch.cmd, this.opts.cwd, launch.args);
3656
- this.strategy = pipe;
3657
- return;
3658
- }
3659
- const unix = new UnixPtyStrategy(this.strategyOpts);
3660
- unix.spawn(launch.cmd, this.opts.cwd, launch.args);
3661
- this.strategy = unix;
3662
- }
3663
- /**
3664
- * Send a command to Claude's stdin (remote control from mobile).
3665
- *
3666
- * Why two separate writes with a delay?
3667
- * Same batching problem as selectOption: all bytes arriving in one write()
3668
- * call are processed by readline in one synchronous run. React Ink batches
3669
- * the resulting state updates, so when '\r' fires the input's value is still
3670
- * the pre-batch (empty/previous) state → Enter submits nothing and the text
3671
- * stays visible-but-unsubmitted in the input field.
3672
- *
3673
- * Sending '\r' in a separate write() ~50 ms later guarantees it arrives on
3674
- * a fresh event-loop tick, after React has flushed the text into input state.
3675
- *
3676
- * The delay scales with line count: Smart Composer outputs are ~500–1500
3677
- * chars with embedded `\n`s (Task / Context / Steps blocks). Ink's input
3678
- * field re-flows multi-line content per render and the 50 ms baseline that
3679
- * worked for single-line prompts isn't enough headroom — the text lands in
3680
- * the field but `\r` submits stale state and the prompt sits there until
3681
- * the user hits Enter manually. Adding ~40 ms per extra line (capped at
3682
- * 300 ms) keeps short prompts snappy while giving multi-line composer
3683
- * outputs the time they need to settle.
3684
- */
3685
- sendCommand(text) {
3686
- if (!this.strategy) {
3687
- log.trace("claude", "sendCommand dropped (no strategy)");
3688
- return;
3689
- }
3690
- if (!this.claudeReady) {
3691
- log.trace("claude", `sendCommand buffered (not ready) text=${text.length}B`);
3692
- this.pendingInputs.push(text);
3693
- return;
3655
+ const f = { header: h, value: "", full: "" };
3656
+ return a.push(f), { message(v, T2) {
3657
+ m(f, v, T2);
3658
+ }, error(v) {
3659
+ S2(f, { status: "error", message: v });
3660
+ }, success(v) {
3661
+ S2(f, { status: "success", message: v });
3662
+ } };
3663
+ }, error(h, f) {
3664
+ $2(true), O2.error(h, { output: i, secondarySymbol: r, spacing: 1 }), f?.showLog !== false && p2(), a.splice(1, a.length - 1), a[0].value = "", a[0].full = "";
3665
+ }, success(h, f) {
3666
+ $2(true), O2.success(h, { output: i, secondarySymbol: r, spacing: 1 }), f?.showLog === true && p2(), a.splice(1, a.length - 1), a[0].value = "", a[0].full = "";
3667
+ } };
3668
+ };
3669
+ var Ot = (e) => new at({ validate: e.validate, placeholder: e.placeholder, defaultValue: e.defaultValue, initialValue: e.initialValue, output: e.output, signal: e.signal, input: e.input, render() {
3670
+ const i = e?.withGuide ?? u.withGuide, s = `${`${i ? `${(0, import_node_util2.styleText)("gray", d2)}
3671
+ ` : ""}${V2(this.state)} `}${e.message}
3672
+ `, r = e.placeholder ? (0, import_node_util2.styleText)("inverse", e.placeholder[0]) + (0, import_node_util2.styleText)("dim", e.placeholder.slice(1)) : (0, import_node_util2.styleText)(["inverse", "hidden"], "_"), u2 = this.userInput ? this.userInputWithCursor : r, n = this.value ?? "";
3673
+ switch (this.state) {
3674
+ case "error": {
3675
+ const o = this.error ? ` ${(0, import_node_util2.styleText)("yellow", this.error)}` : "", c2 = i ? `${(0, import_node_util2.styleText)("yellow", d2)} ` : "", a = i ? (0, import_node_util2.styleText)("yellow", E2) : "";
3676
+ return `${s.trim()}
3677
+ ${c2}${u2}
3678
+ ${a}${o}
3679
+ `;
3694
3680
  }
3695
- const s = this.strategy;
3696
- log.trace("claude", `sendCommand text=${text.length}B`);
3697
- s.write(text);
3698
- const lineCount = text.split("\n").length;
3699
- const delay = Math.min(300, 50 + (lineCount - 1) * 40);
3700
- setTimeout(() => s.write("\r"), delay);
3701
- }
3702
- /**
3703
- * Navigate a React Ink selector to the given 0-based target index and confirm.
3704
- *
3705
- * `fromIndex` is the current highlighted position (defaults to 0 for
3706
- * numbered selectors which always start at the first option). For list-style
3707
- * selectors (e.g. /mcp), the CLI sends `currentIndex` in the select_prompt
3708
- * chunk so the client can pass it back here as `fromIndex`, enabling both
3709
- * up-arrow and down-arrow navigation without always rewinding to position 0.
3710
- *
3711
- * Why not sendCommand(arrows + Enter) in one write()?
3712
- * All bytes arrive as one chunk → readline fires all keypress events in the
3713
- * same synchronous run → React Ink batches the state updates → each arrow
3714
- * sees selectedIndex=0 → final state is still 0 or 1 → wrong option selected.
3715
- *
3716
- * Fix: send each arrow in a separate write(), ARROW_MS apart, so React has
3717
- * time to process and re-render between each keystroke. Enter is sent
3718
- * ENTER_MS after the last arrow.
3719
- */
3720
- selectOption(targetIndex, fromIndex = 0) {
3721
- if (!this.strategy) return;
3722
- const s = this.strategy;
3723
- const delta = targetIndex - fromIndex;
3724
- const steps = Math.abs(delta);
3725
- const arrow = delta >= 0 ? "\x1B[B" : "\x1B[A";
3726
- const ARROW_MS = 80;
3727
- const ENTER_MS = 200;
3728
- if (steps === 0) {
3729
- s.write("\r");
3730
- return;
3681
+ case "submit": {
3682
+ const o = n ? ` ${(0, import_node_util2.styleText)("dim", n)}` : "", c2 = i ? (0, import_node_util2.styleText)("gray", d2) : "";
3683
+ return `${s}${c2}${o}`;
3731
3684
  }
3732
- for (let i = 0; i < steps; i++) {
3733
- setTimeout(() => {
3734
- s.write(arrow);
3735
- }, i * ARROW_MS);
3685
+ case "cancel": {
3686
+ const o = n ? ` ${(0, import_node_util2.styleText)(["strikethrough", "dim"], n)}` : "", c2 = i ? (0, import_node_util2.styleText)("gray", d2) : "";
3687
+ return `${s}${c2}${o}${n.trim() ? `
3688
+ ${c2}` : ""}`;
3736
3689
  }
3737
- setTimeout(() => {
3738
- s.write("\r");
3739
- }, steps * ARROW_MS + ENTER_MS);
3740
- }
3741
- /**
3742
- * Write raw bytes to the PTY without any auto-appended `\r` or delay.
3743
- * Use this when the caller already owns the full input (e.g. the
3744
- * `ptyInput` returned by `RuntimeStrategy.changeModelInstruction()`
3745
- * already contains the trailing `\r`).
3746
- */
3747
- sendRawPtyInput(text) {
3748
- if (!this.strategy) {
3749
- log.trace("claude", "sendRawPtyInput dropped (no strategy)");
3750
- return;
3690
+ default: {
3691
+ const o = i ? `${(0, import_node_util2.styleText)("cyan", d2)} ` : "", c2 = i ? (0, import_node_util2.styleText)("cyan", E2) : "";
3692
+ return `${s}${o}${u2}
3693
+ ${c2}
3694
+ `;
3751
3695
  }
3752
- log.trace("claude", `sendRawPtyInput len=${text.length}`);
3753
- this.strategy.write(text);
3754
3696
  }
3755
- /** Send Escape key to Claude (cancels interactive prompts). */
3756
- sendEscape() {
3757
- this.strategy?.write("\x1B");
3697
+ } }).prompt();
3698
+
3699
+ // src/services/claude-installer.ts
3700
+ init_types();
3701
+ function probeInstallDirs() {
3702
+ const home = os5.homedir();
3703
+ if (process.platform === "win32") {
3704
+ return [
3705
+ path7.join(home, ".claude", "local"),
3706
+ path7.join(home, "AppData", "Local", "AnthropicClaude"),
3707
+ path7.join(home, "AppData", "Local", "Programs", "AnthropicClaude")
3708
+ ];
3758
3709
  }
3759
- /** Send Ctrl+C to Claude. */
3760
- interrupt() {
3761
- this.strategy?.write("");
3710
+ return [
3711
+ path7.join(home, ".local", "bin"),
3712
+ path7.join(home, ".claude", "local"),
3713
+ "/usr/local/bin"
3714
+ ];
3715
+ }
3716
+ function isAvailable() {
3717
+ return findInPath("claude") !== null || findInPath("claude-code") !== null;
3718
+ }
3719
+ function augmentPath() {
3720
+ const dirs = probeInstallDirs();
3721
+ const sep2 = path7.delimiter;
3722
+ const current = process.env.PATH ?? "";
3723
+ const existing = new Set(current.split(sep2).filter(Boolean));
3724
+ const additions = dirs.filter((d3) => !existing.has(d3));
3725
+ if (additions.length === 0) return;
3726
+ process.env.PATH = additions.join(sep2) + sep2 + current;
3727
+ }
3728
+ function runInstaller() {
3729
+ const isWindows = process.platform === "win32";
3730
+ const cmd = isWindows ? "powershell.exe" : "bash";
3731
+ const args2 = isWindows ? [
3732
+ "-NoProfile",
3733
+ "-ExecutionPolicy",
3734
+ "Bypass",
3735
+ "-Command",
3736
+ "irm https://claude.ai/install.ps1 | iex"
3737
+ ] : ["-c", "curl -fsSL https://claude.ai/install.sh | bash"];
3738
+ return new Promise((resolve3) => {
3739
+ const proc = (0, import_child_process4.spawn)(cmd, args2, { stdio: "inherit" });
3740
+ proc.on("error", (err) => {
3741
+ console.error(`
3742
+ \u2717 Installer failed to launch: ${err.message}`);
3743
+ resolve3(false);
3744
+ });
3745
+ proc.on("exit", (code) => {
3746
+ resolve3(code === 0);
3747
+ });
3748
+ });
3749
+ }
3750
+ async function ensureClaudeInstalled() {
3751
+ if (isAvailable()) return true;
3752
+ const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
3753
+ if (isInteractive) {
3754
+ const proceed = await ot2({
3755
+ message: "Claude Code is not installed on this machine. Install it now using the official installer?",
3756
+ initialValue: true
3757
+ });
3758
+ if (q(proceed) || proceed === false) return false;
3759
+ } else {
3760
+ console.log("\n Claude Code not found \u2014 running the official installer...\n");
3762
3761
  }
3763
- kill() {
3764
- this.strategy?.kill();
3762
+ console.log();
3763
+ const ok = await runInstaller();
3764
+ if (!ok) {
3765
+ console.error("\n \u2717 Claude Code installation failed. See the installer output above.");
3766
+ return false;
3765
3767
  }
3766
- /**
3767
- * Kill the current Claude process and relaunch it resuming the given session.
3768
- * Pass auto=true to add --dangerously-skip-permissions (no confirmation prompts).
3769
- *
3770
- * For agents that use CLI flags (Claude: --resume <id>), `resumeLaunchArgs`
3771
- * returns a non-empty array and we pass those directly to the binary.
3772
- * For agents that use a post-spawn PTY instruction (e.g. Codex), `resumeLaunchArgs`
3773
- * returns [] and `postSpawnInstruction` types the resume command into the PTY.
3774
- */
3775
- restart(sessionId, auto = false) {
3776
- if (!this.strategy) return;
3777
- const resumeArgs = auto ? this.runtime.resumeLaunchArgs(sessionId) : ["--resume", sessionId];
3778
- const launch = buildClaudeLaunch(resumeArgs);
3779
- if (!launch) return;
3780
- this.strategy.kill();
3781
- this.strategy.spawn(launch.cmd, this.opts.cwd, launch.args);
3782
- if (resumeArgs.length === 0 && this.runtime.postSpawnInstruction) {
3783
- const { ptyInput } = this.runtime.postSpawnInstruction(sessionId);
3784
- setTimeout(() => {
3785
- this.strategy?.write(ptyInput);
3786
- }, 500);
3787
- }
3768
+ augmentPath();
3769
+ if (!isAvailable()) {
3770
+ console.error(
3771
+ "\n \u26A0 Claude Code installed but the binary is still not on PATH for this process.\n This usually means the installer registered a new directory that only takes\n effect in a fresh shell. Restart your terminal and run `codeam pair` again."
3772
+ );
3773
+ return false;
3788
3774
  }
3789
- };
3775
+ return true;
3776
+ }
3790
3777
 
3791
3778
  // src/agents/claude/quota.ts
3792
3779
  var fs5 = __toESM(require("fs"));
@@ -4272,8 +4259,19 @@ var ClaudeRuntimeStrategy = class {
4272
4259
  id = "claude";
4273
4260
  meta = getAgent("claude");
4274
4261
  async prepareLaunch() {
4275
- const launch = buildClaudeLaunch();
4276
- if (!launch) throw new Error("claude binary not found in PATH");
4262
+ let launch = buildClaudeLaunch();
4263
+ if (!launch) {
4264
+ const installed = await ensureClaudeInstalled();
4265
+ if (installed) launch = buildClaudeLaunch();
4266
+ }
4267
+ if (!launch) {
4268
+ const cmd = process.platform === "win32" ? "irm https://claude.ai/install.ps1 | iex" : "curl -fsSL https://claude.ai/install.sh | bash";
4269
+ throw new Error(
4270
+ `Claude Code is required to continue. Install it manually with:
4271
+ ${cmd}
4272
+ Then restart your terminal and run \`codeam pair\` again.`
4273
+ );
4274
+ }
4277
4275
  return { cmd: launch.cmd, args: launch.args };
4278
4276
  }
4279
4277
  resumeLaunchArgs(sessionId) {
@@ -4537,6 +4535,7 @@ var ClaudeDeployStrategy = class {
4537
4535
 
4538
4536
  // src/agents/codex/runtime.ts
4539
4537
  var import_node_child_process = require("child_process");
4538
+ var path13 = __toESM(require("path"));
4540
4539
  init_types();
4541
4540
 
4542
4541
  // src/agents/codex/history.ts
@@ -5062,16 +5061,26 @@ var CodexRuntimeStrategy = class {
5062
5061
  id = "codex";
5063
5062
  meta = getAgent("codex");
5064
5063
  async prepareLaunch() {
5065
- const binary = findInPath("codex");
5066
- if (!binary) {
5064
+ let binary = findInPath("codex");
5065
+ if (binary) return { cmd: binary, args: [] };
5066
+ console.log("\n Codex CLI not found \u2014 installing via `npm install -g @openai/codex`...\n");
5067
+ try {
5067
5068
  await installCodexViaNpm();
5068
- const afterInstall = findInPath("codex");
5069
- if (!afterInstall) {
5070
- throw new Error(
5071
- "Could not find 'codex' on PATH after running 'npm install -g @openai/codex'. Install it manually and retry."
5072
- );
5073
- }
5074
- return { cmd: afterInstall, args: [] };
5069
+ } catch (err) {
5070
+ const msg = err instanceof Error ? err.message : String(err);
5071
+ throw new Error(
5072
+ `Failed to install Codex CLI automatically (${msg}).
5073
+ Install it manually with:
5074
+ npm install -g @openai/codex
5075
+ Then run \`codeam pair\` again.`
5076
+ );
5077
+ }
5078
+ augmentNpmGlobalBin();
5079
+ binary = findInPath("codex");
5080
+ if (!binary) {
5081
+ throw new Error(
5082
+ "Codex CLI was installed but the binary is not visible on PATH for this process.\n Restart your terminal and run `codeam pair` again."
5083
+ );
5075
5084
  }
5076
5085
  return { cmd: binary, args: [] };
5077
5086
  }
@@ -5141,9 +5150,35 @@ async function installCodexViaNpm() {
5141
5150
  if (code === 0) resolve3();
5142
5151
  else reject(new Error(`npm install -g @openai/codex exited ${code}`));
5143
5152
  });
5144
- proc.on("error", reject);
5153
+ proc.on("error", (err) => {
5154
+ const code = err.code;
5155
+ if (code === "ENOENT") {
5156
+ reject(new Error("`npm` is not on PATH. Install Node.js first, then retry."));
5157
+ } else {
5158
+ reject(err);
5159
+ }
5160
+ });
5145
5161
  });
5146
5162
  }
5163
+ function augmentNpmGlobalBin() {
5164
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
5165
+ try {
5166
+ const result = (0, import_node_child_process.spawnSync)(npm, ["prefix", "-g"], {
5167
+ stdio: ["ignore", "pipe", "ignore"]
5168
+ });
5169
+ if (result.status !== 0) return;
5170
+ const prefix = result.stdout.toString().trim();
5171
+ if (!prefix) return;
5172
+ const binDir = process.platform === "win32" ? prefix : path13.join(prefix, "bin");
5173
+ const sep2 = path13.delimiter;
5174
+ const current = process.env.PATH ?? "";
5175
+ const existing = new Set(current.split(sep2).filter(Boolean));
5176
+ if (!existing.has(binDir)) {
5177
+ process.env.PATH = binDir + sep2 + current;
5178
+ }
5179
+ } catch {
5180
+ }
5181
+ }
5147
5182
 
5148
5183
  // src/agents/codex/credentials.ts
5149
5184
  var import_node_fs3 = __toESM(require("fs"));
@@ -5772,7 +5807,7 @@ var OutputService = class _OutputService {
5772
5807
 
5773
5808
  // src/services/history.service.ts
5774
5809
  var fs11 = __toESM(require("fs"));
5775
- var path14 = __toESM(require("path"));
5810
+ var path15 = __toESM(require("path"));
5776
5811
  var os12 = __toESM(require("os"));
5777
5812
  var https4 = __toESM(require("https"));
5778
5813
  var http4 = __toESM(require("http"));
@@ -5935,7 +5970,7 @@ var HistoryService = class _HistoryService {
5935
5970
  return this._quotaPercent === null || Date.now() - this._quotaFetchedAt > ttlMs;
5936
5971
  }
5937
5972
  get projectDir() {
5938
- return this.runtime.resolveHistoryDir(this.cwd) ?? path14.join(os12.homedir(), ".claude", "projects", encodeCwd(this.cwd));
5973
+ return this.runtime.resolveHistoryDir(this.cwd) ?? path15.join(os12.homedir(), ".claude", "projects", encodeCwd(this.cwd));
5939
5974
  }
5940
5975
  /** Set the current Claude conversation ID (extracted from /cost command or session start) */
5941
5976
  setCurrentConversationId(id) {
@@ -5947,7 +5982,7 @@ var HistoryService = class _HistoryService {
5947
5982
  /** Return the current message count in the active conversation. */
5948
5983
  getCurrentMessageCount() {
5949
5984
  if (!this.currentConversationId) return 0;
5950
- const filePath = path14.join(this.projectDir, `${this.currentConversationId}.jsonl`);
5985
+ const filePath = path15.join(this.projectDir, `${this.currentConversationId}.jsonl`);
5951
5986
  return parseJsonl(filePath).length;
5952
5987
  }
5953
5988
  /**
@@ -5958,7 +5993,7 @@ var HistoryService = class _HistoryService {
5958
5993
  const deadline = Date.now() + timeoutMs;
5959
5994
  while (Date.now() < deadline) {
5960
5995
  if (!this.currentConversationId) return null;
5961
- const filePath = path14.join(this.projectDir, `${this.currentConversationId}.jsonl`);
5996
+ const filePath = path15.join(this.projectDir, `${this.currentConversationId}.jsonl`);
5962
5997
  const messages = parseJsonl(filePath);
5963
5998
  if (messages.length > previousCount) {
5964
5999
  for (let i = messages.length - 1; i >= previousCount; i--) {
@@ -5986,14 +6021,14 @@ var HistoryService = class _HistoryService {
5986
6021
  try {
5987
6022
  const files = fs11.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => {
5988
6023
  try {
5989
- const stat3 = fs11.statSync(path14.join(dir, e.name));
6024
+ const stat3 = fs11.statSync(path15.join(dir, e.name));
5990
6025
  return { name: e.name, mtime: stat3.mtimeMs, birthtime: stat3.birthtimeMs };
5991
6026
  } catch {
5992
6027
  return { name: e.name, mtime: 0, birthtime: 0 };
5993
6028
  }
5994
6029
  }).filter((f) => f.birthtime >= cutoff).sort((a, b) => b.mtime - a.mtime);
5995
6030
  if (files.length > 0) {
5996
- this.currentConversationId = path14.basename(files[0].name, ".jsonl");
6031
+ this.currentConversationId = path15.basename(files[0].name, ".jsonl");
5997
6032
  }
5998
6033
  } catch {
5999
6034
  }
@@ -6033,7 +6068,7 @@ var HistoryService = class _HistoryService {
6033
6068
  }
6034
6069
  const files = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => {
6035
6070
  try {
6036
- const stat3 = fs11.statSync(path14.join(dir, e.name));
6071
+ const stat3 = fs11.statSync(path15.join(dir, e.name));
6037
6072
  return { name: e.name, mtime: stat3.mtimeMs, birthtime: stat3.birthtimeMs };
6038
6073
  } catch {
6039
6074
  return { name: e.name, mtime: 0, birthtime: 0 };
@@ -6042,7 +6077,7 @@ var HistoryService = class _HistoryService {
6042
6077
  if (files.length === 0) return null;
6043
6078
  const targetFile = this.currentConversationId ? `${this.currentConversationId}.jsonl` : files[0].name;
6044
6079
  if (!files.some((f) => f.name === targetFile)) return null;
6045
- return this.extractUsageFromFile(path14.join(dir, targetFile));
6080
+ return this.extractUsageFromFile(path15.join(dir, targetFile));
6046
6081
  }
6047
6082
  extractUsageFromFile(filePath) {
6048
6083
  let raw;
@@ -6094,7 +6129,7 @@ var HistoryService = class _HistoryService {
6094
6129
  try {
6095
6130
  files = fs11.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).filter((f) => {
6096
6131
  try {
6097
- return fs11.statSync(path14.join(projectDir, f)).mtimeMs >= monthStartMs;
6132
+ return fs11.statSync(path15.join(projectDir, f)).mtimeMs >= monthStartMs;
6098
6133
  } catch {
6099
6134
  return false;
6100
6135
  }
@@ -6105,7 +6140,7 @@ var HistoryService = class _HistoryService {
6105
6140
  for (const file of files) {
6106
6141
  let raw;
6107
6142
  try {
6108
- raw = fs11.readFileSync(path14.join(projectDir, file), "utf8");
6143
+ raw = fs11.readFileSync(path15.join(projectDir, file), "utf8");
6109
6144
  } catch {
6110
6145
  continue;
6111
6146
  }
@@ -6147,8 +6182,8 @@ var HistoryService = class _HistoryService {
6147
6182
  const sessions3 = [];
6148
6183
  for (const entry of entries) {
6149
6184
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
6150
- const id = path14.basename(entry.name, ".jsonl");
6151
- const filePath = path14.join(dir, entry.name);
6185
+ const id = path15.basename(entry.name, ".jsonl");
6186
+ const filePath = path15.join(dir, entry.name);
6152
6187
  let mtime = Date.now();
6153
6188
  try {
6154
6189
  mtime = fs11.statSync(filePath).mtimeMs;
@@ -6189,7 +6224,7 @@ var HistoryService = class _HistoryService {
6189
6224
  * showing an empty conversation.
6190
6225
  */
6191
6226
  async loadConversation(sessionId) {
6192
- const filePath = path14.join(this.projectDir, `${sessionId}.jsonl`);
6227
+ const filePath = path15.join(this.projectDir, `${sessionId}.jsonl`);
6193
6228
  const messages = parseJsonl(filePath);
6194
6229
  if (messages.length === 0) return;
6195
6230
  const totalBatches = Math.ceil(messages.length / CONVERSATION_BATCH_SIZE);
@@ -6232,7 +6267,7 @@ var HistoryService = class _HistoryService {
6232
6267
  if (!this.currentConversationId) return 0;
6233
6268
  }
6234
6269
  const sessionId = this.currentConversationId;
6235
- const filePath = path14.join(this.projectDir, `${sessionId}.jsonl`);
6270
+ const filePath = path15.join(this.projectDir, `${sessionId}.jsonl`);
6236
6271
  const messages = parseJsonl(filePath);
6237
6272
  if (messages.length === 0) return 0;
6238
6273
  const marker = this.lastUploadedUuid.get(sessionId);
@@ -6263,7 +6298,7 @@ var HistoryService = class _HistoryService {
6263
6298
  // src/services/file-watcher.service.ts
6264
6299
  var import_child_process7 = require("child_process");
6265
6300
  var os13 = __toESM(require("os"));
6266
- var path15 = __toESM(require("path"));
6301
+ var path16 = __toESM(require("path"));
6267
6302
 
6268
6303
  // src/services/file-watcher/diff-parser.ts
6269
6304
  var HUNK_HEADER_RE = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
@@ -6587,7 +6622,7 @@ var FileWatcherService = class {
6587
6622
  }
6588
6623
  async emitForFile(absPath, changeType) {
6589
6624
  if (this.stopped) return;
6590
- const relPath = path15.relative(this.opts.workingDir, absPath);
6625
+ const relPath = path16.relative(this.opts.workingDir, absPath);
6591
6626
  if (!relPath || relPath.startsWith("..")) {
6592
6627
  return;
6593
6628
  }
@@ -7077,7 +7112,7 @@ var StreamingEmitterService = class {
7077
7112
  const questionId = this.pendingAnswer.questionId;
7078
7113
  try {
7079
7114
  const { statusCode, body } = await _transport4.get(
7080
- `${this.apiBase}/api/sessions/${encodeURIComponent(this.opts.sessionId)}/pending-answer?questionId=${encodeURIComponent(questionId)}`,
7115
+ `${this.apiBase}/api/sessions/${encodeURIComponent(this.opts.sessionId)}/pending-answer?questionId=${encodeURIComponent(questionId)}&pluginId=${encodeURIComponent(this.opts.pluginId)}`,
7081
7116
  this.headers
7082
7117
  );
7083
7118
  if (statusCode === 204 || statusCode === 404) {
@@ -7130,7 +7165,11 @@ var StreamingEmitterService = class {
7130
7165
  );
7131
7166
  }
7132
7167
  async postWithRetries(url, body) {
7133
- const payload = JSON.stringify(body);
7168
+ const payload = JSON.stringify({
7169
+ sessionId: this.opts.sessionId,
7170
+ pluginId: this.opts.pluginId,
7171
+ ...body
7172
+ });
7134
7173
  for (let attempt = 0; attempt <= MAX_RETRIES2; attempt += 1) {
7135
7174
  try {
7136
7175
  const { statusCode, body: resBody } = await _transport4.post(url, this.headers, payload);
@@ -7262,7 +7301,7 @@ function buildKeepAlive(ctx) {
7262
7301
  // src/commands/start/handlers.ts
7263
7302
  var fs14 = __toESM(require("fs"));
7264
7303
  var os14 = __toESM(require("os"));
7265
- var path19 = __toESM(require("path"));
7304
+ var path20 = __toESM(require("path"));
7266
7305
  var import_crypto3 = require("crypto");
7267
7306
  var import_child_process11 = require("child_process");
7268
7307
 
@@ -7321,7 +7360,7 @@ function parsePayload(schema, raw) {
7321
7360
 
7322
7361
  // src/services/file-ops.service.ts
7323
7362
  var fs12 = __toESM(require("fs/promises"));
7324
- var path16 = __toESM(require("path"));
7363
+ var path17 = __toESM(require("path"));
7325
7364
  var MAX_FILE_BYTES = 5 * 1024 * 1024;
7326
7365
  var MAX_WALK_DEPTH = 6;
7327
7366
  var MAX_VISITED_DIRS = 5e3;
@@ -7356,8 +7395,8 @@ var SUBDIR_IGNORE = /* @__PURE__ */ new Set([
7356
7395
  "__pycache__"
7357
7396
  ]);
7358
7397
  function isUnder(parent, candidate) {
7359
- const rel = path16.relative(parent, candidate);
7360
- return rel === "" || !rel.startsWith("..") && !path16.isAbsolute(rel);
7398
+ const rel = path17.relative(parent, candidate);
7399
+ return rel === "" || !rel.startsWith("..") && !path17.isAbsolute(rel);
7361
7400
  }
7362
7401
  async function isExistingFile(absPath) {
7363
7402
  try {
@@ -7380,7 +7419,7 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
7380
7419
  }
7381
7420
  for (const e of entries) {
7382
7421
  if (!e.isFile()) continue;
7383
- const full = path16.join(dir, e.name);
7422
+ const full = path17.join(dir, e.name);
7384
7423
  if (needleVariants.some((needle) => full.endsWith(needle))) {
7385
7424
  ctx.matches.push(full);
7386
7425
  if (ctx.matches.length >= ctx.cap) return;
@@ -7390,21 +7429,21 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
7390
7429
  if (!e.isDirectory()) continue;
7391
7430
  if (SUBDIR_IGNORE.has(e.name)) continue;
7392
7431
  if (e.name.startsWith(".") && SUBDIR_IGNORE.has(e.name)) continue;
7393
- await walkForSuffix(path16.join(dir, e.name), needleVariants, depth + 1, ctx);
7432
+ await walkForSuffix(path17.join(dir, e.name), needleVariants, depth + 1, ctx);
7394
7433
  if (ctx.matches.length >= ctx.cap) return;
7395
7434
  }
7396
7435
  }
7397
7436
  async function findFile(rawPath) {
7398
7437
  const cwd = process.cwd();
7399
- if (path16.isAbsolute(rawPath)) {
7400
- const abs = path16.normalize(rawPath);
7438
+ if (path17.isAbsolute(rawPath)) {
7439
+ const abs = path17.normalize(rawPath);
7401
7440
  if (isUnder(cwd, abs) && await isExistingFile(abs)) return abs;
7402
7441
  }
7403
- const direct = path16.resolve(cwd, rawPath);
7442
+ const direct = path17.resolve(cwd, rawPath);
7404
7443
  if (isUnder(cwd, direct) && await isExistingFile(direct)) return direct;
7405
- const normalized = path16.normalize(rawPath).replace(/^[./\\]+/, "");
7444
+ const normalized = path17.normalize(rawPath).replace(/^[./\\]+/, "");
7406
7445
  const needles = [
7407
- `${path16.sep}${normalized}`,
7446
+ `${path17.sep}${normalized}`,
7408
7447
  `/${normalized}`
7409
7448
  ].filter((v, i, a) => a.indexOf(v) === i);
7410
7449
  const ctx = { visited: 0, matches: [], cap: 16 };
@@ -7418,7 +7457,7 @@ async function findWriteTarget(rawPath) {
7418
7457
  const found = await findFile(rawPath);
7419
7458
  if (found) return found;
7420
7459
  const cwd = process.cwd();
7421
- const fallback = path16.isAbsolute(rawPath) ? path16.normalize(rawPath) : path16.resolve(cwd, rawPath);
7460
+ const fallback = path17.isAbsolute(rawPath) ? path17.normalize(rawPath) : path17.resolve(cwd, rawPath);
7422
7461
  if (!isUnder(cwd, fallback)) return null;
7423
7462
  return fallback;
7424
7463
  }
@@ -7458,7 +7497,7 @@ async function writeProjectFile(rawPath, content) {
7458
7497
  if (Buffer.byteLength(content, "utf-8") > MAX_FILE_BYTES) {
7459
7498
  return { error: "Content too large." };
7460
7499
  }
7461
- await fs12.mkdir(path16.dirname(abs), { recursive: true });
7500
+ await fs12.mkdir(path17.dirname(abs), { recursive: true });
7462
7501
  await fs12.writeFile(abs, content, "utf-8");
7463
7502
  return { ok: true };
7464
7503
  } catch (e) {
@@ -7471,7 +7510,7 @@ async function writeProjectFile(rawPath, content) {
7471
7510
  var import_child_process9 = require("child_process");
7472
7511
  var import_util2 = require("util");
7473
7512
  var fs13 = __toESM(require("fs/promises"));
7474
- var path17 = __toESM(require("path"));
7513
+ var path18 = __toESM(require("path"));
7475
7514
  var execFileP2 = (0, import_util2.promisify)(import_child_process9.execFile);
7476
7515
  var PROJECT_IGNORE = /* @__PURE__ */ new Set([
7477
7516
  "node_modules",
@@ -7529,12 +7568,12 @@ async function listProjectFiles(opts = {}) {
7529
7568
  return;
7530
7569
  }
7531
7570
  if (PROJECT_IGNORE.has(e.name)) continue;
7532
- const full = path17.join(dir, e.name);
7571
+ const full = path18.join(dir, e.name);
7533
7572
  if (e.isDirectory()) {
7534
7573
  if (depth >= 12) continue;
7535
7574
  await walk(full, depth + 1);
7536
7575
  } else if (e.isFile()) {
7537
- const rel = path17.relative(root, full);
7576
+ const rel = path18.relative(root, full);
7538
7577
  if (q2 && !rel.toLowerCase().includes(q2) && !e.name.toLowerCase().includes(q2)) {
7539
7578
  continue;
7540
7579
  }
@@ -7642,7 +7681,7 @@ async function gitStatus(cwd) {
7642
7681
  let hasMergeInProgress = false;
7643
7682
  try {
7644
7683
  const gitDir = (await git(["rev-parse", "--git-dir"], root)).stdout.trim();
7645
- const mergeHead = path17.isAbsolute(gitDir) ? path17.join(gitDir, "MERGE_HEAD") : path17.join(root, gitDir, "MERGE_HEAD");
7684
+ const mergeHead = path18.isAbsolute(gitDir) ? path18.join(gitDir, "MERGE_HEAD") : path18.join(root, gitDir, "MERGE_HEAD");
7646
7685
  await fs13.access(mergeHead);
7647
7686
  hasMergeInProgress = true;
7648
7687
  } catch {
@@ -7789,7 +7828,7 @@ async function jsSearchFiles(opts, cwd, cap) {
7789
7828
  }
7790
7829
  let content = "";
7791
7830
  try {
7792
- content = await fs13.readFile(path17.join(cwd, f.path), "utf8");
7831
+ content = await fs13.readFile(path18.join(cwd, f.path), "utf8");
7793
7832
  } catch {
7794
7833
  continue;
7795
7834
  }
@@ -8069,7 +8108,7 @@ function closeTerminal(sessionId) {
8069
8108
  function saveFilesTemp(files) {
8070
8109
  return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
8071
8110
  const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
8072
- const tmpPath = path19.join(os14.tmpdir(), `codeam-${(0, import_crypto3.randomUUID)()}-${safeName}`);
8111
+ const tmpPath = path20.join(os14.tmpdir(), `codeam-${(0, import_crypto3.randomUUID)()}-${safeName}`);
8073
8112
  fs14.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
8074
8113
  return tmpPath;
8075
8114
  });
@@ -8690,12 +8729,12 @@ function readTokenFromArgs(args2) {
8690
8729
  }
8691
8730
  const fileFlag = args2.find((a) => a.startsWith("--token-file="));
8692
8731
  if (fileFlag) {
8693
- const path28 = fileFlag.slice("--token-file=".length);
8732
+ const path29 = fileFlag.slice("--token-file=".length);
8694
8733
  try {
8695
- const content = fs15.readFileSync(path28, "utf8").trim();
8696
- if (content.length === 0) fail(`--token-file ${path28} is empty`);
8734
+ const content = fs15.readFileSync(path29, "utf8").trim();
8735
+ if (content.length === 0) fail(`--token-file ${path29} is empty`);
8697
8736
  try {
8698
- fs15.unlinkSync(path28);
8737
+ fs15.unlinkSync(path29);
8699
8738
  } catch {
8700
8739
  }
8701
8740
  return content;
@@ -8869,7 +8908,7 @@ var import_picocolors9 = __toESM(require("picocolors"));
8869
8908
  var import_child_process12 = require("child_process");
8870
8909
  var import_util3 = require("util");
8871
8910
  var import_picocolors7 = __toESM(require("picocolors"));
8872
- var path20 = __toESM(require("path"));
8911
+ var path21 = __toESM(require("path"));
8873
8912
  var execFileP3 = (0, import_util3.promisify)(import_child_process12.execFile);
8874
8913
  var MAX_BUFFER = 8 * 1024 * 1024;
8875
8914
  function resetStdinForChild() {
@@ -9358,7 +9397,7 @@ var GitHubCodespacesProvider = class {
9358
9397
  });
9359
9398
  }
9360
9399
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
9361
- const remoteDir = path20.posix.dirname(remotePath);
9400
+ const remoteDir = path21.posix.dirname(remotePath);
9362
9401
  const parts = [
9363
9402
  `mkdir -p ${shellQuote(remoteDir)}`,
9364
9403
  `cat > ${shellQuote(remotePath)}`
@@ -9428,7 +9467,7 @@ function shellQuote(s) {
9428
9467
  // src/services/providers/gitpod.ts
9429
9468
  var import_child_process13 = require("child_process");
9430
9469
  var import_util4 = require("util");
9431
- var path21 = __toESM(require("path"));
9470
+ var path22 = __toESM(require("path"));
9432
9471
  var import_picocolors8 = __toESM(require("picocolors"));
9433
9472
  var execFileP4 = (0, import_util4.promisify)(import_child_process13.execFile);
9434
9473
  var MAX_BUFFER2 = 8 * 1024 * 1024;
@@ -9668,7 +9707,7 @@ var GitpodProvider = class {
9668
9707
  });
9669
9708
  }
9670
9709
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
9671
- const remoteDir = path21.posix.dirname(remotePath);
9710
+ const remoteDir = path22.posix.dirname(remotePath);
9672
9711
  const parts = [
9673
9712
  `mkdir -p ${shellQuote2(remoteDir)}`,
9674
9713
  `cat > ${shellQuote2(remotePath)}`
@@ -9704,7 +9743,7 @@ function shellQuote2(s) {
9704
9743
  // src/services/providers/gitlab-workspaces.ts
9705
9744
  var import_child_process14 = require("child_process");
9706
9745
  var import_util5 = require("util");
9707
- var path22 = __toESM(require("path"));
9746
+ var path23 = __toESM(require("path"));
9708
9747
  var execFileP5 = (0, import_util5.promisify)(import_child_process14.execFile);
9709
9748
  var MAX_BUFFER3 = 8 * 1024 * 1024;
9710
9749
  var GITLAB_API_BASE = process.env.CODEAM_GITLAB_API_URL ?? "https://gitlab.com/api/v4";
@@ -9964,7 +10003,7 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
9964
10003
  }
9965
10004
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
9966
10005
  const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
9967
- const remoteDir = path22.posix.dirname(remotePath);
10006
+ const remoteDir = path23.posix.dirname(remotePath);
9968
10007
  const parts = [`mkdir -p ${shellQuote3(remoteDir)}`, `cat > ${shellQuote3(remotePath)}`];
9969
10008
  if (options.mode != null) {
9970
10009
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote3(remotePath)}`);
@@ -10032,7 +10071,7 @@ function shellQuote3(s) {
10032
10071
  // src/services/providers/railway.ts
10033
10072
  var import_child_process15 = require("child_process");
10034
10073
  var import_util6 = require("util");
10035
- var path23 = __toESM(require("path"));
10074
+ var path24 = __toESM(require("path"));
10036
10075
  var execFileP6 = (0, import_util6.promisify)(import_child_process15.execFile);
10037
10076
  var MAX_BUFFER4 = 8 * 1024 * 1024;
10038
10077
  function resetStdinForChild4() {
@@ -10268,7 +10307,7 @@ var RailwayProvider = class {
10268
10307
  if (!projectId || !serviceId) {
10269
10308
  throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
10270
10309
  }
10271
- const remoteDir = path23.posix.dirname(remotePath);
10310
+ const remoteDir = path24.posix.dirname(remotePath);
10272
10311
  const parts = [`mkdir -p ${shellQuote4(remoteDir)}`, `cat > ${shellQuote4(remotePath)}`];
10273
10312
  if (options.mode != null) {
10274
10313
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote4(remotePath)}`);
@@ -10811,7 +10850,7 @@ async function stopWorkspaceFromLocal(target) {
10811
10850
  var import_node_child_process3 = require("child_process");
10812
10851
  var import_node_crypto = require("crypto");
10813
10852
  var fs18 = __toESM(require("fs"));
10814
- var path26 = __toESM(require("path"));
10853
+ var path27 = __toESM(require("path"));
10815
10854
  var import_chokidar = __toESM(require("chokidar"));
10816
10855
  var import_picocolors11 = __toESM(require("picocolors"));
10817
10856
 
@@ -10819,7 +10858,7 @@ var import_picocolors11 = __toESM(require("picocolors"));
10819
10858
  var import_node_child_process2 = require("child_process");
10820
10859
  var fs16 = __toESM(require("fs"));
10821
10860
  var os16 = __toESM(require("os"));
10822
- var path24 = __toESM(require("path"));
10861
+ var path25 = __toESM(require("path"));
10823
10862
  var import_node_util3 = require("util");
10824
10863
  var execFileP7 = (0, import_node_util3.promisify)(import_node_child_process2.execFile);
10825
10864
  var KEYCHAIN_SERVICE_NAMES = [
@@ -10831,8 +10870,8 @@ var KEYCHAIN_SERVICE_NAMES = [
10831
10870
  function claudeCredentialsPaths() {
10832
10871
  const home = os16.homedir();
10833
10872
  return [
10834
- path24.join(home, ".claude", ".credentials.json"),
10835
- path24.join(home, ".config", "claude", ".credentials.json")
10873
+ path25.join(home, ".claude", ".credentials.json"),
10874
+ path25.join(home, ".config", "claude", ".credentials.json")
10836
10875
  ];
10837
10876
  }
10838
10877
  async function extractLocalClaudeToken() {
@@ -10865,9 +10904,9 @@ async function extractLocalClaudeToken() {
10865
10904
  // src/agents/codex/local-token.ts
10866
10905
  var fs17 = __toESM(require("fs"));
10867
10906
  var os17 = __toESM(require("os"));
10868
- var path25 = __toESM(require("path"));
10907
+ var path26 = __toESM(require("path"));
10869
10908
  function codexCredentialsPath() {
10870
- return path25.join(os17.homedir(), ".codex", "auth.json");
10909
+ return path26.join(os17.homedir(), ".codex", "auth.json");
10871
10910
  }
10872
10911
  async function extractLocalCodexToken() {
10873
10912
  const file = codexCredentialsPath();
@@ -11021,7 +11060,7 @@ async function link(args2 = []) {
11021
11060
  return;
11022
11061
  }
11023
11062
  if (parsed.tokenFile) {
11024
- const credential = fs18.readFileSync(path26.resolve(parsed.tokenFile), "utf8").trim();
11063
+ const credential = fs18.readFileSync(path27.resolve(parsed.tokenFile), "utf8").trim();
11025
11064
  if (!credential) {
11026
11065
  showError(`--token-file ${parsed.tokenFile} is empty.`);
11027
11066
  process.exit(1);
@@ -11208,7 +11247,7 @@ async function linkDryRunPreflight(meta) {
11208
11247
  // src/commands/version.ts
11209
11248
  var import_picocolors12 = __toESM(require("picocolors"));
11210
11249
  function version() {
11211
- const v = true ? "2.17.0" : "unknown";
11250
+ const v = true ? "2.17.2" : "unknown";
11212
11251
  console.log(`${import_picocolors12.default.bold("codeam-cli")} ${import_picocolors12.default.cyan(v)}`);
11213
11252
  }
11214
11253
 
@@ -11314,7 +11353,7 @@ var _subcommandHelpKeys = Object.keys(HELPS);
11314
11353
  // src/lib/updateNotifier.ts
11315
11354
  var fs19 = __toESM(require("fs"));
11316
11355
  var os18 = __toESM(require("os"));
11317
- var path27 = __toESM(require("path"));
11356
+ var path28 = __toESM(require("path"));
11318
11357
  var https7 = __toESM(require("https"));
11319
11358
  var import_picocolors15 = __toESM(require("picocolors"));
11320
11359
  var PKG_NAME = "codeam-cli";
@@ -11322,8 +11361,8 @@ var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
11322
11361
  var TTL_MS = 24 * 60 * 60 * 1e3;
11323
11362
  var REQUEST_TIMEOUT_MS = 1500;
11324
11363
  function cachePath() {
11325
- const dir = path27.join(os18.homedir(), ".codeam");
11326
- return path27.join(dir, "update-check.json");
11364
+ const dir = path28.join(os18.homedir(), ".codeam");
11365
+ return path28.join(dir, "update-check.json");
11327
11366
  }
11328
11367
  function readCache() {
11329
11368
  try {
@@ -11338,7 +11377,7 @@ function readCache() {
11338
11377
  function writeCache(cache) {
11339
11378
  try {
11340
11379
  const file = cachePath();
11341
- fs19.mkdirSync(path27.dirname(file), { recursive: true });
11380
+ fs19.mkdirSync(path28.dirname(file), { recursive: true });
11342
11381
  fs19.writeFileSync(file, JSON.stringify(cache));
11343
11382
  } catch {
11344
11383
  }
@@ -11410,7 +11449,7 @@ function checkForUpdates() {
11410
11449
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
11411
11450
  if (process.env.CI) return;
11412
11451
  if (!process.stdout.isTTY) return;
11413
- const current = true ? "2.17.0" : null;
11452
+ const current = true ? "2.17.2" : null;
11414
11453
  if (!current) return;
11415
11454
  const cache = readCache();
11416
11455
  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.17.0",
3
+ "version": "2.17.2",
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",