codeam-cli 2.4.39 → 2.5.1

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 +6 -0
  2. package/dist/index.js +1196 -1069
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -519,9 +519,9 @@ var require_windowsPtyAgent = __commonJS({
519
519
  "use strict";
520
520
  Object.defineProperty(exports2, "__esModule", { value: true });
521
521
  exports2.argsToCommandLine = exports2.WindowsPtyAgent = void 0;
522
- var fs11 = require("fs");
523
- var os10 = require("os");
524
- var path18 = require("path");
522
+ var fs12 = require("fs");
523
+ var os11 = require("os");
524
+ var path19 = require("path");
525
525
  var child_process_1 = require("child_process");
526
526
  var net_1 = require("net");
527
527
  var windowsConoutConnection_1 = require_windowsConoutConnection();
@@ -557,7 +557,7 @@ var require_windowsPtyAgent = __commonJS({
557
557
  }
558
558
  }
559
559
  this._ptyNative = this._useConpty ? conptyNative : winptyNative;
560
- cwd = path18.resolve(cwd);
560
+ cwd = path19.resolve(cwd);
561
561
  var commandLine = argsToCommandLine(file, args2);
562
562
  var term;
563
563
  if (this._useConpty) {
@@ -578,7 +578,7 @@ var require_windowsPtyAgent = __commonJS({
578
578
  this._outSocket.on("connect", function() {
579
579
  _this._outSocket.emit("ready_datapipe");
580
580
  });
581
- var inSocketFD = fs11.openSync(term.conin, "w");
581
+ var inSocketFD = fs12.openSync(term.conin, "w");
582
582
  this._inSocket = new net_1.Socket({
583
583
  fd: inSocketFD,
584
584
  readable: false,
@@ -679,7 +679,7 @@ var require_windowsPtyAgent = __commonJS({
679
679
  WindowsPtyAgent2.prototype._getConsoleProcessList = function() {
680
680
  var _this = this;
681
681
  return new Promise(function(resolve2) {
682
- var agent = child_process_1.fork(path18.join(__dirname, "conpty_console_list_agent"), [_this._innerPid.toString()]);
682
+ var agent = child_process_1.fork(path19.join(__dirname, "conpty_console_list_agent"), [_this._innerPid.toString()]);
683
683
  agent.on("message", function(message) {
684
684
  clearTimeout(timeout);
685
685
  resolve2(message.consoleProcessList);
@@ -702,7 +702,7 @@ var require_windowsPtyAgent = __commonJS({
702
702
  configurable: true
703
703
  });
704
704
  WindowsPtyAgent2.prototype._getWindowsBuildNumber = function() {
705
- var osVersion = /(\d+)\.(\d+)\.(\d+)/g.exec(os10.release());
705
+ var osVersion = /(\d+)\.(\d+)\.(\d+)/g.exec(os11.release());
706
706
  var buildNumber = 0;
707
707
  if (osVersion && osVersion.length === 4) {
708
708
  buildNumber = parseInt(osVersion[3]);
@@ -1012,15 +1012,15 @@ var require_unixTerminal = __commonJS({
1012
1012
  })();
1013
1013
  Object.defineProperty(exports2, "__esModule", { value: true });
1014
1014
  exports2.UnixTerminal = void 0;
1015
- var fs11 = require("fs");
1016
- var path18 = require("path");
1015
+ var fs12 = require("fs");
1016
+ var path19 = require("path");
1017
1017
  var tty = require("tty");
1018
1018
  var terminal_1 = require_terminal();
1019
1019
  var utils_1 = require_utils();
1020
1020
  var native = utils_1.loadNativeModule("pty");
1021
1021
  var pty = native.module;
1022
1022
  var helperPath = native.dir + "/spawn-helper";
1023
- helperPath = path18.resolve(__dirname, helperPath);
1023
+ helperPath = path19.resolve(__dirname, helperPath);
1024
1024
  helperPath = helperPath.replace("app.asar", "app.asar.unpacked");
1025
1025
  helperPath = helperPath.replace("node_modules.asar", "node_modules.asar.unpacked");
1026
1026
  var DEFAULT_FILE = "sh";
@@ -1275,7 +1275,7 @@ var require_unixTerminal = __commonJS({
1275
1275
  return;
1276
1276
  }
1277
1277
  var task = this._writeQueue[0];
1278
- fs11.write(this._fd, task.buffer, task.offset, function(err, written) {
1278
+ fs12.write(this._fd, task.buffer, task.offset, function(err, written) {
1279
1279
  if (err) {
1280
1280
  if ("code" in err && err.code === "EAGAIN") {
1281
1281
  _this._writeImmediate = setImmediate(function() {
@@ -1313,10 +1313,10 @@ var require_lib = __commonJS({
1313
1313
  } else {
1314
1314
  terminalCtor = require_unixTerminal().UnixTerminal;
1315
1315
  }
1316
- function spawn8(file, args2, opt) {
1316
+ function spawn10(file, args2, opt) {
1317
1317
  return new terminalCtor(file, args2, opt);
1318
1318
  }
1319
- exports2.spawn = spawn8;
1319
+ exports2.spawn = spawn10;
1320
1320
  function fork(file, args2, opt) {
1321
1321
  return new terminalCtor(file, args2, opt);
1322
1322
  }
@@ -1390,11 +1390,6 @@ var require_src = __commonJS({
1390
1390
  });
1391
1391
 
1392
1392
  // src/commands/start.ts
1393
- var fs8 = __toESM(require("fs"));
1394
- var os7 = __toESM(require("os"));
1395
- var path11 = __toESM(require("path"));
1396
- var import_crypto = require("crypto");
1397
- var import_child_process5 = require("child_process");
1398
1393
  var import_picocolors2 = __toESM(require("picocolors"));
1399
1394
 
1400
1395
  // src/config.ts
@@ -1482,7 +1477,7 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
1482
1477
  // package.json
1483
1478
  var package_default = {
1484
1479
  name: "codeam-cli",
1485
- version: "2.4.39",
1480
+ version: "2.5.1",
1486
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.",
1487
1482
  type: "commonjs",
1488
1483
  main: "dist/index.js",
@@ -1601,8 +1596,14 @@ function showPairingCode(code, expiresAt) {
1601
1596
  console.log("");
1602
1597
  }
1603
1598
 
1604
- // src/services/websocket.service.ts
1605
- var import_ws = __toESM(require("ws"));
1599
+ // src/services/command-relay.service.ts
1600
+ var https2 = __toESM(require("https"));
1601
+ var http2 = __toESM(require("http"));
1602
+
1603
+ // src/services/pairing.service.ts
1604
+ var https = __toESM(require("https"));
1605
+ var http = __toESM(require("http"));
1606
+ var os2 = __toESM(require("os"));
1606
1607
 
1607
1608
  // src/lib/poll-delay.ts
1608
1609
  var MAX_DELAY_MS = 3e4;
@@ -1612,175 +1613,17 @@ function computePollDelay({ baseMs, failures }) {
1612
1613
  return Math.round(jitter);
1613
1614
  }
1614
1615
 
1615
- // src/services/logger.ts
1616
- var fs2 = __toESM(require("fs"));
1617
- var os2 = __toESM(require("os"));
1618
- var path2 = __toESM(require("path"));
1619
- var LEVELS = { silent: 0, error: 1, warn: 2, info: 3, debug: 4, trace: 5 };
1620
- function currentLevel() {
1621
- if (process.env.CODEAM_DEBUG === "1") return LEVELS.trace;
1622
- const raw = (process.env.CODEAM_LOG ?? "error").toLowerCase();
1623
- return LEVELS[raw] ?? LEVELS.error;
1624
- }
1625
- var fileEnabled = process.env.CODEAM_DEBUG === "1" || process.env.CODEAM_LOG === "debug" || process.env.CODEAM_LOG === "trace";
1626
- var debugFilePath = path2.join(os2.homedir(), ".codeam", "debug.log");
1627
- var fileInitialized = false;
1628
- function appendToFile(line) {
1629
- if (!fileEnabled) return;
1630
- try {
1631
- if (!fileInitialized) {
1632
- fs2.mkdirSync(path2.dirname(debugFilePath), { recursive: true });
1633
- fs2.writeFileSync(
1634
- debugFilePath,
1635
- `=== codeam debug log \u2014 pid ${process.pid} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()} ===
1636
- platform=${process.platform} node=${process.version} cwd=${process.cwd()}
1637
-
1638
- `
1639
- );
1640
- fileInitialized = true;
1641
- }
1642
- fs2.appendFileSync(debugFilePath, line);
1643
- } catch {
1644
- }
1645
- }
1646
- function emit(level, tag, msg, err) {
1647
- if (LEVELS[level] > currentLevel()) return;
1648
- const detail = err instanceof Error ? `: ${err.message}` : err !== void 0 ? `: ${String(err)}` : "";
1649
- const line = `[codeam:${level}] ${tag} \u2014 ${msg}${detail}
1650
- `;
1651
- process.stderr.write(line);
1652
- if (LEVELS[level] >= LEVELS.debug) {
1653
- appendToFile(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}`);
1654
- }
1655
- }
1656
- var log = {
1657
- error: (tag, msg, err) => emit("error", tag, msg, err),
1658
- warn: (tag, msg, err) => emit("warn", tag, msg, err),
1659
- info: (tag, msg, err) => emit("info", tag, msg, err),
1660
- debug: (tag, msg, err) => emit("debug", tag, msg, err),
1661
- /**
1662
- * Verbose pipeline breadcrumb. Only fires when CODEAM_LOG=trace or
1663
- * CODEAM_DEBUG=1, so call sites can be liberal — they have zero
1664
- * cost in normal runs.
1665
- */
1666
- trace: (tag, msg, err) => emit("trace", tag, msg, err)
1667
- };
1668
-
1669
- // src/services/websocket.service.ts
1670
- var API_BASE = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
1671
- var WS_URL = API_BASE.replace("https://", "wss://").replace("http://", "ws://") + "/api/ws";
1672
- var HEARTBEAT_MS = 3e4;
1673
- var MAX_RECONNECT = 10;
1674
- var WebSocketService = class {
1675
- constructor(sessionId, pluginId) {
1676
- this.sessionId = sessionId;
1677
- this.pluginId = pluginId;
1678
- }
1679
- sessionId;
1680
- pluginId;
1681
- client = null;
1682
- heartbeat = null;
1683
- reconnectTimer = null;
1684
- reconnectAttempts = 0;
1685
- handlers = [];
1686
- _connected = false;
1687
- get connected() {
1688
- return this._connected;
1689
- }
1690
- addHandler(h) {
1691
- this.handlers.push(h);
1692
- }
1693
- connect() {
1694
- this.disconnect();
1695
- try {
1696
- this.client = new import_ws.default(WS_URL);
1697
- this.client.on("open", () => {
1698
- log.trace("ws", `connected to ${WS_URL}`);
1699
- this._connected = true;
1700
- this.reconnectAttempts = 0;
1701
- this.client.send(JSON.stringify({
1702
- type: "auth",
1703
- payload: { sessionId: this.sessionId, pluginId: this.pluginId },
1704
- timestamp: Date.now()
1705
- }));
1706
- this.startHeartbeat();
1707
- this.handlers.forEach((h) => h.onConnected());
1708
- });
1709
- this.client.on("message", (raw) => {
1710
- try {
1711
- const msg = JSON.parse(raw.toString());
1712
- if (msg.type === "pong" || msg.type === "auth_success" || msg.type === "auth_error") {
1713
- log.trace("ws", `meta msg type=${msg.type}`);
1714
- return;
1715
- }
1716
- log.trace("ws", `dispatch msg type=${msg.type}`);
1717
- this.handlers.forEach((h) => h.onMessage(msg.type, msg.payload ?? {}));
1718
- } catch (err) {
1719
- log.trace("ws", "malformed message", err);
1720
- }
1721
- });
1722
- this.client.on("close", (code, reason) => {
1723
- log.trace("ws", `closed code=${code} reason=${reason?.toString() || "(empty)"}`);
1724
- this._connected = false;
1725
- this.stopHeartbeat();
1726
- this.handlers.forEach((h) => h.onDisconnected());
1727
- if (this.reconnectAttempts < MAX_RECONNECT) {
1728
- this.reconnectAttempts++;
1729
- const delay = computePollDelay({ baseMs: 1e3, failures: this.reconnectAttempts });
1730
- this.reconnectTimer = setTimeout(() => this.connect(), delay);
1731
- }
1732
- });
1733
- this.client.on("error", (err) => {
1734
- log.trace("ws", "error", err);
1735
- });
1736
- } catch (err) {
1737
- log.trace("ws", "sync connect threw", err);
1738
- }
1739
- }
1740
- send(type, payload) {
1741
- if (!this._connected || !this.client) return;
1742
- this.client.send(JSON.stringify({ type, payload, timestamp: Date.now() }));
1743
- }
1744
- disconnect() {
1745
- if (this.reconnectTimer) {
1746
- clearTimeout(this.reconnectTimer);
1747
- this.reconnectTimer = null;
1748
- }
1749
- this.reconnectAttempts = 0;
1750
- this.stopHeartbeat();
1751
- this.client?.removeAllListeners();
1752
- this.client?.close();
1753
- this.client = null;
1754
- this._connected = false;
1755
- }
1756
- startHeartbeat() {
1757
- this.stopHeartbeat();
1758
- this.heartbeat = setInterval(() => {
1759
- if (this._connected) this.client?.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
1760
- }, HEARTBEAT_MS);
1761
- }
1762
- stopHeartbeat() {
1763
- if (this.heartbeat) {
1764
- clearInterval(this.heartbeat);
1765
- this.heartbeat = null;
1766
- }
1767
- }
1768
- };
1769
-
1770
1616
  // src/services/pairing.service.ts
1771
- var https = __toESM(require("https"));
1772
- var http = __toESM(require("http"));
1773
- var os3 = __toESM(require("os"));
1774
- var API_BASE2 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
1617
+ var API_BASE = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
1775
1618
  async function requestCode(pluginId) {
1776
1619
  try {
1777
1620
  const runtime = process.env.CODESPACES === "true" ? "github-codespaces" : "local";
1778
1621
  const codespaceName = process.env.CODESPACE_NAME;
1779
- const result = await _transport.postJson(`${API_BASE2}/api/pairing/code`, {
1622
+ const result = await _transport.postJson(`${API_BASE}/api/pairing/code`, {
1780
1623
  pluginId,
1781
1624
  ideName: "Terminal (codeam-cli)",
1782
1625
  ideVersion: package_default.version,
1783
- hostname: os3.hostname(),
1626
+ hostname: os2.hostname(),
1784
1627
  runtime,
1785
1628
  ...codespaceName ? { codespaceName } : {}
1786
1629
  });
@@ -1799,7 +1642,7 @@ function pollStatus(pluginId, onPaired, onTimeout) {
1799
1642
  if (stopped) return;
1800
1643
  try {
1801
1644
  const result = await _transport.getJson(
1802
- `${API_BASE2}/api/pairing/status?pluginId=${pluginId}`
1645
+ `${API_BASE}/api/pairing/status?pluginId=${pluginId}`
1803
1646
  );
1804
1647
  consecutiveFailures = 0;
1805
1648
  const data = result?.data;
@@ -1932,8 +1775,62 @@ async function _getJson(url) {
1932
1775
  });
1933
1776
  }
1934
1777
 
1778
+ // src/services/logger.ts
1779
+ var fs2 = __toESM(require("fs"));
1780
+ var os3 = __toESM(require("os"));
1781
+ var path2 = __toESM(require("path"));
1782
+ var LEVELS = { silent: 0, error: 1, warn: 2, info: 3, debug: 4, trace: 5 };
1783
+ function currentLevel() {
1784
+ if (process.env.CODEAM_DEBUG === "1") return LEVELS.trace;
1785
+ const raw = (process.env.CODEAM_LOG ?? "error").toLowerCase();
1786
+ return LEVELS[raw] ?? LEVELS.error;
1787
+ }
1788
+ var fileEnabled = process.env.CODEAM_DEBUG === "1" || process.env.CODEAM_LOG === "debug" || process.env.CODEAM_LOG === "trace";
1789
+ var debugFilePath = path2.join(os3.homedir(), ".codeam", "debug.log");
1790
+ var fileInitialized = false;
1791
+ function appendToFile(line) {
1792
+ if (!fileEnabled) return;
1793
+ try {
1794
+ if (!fileInitialized) {
1795
+ fs2.mkdirSync(path2.dirname(debugFilePath), { recursive: true });
1796
+ fs2.writeFileSync(
1797
+ debugFilePath,
1798
+ `=== codeam debug log \u2014 pid ${process.pid} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()} ===
1799
+ platform=${process.platform} node=${process.version} cwd=${process.cwd()}
1800
+
1801
+ `
1802
+ );
1803
+ fileInitialized = true;
1804
+ }
1805
+ fs2.appendFileSync(debugFilePath, line);
1806
+ } catch {
1807
+ }
1808
+ }
1809
+ function emit(level, tag, msg, err) {
1810
+ if (LEVELS[level] > currentLevel()) return;
1811
+ const detail = err instanceof Error ? `: ${err.message}` : err !== void 0 ? `: ${String(err)}` : "";
1812
+ const line = `[codeam:${level}] ${tag} \u2014 ${msg}${detail}
1813
+ `;
1814
+ process.stderr.write(line);
1815
+ if (LEVELS[level] >= LEVELS.debug) {
1816
+ appendToFile(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}`);
1817
+ }
1818
+ }
1819
+ var log = {
1820
+ error: (tag, msg, err) => emit("error", tag, msg, err),
1821
+ warn: (tag, msg, err) => emit("warn", tag, msg, err),
1822
+ info: (tag, msg, err) => emit("info", tag, msg, err),
1823
+ debug: (tag, msg, err) => emit("debug", tag, msg, err),
1824
+ /**
1825
+ * Verbose pipeline breadcrumb. Only fires when CODEAM_LOG=trace or
1826
+ * CODEAM_DEBUG=1, so call sites can be liberal — they have zero
1827
+ * cost in normal runs.
1828
+ */
1829
+ trace: (tag, msg, err) => emit("trace", tag, msg, err)
1830
+ };
1831
+
1935
1832
  // src/services/command-relay.service.ts
1936
- var API_BASE3 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
1833
+ var API_BASE2 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
1937
1834
  var CommandRelayService = class {
1938
1835
  constructor(pluginId, onCommand) {
1939
1836
  this.pluginId = pluginId;
@@ -1942,108 +1839,179 @@ var CommandRelayService = class {
1942
1839
  pluginId;
1943
1840
  onCommand;
1944
1841
  _running = false;
1945
- pollTimer = null;
1946
1842
  heartbeatTimer = null;
1947
1843
  agentsTimer = null;
1948
- consecutiveFailures = 0;
1949
1844
  /** True once `/api/plugin/agents` has accepted at least one report. */
1950
1845
  agentsRegistered = false;
1846
+ /** SSE connection (null when on the polling fallback or stopped). */
1847
+ sseRequest = null;
1848
+ /** Polling backoff state (only used on the fallback). */
1849
+ pollTimer = null;
1850
+ pollFailures = 0;
1851
+ /** Reconnect backoff state for the SSE stream. */
1852
+ sseFailures = 0;
1853
+ sseReconnectTimer = null;
1951
1854
  start() {
1952
- if (this.pollTimer) {
1953
- clearTimeout(this.pollTimer);
1954
- this.pollTimer = null;
1955
- }
1956
- if (this.heartbeatTimer) {
1957
- clearInterval(this.heartbeatTimer);
1958
- this.heartbeatTimer = null;
1959
- }
1960
- if (this.agentsTimer) {
1961
- clearInterval(this.agentsTimer);
1962
- this.agentsTimer = null;
1963
- }
1855
+ this.cleanup();
1964
1856
  this._running = true;
1965
1857
  this.agentsRegistered = false;
1966
1858
  this.sendHeartbeat(true);
1967
1859
  this.heartbeatTimer = setInterval(() => this.sendHeartbeat(true), 2e4);
1968
- void this.pollLoop();
1969
- this.reportAgents();
1970
1860
  this.agentsTimer = setInterval(() => {
1971
1861
  if (this._running && !this.agentsRegistered) this.reportAgents();
1972
1862
  }, 5e3);
1863
+ this.reportAgents();
1864
+ if (process.env.NODE_ENV === "test" || process.env.CODEAM_DISABLE_SSE_PULL === "1") {
1865
+ this.startPollingFallback();
1866
+ } else {
1867
+ this.connectSSE();
1868
+ }
1973
1869
  }
1974
1870
  stop() {
1975
1871
  if (!this._running) return;
1976
1872
  this._running = false;
1977
- if (this.pollTimer) {
1978
- clearTimeout(this.pollTimer);
1979
- this.pollTimer = null;
1980
- }
1981
- if (this.heartbeatTimer) {
1982
- clearInterval(this.heartbeatTimer);
1983
- this.heartbeatTimer = null;
1984
- }
1985
- if (this.agentsTimer) {
1986
- clearInterval(this.agentsTimer);
1987
- this.agentsTimer = null;
1988
- }
1873
+ this.cleanup();
1989
1874
  this.sendHeartbeat(false).catch(() => {
1990
1875
  });
1991
1876
  }
1992
1877
  async sendResult(commandId, status2, result) {
1993
- await _postJson(`${API_BASE3}/api/commands/result`, { commandId, status: status2, result });
1878
+ await _postJson(`${API_BASE2}/api/commands/result`, { commandId, status: status2, result });
1879
+ }
1880
+ // ─── SSE pull (primary) ──────────────────────────────────────────
1881
+ connectSSE() {
1882
+ if (!this._running) return;
1883
+ const url = new URL(`${API_BASE2}/api/commands/pending/stream`);
1884
+ url.searchParams.set("pluginId", this.pluginId);
1885
+ const transport = url.protocol === "https:" ? https2 : http2;
1886
+ log.trace("relay", `sse connect ${url.pathname}`);
1887
+ const req = transport.request(
1888
+ {
1889
+ hostname: url.hostname,
1890
+ port: url.port || (url.protocol === "https:" ? 443 : 80),
1891
+ path: `${url.pathname}${url.search}`,
1892
+ method: "GET",
1893
+ headers: { Accept: "text/event-stream", "Cache-Control": "no-cache" },
1894
+ timeout: 35e3
1895
+ },
1896
+ (res) => {
1897
+ if (res.statusCode !== 200) {
1898
+ log.trace("relay", `sse status=${res.statusCode}`);
1899
+ res.resume();
1900
+ this.sseFailures += 1;
1901
+ if (this.sseFailures >= 2) {
1902
+ log.trace("relay", "sse unavailable, falling back to polling");
1903
+ this.startPollingFallback();
1904
+ return;
1905
+ }
1906
+ this.scheduleSseReconnect();
1907
+ return;
1908
+ }
1909
+ this.sseFailures = 0;
1910
+ let buffer = "";
1911
+ res.setEncoding("utf8");
1912
+ res.on("data", (chunk) => {
1913
+ buffer += chunk;
1914
+ let frameEnd;
1915
+ while ((frameEnd = buffer.indexOf("\n\n")) !== -1) {
1916
+ const frame = buffer.slice(0, frameEnd);
1917
+ buffer = buffer.slice(frameEnd + 2);
1918
+ this.handleSseFrame(frame);
1919
+ }
1920
+ });
1921
+ res.on("end", () => {
1922
+ if (this._running) this.scheduleSseReconnect();
1923
+ });
1924
+ res.on("error", () => {
1925
+ if (this._running) this.scheduleSseReconnect();
1926
+ });
1927
+ }
1928
+ );
1929
+ req.on("error", (err) => {
1930
+ log.trace("relay", "sse req error", err);
1931
+ this.sseFailures += 1;
1932
+ if (this.sseFailures >= 2) {
1933
+ this.startPollingFallback();
1934
+ return;
1935
+ }
1936
+ this.scheduleSseReconnect();
1937
+ });
1938
+ req.on("timeout", () => {
1939
+ req.destroy();
1940
+ });
1941
+ req.end();
1942
+ this.sseRequest = req;
1943
+ }
1944
+ handleSseFrame(frame) {
1945
+ let event = "message";
1946
+ let data = "";
1947
+ for (const line of frame.split("\n")) {
1948
+ if (line.startsWith("event: ")) event = line.slice(7).trim();
1949
+ else if (line.startsWith("data: ")) data += line.slice(6);
1950
+ }
1951
+ if (event !== "commands" || !data) return;
1952
+ try {
1953
+ const parsed = JSON.parse(data);
1954
+ const commands = parsed.commands ?? [];
1955
+ if (commands.length === 0) return;
1956
+ log.trace("relay", `sse received ${commands.length} command(s)`);
1957
+ void this.dispatchCommands(commands);
1958
+ } catch (err) {
1959
+ log.trace("relay", "sse parse error", err);
1960
+ }
1961
+ }
1962
+ scheduleSseReconnect() {
1963
+ if (this.sseReconnectTimer) return;
1964
+ const delay = computePollDelay({ baseMs: 1e3, failures: this.sseFailures });
1965
+ this.sseReconnectTimer = setTimeout(() => {
1966
+ this.sseReconnectTimer = null;
1967
+ this.connectSSE();
1968
+ }, delay);
1969
+ }
1970
+ // ─── Polling fallback ────────────────────────────────────────────
1971
+ startPollingFallback() {
1972
+ if (this.pollTimer) return;
1973
+ void this.pollLoop();
1994
1974
  }
1995
1975
  async pollLoop() {
1996
1976
  if (!this._running) return;
1997
- await this.poll();
1977
+ await this.pollOnce();
1998
1978
  if (this._running) {
1999
- const delay = computePollDelay({
2000
- baseMs: 2e3,
2001
- failures: this.consecutiveFailures
2002
- });
1979
+ const delay = computePollDelay({ baseMs: 2e3, failures: this.pollFailures });
2003
1980
  this.pollTimer = setTimeout(() => this.pollLoop(), delay);
2004
1981
  }
2005
1982
  }
2006
- async poll() {
1983
+ async pollOnce() {
2007
1984
  try {
2008
- const data = await _getJson(
2009
- `${API_BASE3}/api/commands/pending?pluginId=${this.pluginId}`
2010
- );
1985
+ const data = await _getJson(`${API_BASE2}/api/commands/pending?pluginId=${this.pluginId}`);
2011
1986
  const commands = data?.data;
2012
- this.consecutiveFailures = 0;
2013
- if (!Array.isArray(commands)) return;
2014
- if (commands.length > 0) {
2015
- log.trace("relay", `poll received ${commands.length} command(s)`);
2016
- }
2017
- for (const obj of commands) {
2018
- try {
2019
- log.trace("relay", `dispatch type=${obj.type} id=${obj.id}`);
2020
- await this.onCommand({
2021
- id: obj.id,
2022
- sessionId: obj.sessionId,
2023
- type: obj.type,
2024
- payload: obj.payload ?? {}
2025
- });
2026
- } catch (err) {
2027
- log.trace("relay", `command handler threw`, err);
2028
- }
2029
- }
1987
+ this.pollFailures = 0;
1988
+ if (!Array.isArray(commands) || commands.length === 0) return;
1989
+ log.trace("relay", `poll received ${commands.length} command(s)`);
1990
+ await this.dispatchCommands(commands);
2030
1991
  } catch (err) {
2031
- this.consecutiveFailures += 1;
2032
- log.trace(
2033
- "relay",
2034
- `poll failed (failures=${this.consecutiveFailures})`,
2035
- err
2036
- );
1992
+ this.pollFailures += 1;
1993
+ log.trace("relay", `poll failed (failures=${this.pollFailures})`, err);
1994
+ }
1995
+ }
1996
+ async dispatchCommands(commands) {
1997
+ for (const cmd of commands) {
1998
+ try {
1999
+ log.trace("relay", `dispatch type=${cmd.type} id=${cmd.id}`);
2000
+ await this.onCommand(cmd);
2001
+ } catch (err) {
2002
+ log.trace("relay", "command handler threw", err);
2003
+ }
2037
2004
  }
2038
2005
  }
2006
+ // ─── Heartbeat + agents ──────────────────────────────────────────
2039
2007
  async sendHeartbeat(online) {
2040
- await _postJson(`${API_BASE3}/api/plugin/heartbeat`, {
2008
+ await _postJson(`${API_BASE2}/api/plugin/heartbeat`, {
2041
2009
  pluginId: this.pluginId,
2042
2010
  online
2043
2011
  }).then(() => log.trace("relay", `heartbeat ok online=${online}`)).catch((err) => log.trace("relay", `heartbeat failed online=${online}`, err));
2044
2012
  }
2045
2013
  reportAgents() {
2046
- _postJson(`${API_BASE3}/api/plugin/agents`, {
2014
+ _postJson(`${API_BASE2}/api/plugin/agents`, {
2047
2015
  pluginId: this.pluginId,
2048
2016
  agents: [{ id: "claude-code", name: "Claude Code", icon: "\u{1F916}", installed: true }]
2049
2017
  }).then(() => {
@@ -2051,6 +2019,32 @@ var CommandRelayService = class {
2051
2019
  }).catch(() => {
2052
2020
  });
2053
2021
  }
2022
+ // ─── Lifecycle ───────────────────────────────────────────────────
2023
+ cleanup() {
2024
+ if (this.pollTimer) {
2025
+ clearTimeout(this.pollTimer);
2026
+ this.pollTimer = null;
2027
+ }
2028
+ if (this.heartbeatTimer) {
2029
+ clearInterval(this.heartbeatTimer);
2030
+ this.heartbeatTimer = null;
2031
+ }
2032
+ if (this.agentsTimer) {
2033
+ clearInterval(this.agentsTimer);
2034
+ this.agentsTimer = null;
2035
+ }
2036
+ if (this.sseReconnectTimer) {
2037
+ clearTimeout(this.sseReconnectTimer);
2038
+ this.sseReconnectTimer = null;
2039
+ }
2040
+ if (this.sseRequest) {
2041
+ try {
2042
+ this.sseRequest.destroy();
2043
+ } catch {
2044
+ }
2045
+ this.sseRequest = null;
2046
+ }
2047
+ }
2054
2048
  };
2055
2049
 
2056
2050
  // src/services/pty/unix.strategy.ts
@@ -4496,8 +4490,14 @@ var ClaudeService = class {
4496
4490
  constructor(opts) {
4497
4491
  this.opts = opts;
4498
4492
  this.strategyOpts = {
4499
- onData: opts.onData ?? (() => {
4500
- }),
4493
+ onData: (d3) => {
4494
+ if (!this.claudeReady && d3.length > 0) {
4495
+ this.claudeReady = true;
4496
+ setTimeout(() => this.drainPending(), 250);
4497
+ }
4498
+ (opts.onData ?? (() => {
4499
+ }))(d3);
4500
+ },
4501
4501
  onExit: opts.onExit
4502
4502
  };
4503
4503
  }
@@ -4507,6 +4507,30 @@ var ClaudeService = class {
4507
4507
  // Methods called before spawn() (e.g. early kill/SIGINT) no-op safely.
4508
4508
  strategy = null;
4509
4509
  strategyOpts;
4510
+ /**
4511
+ * Set once the PTY emits its FIRST batch of output — proxy for
4512
+ * "Claude has rendered its input box and is ready to read keystrokes."
4513
+ * Before this, remote `sendCommand`s are buffered (`pendingInputs`)
4514
+ * and replayed in order on first data. Without this guard, the very
4515
+ * first prompt right after `codeam pair` on Windows lands while
4516
+ * Claude's React Ink tree is still mounting — the input bytes are
4517
+ * accepted by the PTY but never make it to the input field, and
4518
+ * the prompt silently vanishes.
4519
+ */
4520
+ claudeReady = false;
4521
+ pendingInputs = [];
4522
+ drainPending() {
4523
+ if (!this.strategy || this.pendingInputs.length === 0) return;
4524
+ const s = this.strategy;
4525
+ log.trace("claude", `drain pending=${this.pendingInputs.length}`);
4526
+ let offset = 0;
4527
+ for (const text of this.pendingInputs) {
4528
+ setTimeout(() => s.write(text), offset);
4529
+ setTimeout(() => s.write("\r"), offset + 50);
4530
+ offset += 200;
4531
+ }
4532
+ this.pendingInputs.length = 0;
4533
+ }
4510
4534
  async spawn() {
4511
4535
  let launch = buildClaudeLaunch();
4512
4536
  if (!launch) {
@@ -4575,6 +4599,11 @@ var ClaudeService = class {
4575
4599
  log.trace("claude", "sendCommand dropped (no strategy)");
4576
4600
  return;
4577
4601
  }
4602
+ if (!this.claudeReady) {
4603
+ log.trace("claude", `sendCommand buffered (not ready) text=${text.length}B`);
4604
+ this.pendingInputs.push(text);
4605
+ return;
4606
+ }
4578
4607
  const s = this.strategy;
4579
4608
  log.trace("claude", `sendCommand text=${text.length}B`);
4580
4609
  s.write(text);
@@ -4645,10 +4674,6 @@ var ClaudeService = class {
4645
4674
  }
4646
4675
  };
4647
4676
 
4648
- // src/services/output.service.ts
4649
- var https2 = __toESM(require("https"));
4650
- var http2 = __toESM(require("http"));
4651
-
4652
4677
  // ../../packages/shared/src/protocol/parseChrome.ts
4653
4678
  var SPINNER_RE = /^(?:[✳✢✶✻✽✴✷✸✹⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏◐◑◒◓▁▂▃▄▅▆▇█]|🔴|🟠|🟡|🟢|🔵|🟣|🟤|⚫|⚪|🌀|💭|✨)\s/u;
4654
4679
  var BULLET_TOOL_RE = /^•\s+(?:Read(?:ing)?|Edit(?:ing)?|Writ(?:e|ing)|Bash|Runn(?:ing)?|Search(?:ing)?|Glob(?:bing)?|Grep(?:ping)?|Creat(?:e|ing)|Execut(?:e|ing)|Task|Agent|NotebookEdit)\b/i;
@@ -5019,158 +5044,358 @@ function getContextWindow(model) {
5019
5044
  return DEFAULT_CONTEXT_WINDOW;
5020
5045
  }
5021
5046
 
5047
+ // src/services/output/chrome-tracker.ts
5048
+ var ChromeStepTracker = class {
5049
+ history = [];
5050
+ sentCount = 0;
5051
+ reset() {
5052
+ this.history = [];
5053
+ this.sentCount = 0;
5054
+ }
5055
+ /** Parse the rendered lines, append unseen steps to the cumulative history. */
5056
+ ingest(lines) {
5057
+ const visible = lines.filter((l) => isChromeLine(l)).map((l) => parseChromeLine(l)).filter((s) => s !== null);
5058
+ if (visible.length === 0) return;
5059
+ for (const step of visible) {
5060
+ const exists = this.history.some(
5061
+ (s) => s.tool === step.tool && s.label === step.label
5062
+ );
5063
+ if (!exists) this.history.push(step);
5064
+ }
5065
+ }
5066
+ /**
5067
+ * Returns the steps that have NOT yet been shipped on the wire,
5068
+ * marking them as shipped. Empty array means nothing new since
5069
+ * the last call. Caller forwards this as the chunk's
5070
+ * `appendSteps` payload.
5071
+ */
5072
+ consumeDelta() {
5073
+ if (this.history.length === this.sentCount) return [];
5074
+ const delta = this.history.slice(this.sentCount);
5075
+ this.sentCount = this.history.length;
5076
+ return delta;
5077
+ }
5078
+ /** Snapshot of the cumulative unique-step history (debug + tests). */
5079
+ get cumulativeHistory() {
5080
+ return this.history;
5081
+ }
5082
+ };
5083
+
5084
+ // src/services/output/chunk-emitter.ts
5085
+ var https3 = __toESM(require("https"));
5086
+ var http3 = __toESM(require("http"));
5087
+ var API_BASE3 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
5088
+ var ChunkEmitter = class {
5089
+ constructor(opts) {
5090
+ this.opts = opts;
5091
+ this.headers = {
5092
+ "Content-Type": "application/json",
5093
+ // Tell the backend which wire-format version we speak so
5094
+ // it can route legacy translations / 426 us when we're
5095
+ // too far behind. Bumped to 2.0.0 with the discriminated-
5096
+ // chunk + delta-chrome refactor in this release.
5097
+ "X-Codeam-Protocol-Version": "2.0.0"
5098
+ };
5099
+ if (opts.pluginAuthToken) {
5100
+ this.headers["X-Plugin-Auth-Token"] = opts.pluginAuthToken;
5101
+ }
5102
+ }
5103
+ opts;
5104
+ url = `${API_BASE3}/api/commands/output`;
5105
+ headers;
5106
+ /**
5107
+ * Send a chunk. `body` is the chunk fields minus `sessionId` /
5108
+ * `pluginId` — the emitter splices those in. `critical = true`
5109
+ * triggers up to 3 retries with linear backoff (200/400/600 ms);
5110
+ * non-critical sends are best-effort (a transient miss gets
5111
+ * superseded by the next tick's emission).
5112
+ */
5113
+ async send(body, opts = {}) {
5114
+ const payload = JSON.stringify({
5115
+ sessionId: this.opts.sessionId,
5116
+ pluginId: this.opts.pluginId,
5117
+ ...body
5118
+ });
5119
+ const maxRetries = opts.critical ? 3 : 0;
5120
+ log.trace(
5121
+ "chunkEmitter",
5122
+ `send type=${body.type ?? "(clear)"} bytes=${payload.length}`
5123
+ );
5124
+ return new Promise((resolve2) => {
5125
+ const attempt = (attemptsLeft) => {
5126
+ _transport2.post(this.url, this.headers, payload).then(({ statusCode, body: resBody }) => {
5127
+ if (statusCode === 410 || statusCode === 404 && /SESSION_NOT_FOUND|SESSION_GONE/.test(resBody)) {
5128
+ process.stderr.write("[codeam] session was deleted/disconnected \u2014 stopping output stream.\n");
5129
+ resolve2({ dead: true });
5130
+ return;
5131
+ }
5132
+ if (statusCode >= 400) {
5133
+ process.stderr.write(`[codeam] output API error ${statusCode}: ${resBody}
5134
+ `);
5135
+ }
5136
+ log.trace("chunkEmitter", `status=${statusCode}`);
5137
+ resolve2({ dead: false });
5138
+ }).catch((err) => {
5139
+ log.trace(
5140
+ "chunkEmitter",
5141
+ `error retries-left=${attemptsLeft}`,
5142
+ err
5143
+ );
5144
+ if (attemptsLeft > 0) {
5145
+ const delay = 200 * (maxRetries - attemptsLeft + 1);
5146
+ setTimeout(() => attempt(attemptsLeft - 1), delay);
5147
+ } else {
5148
+ resolve2({ dead: false });
5149
+ }
5150
+ });
5151
+ };
5152
+ attempt(maxRetries);
5153
+ });
5154
+ }
5155
+ };
5156
+ var _transport2 = {
5157
+ post: _post
5158
+ };
5159
+ function _post(url, headers, payload) {
5160
+ return new Promise((resolve2, reject) => {
5161
+ let settled = false;
5162
+ const u2 = new URL(url);
5163
+ const transport = u2.protocol === "https:" ? https3 : http3;
5164
+ const req = transport.request(
5165
+ {
5166
+ hostname: u2.hostname,
5167
+ port: u2.port || (u2.protocol === "https:" ? 443 : 80),
5168
+ path: u2.pathname,
5169
+ method: "POST",
5170
+ headers: {
5171
+ ...headers,
5172
+ "Content-Length": Buffer.byteLength(payload)
5173
+ },
5174
+ timeout: 8e3
5175
+ },
5176
+ (res) => {
5177
+ let resData = "";
5178
+ res.on("data", (c2) => {
5179
+ resData += c2.toString();
5180
+ });
5181
+ res.on("end", () => {
5182
+ if (settled) return;
5183
+ settled = true;
5184
+ resolve2({ statusCode: res.statusCode ?? 0, body: resData });
5185
+ });
5186
+ }
5187
+ );
5188
+ req.on("error", (err) => {
5189
+ if (settled) return;
5190
+ settled = true;
5191
+ reject(err);
5192
+ });
5193
+ req.on("timeout", () => {
5194
+ req.destroy();
5195
+ });
5196
+ req.write(payload);
5197
+ req.end();
5198
+ });
5199
+ }
5200
+
5201
+ // src/services/output/pty-buffer.ts
5202
+ var PtyBuffer = class {
5203
+ raw = "";
5204
+ active = false;
5205
+ lastPushAt = 0;
5206
+ terminalInputPending = false;
5207
+ /** Whether to absorb pushes (`true`) or only watch for terminal input (`false`). */
5208
+ get isActive() {
5209
+ return this.active;
5210
+ }
5211
+ /** Bytes accumulated since the last reset. */
5212
+ get content() {
5213
+ return this.raw;
5214
+ }
5215
+ /** Wall-clock of the most recent printable push (`0` if none yet this turn). */
5216
+ get lastPushTime() {
5217
+ return this.lastPushAt;
5218
+ }
5219
+ /** Length of the accumulated buffer in raw bytes (debug + tests). */
5220
+ get size() {
5221
+ return this.raw.length;
5222
+ }
5223
+ activate() {
5224
+ this.active = true;
5225
+ this.raw = "";
5226
+ this.lastPushAt = 0;
5227
+ this.terminalInputPending = false;
5228
+ }
5229
+ deactivate() {
5230
+ this.active = false;
5231
+ }
5232
+ reset() {
5233
+ this.raw = "";
5234
+ this.lastPushAt = 0;
5235
+ }
5236
+ /**
5237
+ * Ingest a raw PTY frame. Returns whether the buffer was active
5238
+ * at the time (caller cares because rendering only matters for
5239
+ * active frames) and whether this push triggered the
5240
+ * terminal-initiated-turn signal.
5241
+ */
5242
+ push(raw) {
5243
+ if (!this.active) {
5244
+ let terminalInputDetected = false;
5245
+ if (!this.terminalInputPending && hasPrintable(raw)) {
5246
+ this.terminalInputPending = true;
5247
+ terminalInputDetected = true;
5248
+ }
5249
+ return { active: false, terminalInputDetected };
5250
+ }
5251
+ this.raw += raw;
5252
+ if (hasPrintable(raw)) this.lastPushAt = Date.now();
5253
+ return { active: true, terminalInputDetected: false };
5254
+ }
5255
+ };
5256
+ function hasPrintable(raw) {
5257
+ const stripped = raw.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, "");
5258
+ return stripped.trim().length > 0;
5259
+ }
5260
+
5261
+ // src/services/output/turn-renderer.ts
5262
+ function renderLines(buffer) {
5263
+ return renderToLines(buffer);
5264
+ }
5265
+ function detectAnySelector(lines) {
5266
+ return detectSelector(lines) ?? detectListSelector(lines);
5267
+ }
5268
+ function extractContent(lines) {
5269
+ return filterChrome(lines).join("\n").replace(/\n{3,}/g, "\n\n").trim();
5270
+ }
5271
+
5022
5272
  // src/services/output.service.ts
5023
- var API_BASE4 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
5024
5273
  var OutputService = class _OutputService {
5025
- constructor(sessionId, pluginId, onSessionIdDetected, onRateLimitDetected, onTurnComplete, onTerminalTurnDetected, pluginAuthToken) {
5026
- this.sessionId = sessionId;
5027
- this.pluginId = pluginId;
5028
- this.pluginAuthToken = pluginAuthToken;
5029
- this.onSessionIdDetected = onSessionIdDetected;
5030
- this.onRateLimitDetected = onRateLimitDetected;
5031
- this.onTurnComplete = onTurnComplete;
5032
- this.onTerminalTurnDetected = onTerminalTurnDetected;
5033
- }
5034
- sessionId;
5035
- pluginId;
5036
- pluginAuthToken;
5037
- rawBuffer = "";
5274
+ pty = new PtyBuffer();
5275
+ steps = new ChromeStepTracker();
5276
+ emitter;
5038
5277
  lastSentContent = "";
5039
- lastSentChromeStepsJson = "";
5040
- chromeStepsHistory = [];
5041
5278
  pollTimer = null;
5042
5279
  startTime = 0;
5043
- active = false;
5044
5280
  terminalTurnPending = false;
5045
- lastPushTime = 0;
5046
5281
  onSessionIdDetected;
5047
5282
  onRateLimitDetected;
5048
5283
  onTurnComplete;
5049
5284
  onTerminalTurnDetected;
5285
+ /** Tick cadence — every 1 s while a turn is active. */
5050
5286
  static POLL_MS = 1e3;
5287
+ /** Idle threshold for "the agent's text settled, finalize the turn". */
5051
5288
  static IDLE_MS = 3e3;
5052
- /** Shorter idle threshold for selector detection (UI is ready immediately). */
5289
+ /** Same threshold but tighter for selectors (UI is ready to interact immediately). */
5053
5290
  static SELECTOR_IDLE_MS = 1500;
5054
5291
  /**
5055
- * Grace period before the first tick processes output.
5056
- * Prevents the raw PTY input echo from being captured before Claude Code
5057
- * clears and re-renders its TUI (which happens within ~100-200 ms of
5058
- * receiving the input, but we give a 1.5 s margin for loaded machines).
5292
+ * Grace period before tick processes anything — Claude needs ~100-
5293
+ * 200 ms after `\r` to clear the input echo and re-render the TUI.
5294
+ * 1.5 s is a comfortable margin on loaded machines.
5059
5295
  */
5060
5296
  static WARMUP_MS = 1500;
5061
- /** Max idle with no visible content (spinner only) before finalizing. */
5297
+ /** Max idle with chrome-only output before we stop waiting on the agent. */
5062
5298
  static EMPTY_TIMEOUT_MS = 6e4;
5299
+ /** Hard turn cap — pathological no-op turns get cut after 2 minutes. */
5063
5300
  static MAX_MS = 12e4;
5301
+ constructor(sessionId, pluginId, onSessionIdDetected, onRateLimitDetected, onTurnComplete, onTerminalTurnDetected, pluginAuthToken) {
5302
+ this.onSessionIdDetected = onSessionIdDetected;
5303
+ this.onRateLimitDetected = onRateLimitDetected;
5304
+ this.onTurnComplete = onTurnComplete;
5305
+ this.onTerminalTurnDetected = onTerminalTurnDetected;
5306
+ this.emitter = new ChunkEmitter({
5307
+ sessionId,
5308
+ pluginId,
5309
+ pluginAuthToken
5310
+ });
5311
+ }
5312
+ // ─── Turn lifecycle ──────────────────────────────────────────────
5313
+ /**
5314
+ * Begin a turn driven by a mobile-side prompt. Resets the buffer
5315
+ * and emits the boundary chunks (clear → new_turn) that tell
5316
+ * clients to wipe the prior agent reply and show "Agent is
5317
+ * typing…".
5318
+ */
5319
+ newTurn() {
5320
+ log.trace("outputSvc", "newTurn() \u2014 activating output stream");
5321
+ this.beginTurn();
5322
+ this.send({ type: "clear" }, { critical: true }).then(() => this.send({ type: "new_turn", done: false }, { critical: true })).catch(() => {
5323
+ });
5324
+ }
5064
5325
  /**
5065
- * Called by the terminal-turn callback once the user message is known.
5066
- * Sequences: clear user_message (if any) new_turn → start timer.
5067
- * This guarantees the user message appears before the typing placeholder
5068
- * in the apps, with no race against the clear event.
5326
+ * Begin a turn driven by the user typing locally in their
5327
+ * terminal. Same shape as `newTurn` but additionally sends a
5328
+ * `user_message` so collaborators see the prompt attributed
5329
+ * correctly. `userText` is the prompt text scraped from the
5330
+ * Claude JSONL by `historySvc.waitForNewUserMessage`.
5069
5331
  */
5070
5332
  async startTerminalTurn(userText) {
5071
5333
  this.terminalTurnPending = false;
5072
- this.stopPoll();
5073
- this.rawBuffer = "";
5074
- this.lastSentContent = "";
5075
- this.lastSentChromeStepsJson = "";
5076
- this.chromeStepsHistory = [];
5077
- this.lastPushTime = 0;
5078
- this.active = true;
5079
- this.startTime = Date.now();
5080
- await this.postChunk({ clear: true });
5334
+ this.beginTurn();
5335
+ await this.send({ type: "clear" }, { critical: true });
5081
5336
  if (userText) {
5082
- await this.postChunk({ type: "user_message", content: userText, done: true });
5337
+ await this.send({ type: "user_message", content: userText, done: true }, { critical: true });
5083
5338
  }
5084
- await this.postChunk({ type: "new_turn", content: "", done: false });
5085
- this.pollTimer = setInterval(() => this.tick(), _OutputService.POLL_MS);
5086
- }
5087
- newTurn() {
5088
- log.trace("outputSvc", "newTurn() \u2014 activating output stream");
5089
- this.stopPoll();
5090
- this.rawBuffer = "";
5091
- this.lastSentContent = "";
5092
- this.lastSentChromeStepsJson = "";
5093
- this.chromeStepsHistory = [];
5094
- this.lastPushTime = 0;
5095
- this.active = true;
5096
- this.terminalTurnPending = false;
5097
- this.startTime = Date.now();
5098
- this.postChunk({ clear: true }).then(() => this.postChunk({ type: "new_turn", content: "", done: false })).catch(() => {
5099
- });
5100
- this.pollTimer = setInterval(() => this.tick(), _OutputService.POLL_MS);
5339
+ await this.send({ type: "new_turn", done: false }, { critical: true });
5101
5340
  }
5102
5341
  /**
5103
- * Like newTurn() but signals clients that a session is being resumed.
5104
- * The resumedSessionId tells clients to fetch the conversation from the API.
5105
- * Awaits the POST so callers can guarantee the signal is sent before restarting Claude.
5342
+ * Begin a turn after a `resume_session` request. Includes the
5343
+ * `resumedSessionId` so the client wipes its history and
5344
+ * re-fetches from the JSONL via `get_conversation`.
5106
5345
  */
5107
5346
  async newTurnResume(resumedSessionId) {
5108
- this.stopPoll();
5109
- this.rawBuffer = "";
5110
- this.lastSentContent = "";
5111
- this.lastSentChromeStepsJson = "";
5112
- this.chromeStepsHistory = [];
5113
- this.lastPushTime = 0;
5114
- this.active = true;
5115
- this.startTime = Date.now();
5116
- await this.postChunk({ clear: true });
5117
- await this.postChunk({ type: "new_turn", resumedSessionId, content: "", done: false });
5118
- this.pollTimer = setInterval(() => this.tick(), _OutputService.POLL_MS);
5347
+ this.beginTurn();
5348
+ await this.send({ type: "clear" }, { critical: true });
5349
+ await this.send(
5350
+ { type: "new_turn", done: false, resumedSessionId },
5351
+ { critical: true }
5352
+ );
5119
5353
  }
5354
+ // ─── Pump ────────────────────────────────────────────────────────
5120
5355
  push(raw) {
5121
- if (!this.active) {
5122
- if (!this.terminalTurnPending) {
5123
- const printable2 = raw.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, "");
5124
- if (printable2.trim()) {
5125
- this.terminalTurnPending = true;
5126
- log.trace("outputSvc", `terminal-turn detected (idle, ${raw.length}B)`);
5127
- this.onTerminalTurnDetected?.();
5128
- }
5356
+ const result = this.pty.push(raw);
5357
+ if (!result.active) {
5358
+ if (result.terminalInputDetected && !this.terminalTurnPending) {
5359
+ this.terminalTurnPending = true;
5360
+ this.onTerminalTurnDetected?.();
5129
5361
  }
5130
5362
  log.trace("outputSvc", `push dropped (inactive, ${raw.length}B)`);
5131
5363
  return;
5132
5364
  }
5133
- this.rawBuffer += raw;
5134
- const printable = raw.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, "");
5135
- if (printable.trim()) {
5136
- this.lastPushTime = Date.now();
5137
- this.tryExtractSessionId(printable);
5138
- this.tryDetectRateLimit(printable);
5139
- }
5140
5365
  log.trace(
5141
5366
  "outputSvc",
5142
- `push +${raw.length}B (buf=${this.rawBuffer.length}B printable=${printable.trim().length})`
5367
+ `push +${raw.length}B (buf=${this.pty.size}B)`
5143
5368
  );
5369
+ this.tryExtractSessionId(raw);
5370
+ this.tryDetectRateLimit(raw);
5144
5371
  }
5145
- /** Extract Claude conversation ID from output text (e.g., from /cost command or session resume) */
5146
- tryExtractSessionId(text) {
5147
- const patterns = [
5148
- /Resuming session[:\s]+([a-f0-9-]{36})/i,
5149
- /Session[:\s]+([a-f0-9-]{36})/i,
5150
- /Conversation[:\s]+([a-f0-9-]{36})/i,
5151
- /Session\s+ID[:\s]+([a-f0-9-]{36})/i
5152
- ];
5153
- for (const pattern of patterns) {
5154
- const match = text.match(pattern);
5155
- if (match && this.onSessionIdDetected) {
5156
- this.onSessionIdDetected(match[1]);
5157
- return;
5158
- }
5159
- }
5372
+ dispose() {
5373
+ this.stopPoll();
5374
+ this.pty.deactivate();
5160
5375
  }
5161
- /** Detect rate limit messages from Claude Code output (e.g. "You've hit your limit · resets Apr 16 at 1pm") */
5162
- tryDetectRateLimit(text) {
5163
- const match = text.match(/hit your limit.*resets\s+(.+?)(?:\s*\(|$)/i) ?? text.match(/rate.?limit.*resets\s+(.+?)(?:\s*\(|$)/i);
5164
- if (match && this.onRateLimitDetected) {
5165
- this.onRateLimitDetected(match[1].trim());
5376
+ // ─── Internals ───────────────────────────────────────────────────
5377
+ beginTurn() {
5378
+ this.stopPoll();
5379
+ this.pty.activate();
5380
+ this.steps.reset();
5381
+ this.lastSentContent = "";
5382
+ this.startTime = Date.now();
5383
+ this.pollTimer = setInterval(() => this.tick(), _OutputService.POLL_MS);
5384
+ }
5385
+ async send(body, opts = {}) {
5386
+ const outcome = await this.emitter.send(body, opts);
5387
+ if (outcome.dead && this.pty.isActive) {
5388
+ this.dispose();
5166
5389
  }
5167
5390
  }
5168
- dispose() {
5169
- this.stopPoll();
5170
- this.active = false;
5391
+ stopPoll() {
5392
+ if (this.pollTimer) {
5393
+ clearInterval(this.pollTimer);
5394
+ this.pollTimer = null;
5395
+ }
5171
5396
  }
5172
5397
  tick() {
5173
- if (!this.active) return;
5398
+ if (!this.pty.isActive) return;
5174
5399
  const now = Date.now();
5175
5400
  const elapsed = now - this.startTime;
5176
5401
  if (elapsed >= _OutputService.MAX_MS) {
@@ -5178,36 +5403,51 @@ var OutputService = class _OutputService {
5178
5403
  return;
5179
5404
  }
5180
5405
  if (elapsed < _OutputService.WARMUP_MS) return;
5181
- const lines = renderToLines(this.rawBuffer);
5182
- this.postChromeSteps(lines);
5183
- const selector = detectSelector(lines) ?? detectListSelector(lines);
5406
+ const lines = renderLines(this.pty.content);
5407
+ this.steps.ingest(lines);
5408
+ const stepsDelta = this.steps.consumeDelta();
5409
+ if (stepsDelta.length > 0) {
5410
+ this.send({ type: "chrome_steps", appendSteps: stepsDelta }).catch(() => {
5411
+ });
5412
+ }
5413
+ const selector = detectAnySelector(lines);
5184
5414
  if (selector) {
5185
- const idleMs2 = this.lastPushTime > 0 ? now - this.lastPushTime : elapsed;
5415
+ const idleMs2 = this.pty.lastPushTime > 0 ? now - this.pty.lastPushTime : elapsed;
5186
5416
  log.trace(
5187
5417
  "outputSvc",
5188
5418
  `tick selector found (idleMs=${idleMs2}, options=${selector.options.length})`
5189
5419
  );
5190
5420
  if (idleMs2 >= _OutputService.SELECTOR_IDLE_MS) {
5191
5421
  this.stopPoll();
5192
- this.active = false;
5193
- this.postChunk({ type: "select_prompt", content: selector.question, options: selector.options, optionDescriptions: selector.optionDescriptions, currentIndex: selector.currentIndex, done: true }).catch(() => {
5422
+ this.pty.deactivate();
5423
+ this.send(
5424
+ {
5425
+ type: "select_prompt",
5426
+ content: selector.question,
5427
+ options: selector.options,
5428
+ optionDescriptions: selector.optionDescriptions,
5429
+ currentIndex: selector.currentIndex,
5430
+ done: true
5431
+ },
5432
+ { critical: true }
5433
+ ).catch(() => {
5194
5434
  });
5195
5435
  }
5196
5436
  return;
5197
5437
  }
5198
- const content = filterChrome(lines).join("\n").replace(/\n{3,}/g, "\n\n").trim();
5438
+ const content = extractContent(lines);
5199
5439
  if (!content) {
5200
5440
  log.trace(
5201
5441
  "outputSvc",
5202
- `tick empty content (raw=${this.rawBuffer.length}B lines=${lines.length} elapsed=${elapsed}ms)`
5442
+ `tick empty content (raw=${this.pty.size}B lines=${lines.length} elapsed=${elapsed}ms)`
5203
5443
  );
5204
5444
  if (elapsed >= _OutputService.EMPTY_TIMEOUT_MS) this.finalize();
5205
5445
  return;
5206
5446
  }
5207
- const idleMs = this.lastPushTime > 0 ? now - this.lastPushTime : elapsed;
5447
+ const idleMs = this.pty.lastPushTime > 0 ? now - this.pty.lastPushTime : elapsed;
5208
5448
  log.trace(
5209
5449
  "outputSvc",
5210
- `tick content (raw=${this.rawBuffer.length}B lines=${lines.length} content=${content.length} idleMs=${idleMs})`
5450
+ `tick content (raw=${this.pty.size}B lines=${lines.length} content=${content.length} idleMs=${idleMs})`
5211
5451
  );
5212
5452
  if (idleMs >= _OutputService.IDLE_MS) {
5213
5453
  this.finalize();
@@ -5215,161 +5455,78 @@ var OutputService = class _OutputService {
5215
5455
  }
5216
5456
  if (content !== this.lastSentContent) {
5217
5457
  this.lastSentContent = content;
5218
- this.postChunk({ type: "text", content, done: false }).catch(() => {
5458
+ this.send({ type: "text", content, done: false }).catch(() => {
5219
5459
  });
5220
5460
  }
5221
5461
  }
5222
5462
  finalize() {
5223
- const lines = renderToLines(this.rawBuffer);
5224
- this.postChromeSteps(lines);
5225
- const selector = detectSelector(lines) ?? detectListSelector(lines);
5463
+ const lines = renderLines(this.pty.content);
5464
+ this.steps.ingest(lines);
5465
+ const stepsDelta = this.steps.consumeDelta();
5466
+ if (stepsDelta.length > 0) {
5467
+ this.send({ type: "chrome_steps", appendSteps: stepsDelta }).catch(() => {
5468
+ });
5469
+ }
5470
+ const selector = detectAnySelector(lines);
5226
5471
  this.stopPoll();
5227
- this.active = false;
5472
+ this.pty.deactivate();
5228
5473
  if (selector) {
5229
- this.postChunk({ type: "select_prompt", content: selector.question, options: selector.options, optionDescriptions: selector.optionDescriptions, currentIndex: selector.currentIndex, done: true }).catch(() => {
5474
+ this.send(
5475
+ {
5476
+ type: "select_prompt",
5477
+ content: selector.question,
5478
+ options: selector.options,
5479
+ optionDescriptions: selector.optionDescriptions,
5480
+ currentIndex: selector.currentIndex,
5481
+ done: true
5482
+ },
5483
+ { critical: true }
5484
+ ).catch(() => {
5230
5485
  });
5231
5486
  } else {
5232
- const content = filterChrome(lines).join("\n").replace(/\n{3,}/g, "\n\n").trim();
5233
- this.postChunk({ type: "text", content, done: true }).catch(() => {
5487
+ const content = extractContent(lines);
5488
+ this.send(
5489
+ { type: "text", content, done: true },
5490
+ { critical: true }
5491
+ ).catch(() => {
5234
5492
  });
5235
5493
  this.onTurnComplete?.();
5236
5494
  }
5237
5495
  }
5238
- stopPoll() {
5239
- if (this.pollTimer) {
5240
- clearInterval(this.pollTimer);
5241
- this.pollTimer = null;
5242
- }
5243
- }
5244
- postChromeSteps(lines) {
5245
- const visible = lines.filter((l) => isChromeLine(l)).map((l) => parseChromeLine(l)).filter((s) => s !== null);
5246
- if (visible.length === 0) return;
5247
- let changed = false;
5248
- for (const step of visible) {
5249
- const exists = this.chromeStepsHistory.some(
5250
- (s) => s.tool === step.tool && s.label === step.label
5251
- );
5252
- if (!exists) {
5253
- this.chromeStepsHistory.push(step);
5254
- changed = true;
5496
+ // ─── Side-channel observation (session id + rate limit) ──────────
5497
+ tryExtractSessionId(text) {
5498
+ if (!this.onSessionIdDetected) return;
5499
+ const printable = text.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, "");
5500
+ const patterns = [
5501
+ /Resuming session[:\s]+([a-f0-9-]{36})/i,
5502
+ /Session[:\s]+([a-f0-9-]{36})/i,
5503
+ /Conversation[:\s]+([a-f0-9-]{36})/i,
5504
+ /Session\s+ID[:\s]+([a-f0-9-]{36})/i
5505
+ ];
5506
+ for (const pattern of patterns) {
5507
+ const match = printable.match(pattern);
5508
+ if (match) {
5509
+ this.onSessionIdDetected(match[1]);
5510
+ return;
5255
5511
  }
5256
5512
  }
5257
- if (!changed) return;
5258
- const json = JSON.stringify(this.chromeStepsHistory);
5259
- if (json === this.lastSentChromeStepsJson) return;
5260
- this.lastSentChromeStepsJson = json;
5261
- this.postChunk({ type: "chrome_steps", content: "", steps: [...this.chromeStepsHistory] }).catch(() => {
5262
- });
5263
5513
  }
5264
- postChunk(body) {
5265
- const isCritical = body.clear === true || body.type === "new_turn" || body.type === "user_message" || body.done === true;
5266
- const maxRetries = isCritical ? 3 : 0;
5267
- const payload = JSON.stringify({
5268
- sessionId: this.sessionId,
5269
- pluginId: this.pluginId,
5270
- ...body
5271
- });
5272
- const headers = {
5273
- "Content-Type": "application/json"
5274
- };
5275
- if (this.pluginAuthToken) {
5276
- headers["X-Plugin-Auth-Token"] = this.pluginAuthToken;
5277
- }
5278
- const chunkType = body.type ?? "(clear)";
5279
- log.trace(
5280
- "outputSvc",
5281
- `postChunk type=${chunkType} done=${body.done === true} bytes=${payload.length}`
5282
- );
5283
- if (chunkType === "select_prompt" || chunkType === "new_turn" || body.type === "text" && body.done === true) {
5284
- const preview = payload.length > 2048 ? payload.slice(0, 2048) + "\u2026(truncated)" : payload;
5285
- log.trace("outputSvc", `payload ${preview}`);
5514
+ tryDetectRateLimit(text) {
5515
+ if (!this.onRateLimitDetected) return;
5516
+ const printable = text.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, "");
5517
+ const match = printable.match(/hit your limit.*resets\s+(.+?)(?:\s*\(|$)/i) ?? printable.match(/rate.?limit.*resets\s+(.+?)(?:\s*\(|$)/i);
5518
+ if (match) {
5519
+ this.onRateLimitDetected(match[1].trim());
5286
5520
  }
5287
- return new Promise((resolve2) => {
5288
- const attempt = (attemptsLeft) => {
5289
- _transport2.sendOutputChunk(`${API_BASE4}/api/commands/output`, headers, payload).then(({ statusCode, body: resBody }) => {
5290
- log.trace("outputSvc", `postChunk status=${statusCode}`);
5291
- if (statusCode === 410 || statusCode === 404 && /SESSION_NOT_FOUND|SESSION_GONE/.test(resBody)) {
5292
- if (this.active) {
5293
- process.stderr.write("[codeam] session was deleted/disconnected \u2014 stopping output stream.\n");
5294
- this.dispose();
5295
- }
5296
- resolve2();
5297
- return;
5298
- }
5299
- if (statusCode >= 400) {
5300
- process.stderr.write(`[codeam] output API error ${statusCode}: ${resBody}
5301
- `);
5302
- }
5303
- resolve2();
5304
- }).catch((err) => {
5305
- log.trace(
5306
- "outputSvc",
5307
- `postChunk error (retries left=${attemptsLeft})`,
5308
- err
5309
- );
5310
- if (attemptsLeft > 0) {
5311
- const delay = 200 * (maxRetries - attemptsLeft + 1);
5312
- setTimeout(() => attempt(attemptsLeft - 1), delay);
5313
- } else {
5314
- resolve2();
5315
- }
5316
- });
5317
- };
5318
- attempt(maxRetries);
5319
- });
5320
5521
  }
5321
5522
  };
5322
- var _transport2 = {
5323
- sendOutputChunk: _sendOutputChunk
5324
- };
5325
- function _sendOutputChunk(url, headers, payload) {
5326
- return new Promise((resolve2, reject) => {
5327
- let settled = false;
5328
- const u2 = new URL(url);
5329
- const transport = u2.protocol === "https:" ? https2 : http2;
5330
- const req = transport.request(
5331
- {
5332
- hostname: u2.hostname,
5333
- port: u2.port || (u2.protocol === "https:" ? 443 : 80),
5334
- path: u2.pathname,
5335
- method: "POST",
5336
- headers: {
5337
- ...headers,
5338
- "Content-Length": Buffer.byteLength(payload)
5339
- },
5340
- timeout: 8e3
5341
- },
5342
- (res) => {
5343
- let resData = "";
5344
- res.on("data", (c2) => {
5345
- resData += c2.toString();
5346
- });
5347
- res.on("end", () => {
5348
- if (settled) return;
5349
- settled = true;
5350
- resolve2({ statusCode: res.statusCode ?? 0, body: resData });
5351
- });
5352
- }
5353
- );
5354
- req.on("error", (err) => {
5355
- if (settled) return;
5356
- settled = true;
5357
- reject(err);
5358
- });
5359
- req.on("timeout", () => {
5360
- req.destroy();
5361
- });
5362
- req.write(payload);
5363
- req.end();
5364
- });
5365
- }
5366
5523
 
5367
5524
  // src/services/history.service.ts
5368
5525
  var fs5 = __toESM(require("fs"));
5369
5526
  var path8 = __toESM(require("path"));
5370
5527
  var os6 = __toESM(require("os"));
5371
- var https3 = __toESM(require("https"));
5372
- var http3 = __toESM(require("http"));
5528
+ var https4 = __toESM(require("https"));
5529
+ var http4 = __toESM(require("http"));
5373
5530
  var import_zod = require("zod");
5374
5531
  var historyRecordSchema = import_zod.z.object({
5375
5532
  type: import_zod.z.string().optional(),
@@ -5381,7 +5538,7 @@ var historyRecordSchema = import_zod.z.object({
5381
5538
  content: import_zod.z.union([import_zod.z.string(), import_zod.z.array(import_zod.z.unknown())]).optional()
5382
5539
  }).passthrough().optional()
5383
5540
  }).passthrough();
5384
- var API_BASE5 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
5541
+ var API_BASE4 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
5385
5542
  function encodeCwd(cwd) {
5386
5543
  return cwd.replace(/[\\/:]/g, "-");
5387
5544
  }
@@ -5454,8 +5611,8 @@ function parseJsonl(filePath) {
5454
5611
  function post(endpoint, body) {
5455
5612
  return new Promise((resolve2) => {
5456
5613
  const payload = JSON.stringify(body);
5457
- const u2 = new URL(`${API_BASE5}${endpoint}`);
5458
- const transport = u2.protocol === "https:" ? https3 : http3;
5614
+ const u2 = new URL(`${API_BASE4}${endpoint}`);
5615
+ const transport = u2.protocol === "https:" ? https4 : http4;
5459
5616
  const req = transport.request(
5460
5617
  {
5461
5618
  hostname: u2.hostname,
@@ -5834,6 +5991,161 @@ var HistoryService = class {
5834
5991
  }
5835
5992
  };
5836
5993
 
5994
+ // src/commands/start/quota-fetcher.ts
5995
+ var fs6 = __toESM(require("fs"));
5996
+ var os7 = __toESM(require("os"));
5997
+ var path9 = __toESM(require("path"));
5998
+ var import_child_process4 = require("child_process");
5999
+ var inProgress = false;
6000
+ var HELPER_SCRIPT = `import os,pty,sys,select,signal,struct,fcntl,termios,errno
6001
+ m,s=pty.openpty()
6002
+ try:
6003
+ fcntl.ioctl(s,termios.TIOCSWINSZ,struct.pack('HHHH',30,120,0,0))
6004
+ except Exception:pass
6005
+ pid=os.fork()
6006
+ if pid==0:
6007
+ os.close(m);os.setsid()
6008
+ try:fcntl.ioctl(s,termios.TIOCSCTTY,0)
6009
+ except Exception:pass
6010
+ for fd in[0,1,2]:os.dup2(s,fd)
6011
+ if s>2:os.close(s)
6012
+ os.execvp(sys.argv[1],sys.argv[1:])
6013
+ sys.exit(127)
6014
+ os.close(s)
6015
+ done=[False]
6016
+ def onchld(n,f):
6017
+ try:os.waitpid(pid,os.WNOHANG)
6018
+ except Exception:pass
6019
+ done[0]=True
6020
+ signal.signal(signal.SIGCHLD,onchld)
6021
+ i=sys.stdin.fileno();o=sys.stdout.fileno()
6022
+ while not done[0]:
6023
+ try:r,_,_=select.select([i,m],[],[],0.1)
6024
+ except OSError as e:
6025
+ if e.errno==errno.EINTR:continue
6026
+ break
6027
+ if i in r:
6028
+ try:
6029
+ d=os.read(i,4096)
6030
+ if d:os.write(m,d)
6031
+ else:break
6032
+ except OSError:break
6033
+ if m in r:
6034
+ try:
6035
+ d=os.read(m,4096)
6036
+ if d:os.write(o,d)
6037
+ except OSError:done[0]=True
6038
+ try:os.kill(pid,signal.SIGTERM)
6039
+ except Exception:pass
6040
+ try:
6041
+ _,st=os.waitpid(pid,0)
6042
+ sys.exit((st>>8)&0xFF)
6043
+ except Exception:sys.exit(0)
6044
+ `;
6045
+ function fetchQuotaUsage(historySvc) {
6046
+ if (inProgress) return;
6047
+ inProgress = true;
6048
+ const claudeCmd = findInPath("claude") ? "claude" : "claude-code";
6049
+ if (!claudeCmd) {
6050
+ inProgress = false;
6051
+ return;
6052
+ }
6053
+ const helperPath = path9.join(os7.tmpdir(), "codeam-quota-helper.py");
6054
+ fs6.writeFileSync(helperPath, HELPER_SCRIPT, { mode: 420 });
6055
+ const python = findInPath("python3") ?? findInPath("python");
6056
+ if (!python) {
6057
+ inProgress = false;
6058
+ return;
6059
+ }
6060
+ const proc = (0, import_child_process4.spawn)(python, [helperPath, claudeCmd, "--tools", ""], {
6061
+ stdio: ["pipe", "pipe", "ignore"],
6062
+ cwd: process.cwd(),
6063
+ env: { ...process.env, TERM: "dumb", COLUMNS: "120", LINES: "30" }
6064
+ });
6065
+ let output = "";
6066
+ proc.stdout?.on("data", (chunk) => {
6067
+ output += chunk.toString("utf8");
6068
+ });
6069
+ setTimeout(() => {
6070
+ proc.stdin?.write("/usage\r");
6071
+ setTimeout(() => {
6072
+ const clean = output.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, " ").replace(/\s+/g, " ");
6073
+ const weekMatch = clean.match(/(\d+)%\s*used/i) || clean.match(/(\d+)\s*%/);
6074
+ if (weekMatch) historySvc.setQuotaPercent(parseInt(weekMatch[1], 10));
6075
+ const resetMatch = clean.match(/resets\s+(.+?)(?:\s*\(|$)/im);
6076
+ if (resetMatch) historySvc.setRateLimitReset(resetMatch[1].trim());
6077
+ try {
6078
+ proc.kill();
6079
+ } catch {
6080
+ }
6081
+ try {
6082
+ fs6.unlinkSync(helperPath);
6083
+ } catch {
6084
+ }
6085
+ inProgress = false;
6086
+ }, 5e3);
6087
+ }, 8e3);
6088
+ proc.on("exit", () => {
6089
+ inProgress = false;
6090
+ });
6091
+ setTimeout(() => {
6092
+ try {
6093
+ proc.kill();
6094
+ } catch {
6095
+ }
6096
+ }, 2e4);
6097
+ }
6098
+
6099
+ // src/commands/start/keep-alive.ts
6100
+ var import_child_process5 = require("child_process");
6101
+ function buildKeepAlive(ctx) {
6102
+ let timer = null;
6103
+ async function setIdleTimeout(minutes) {
6104
+ if (!ctx.inCodespace || !ctx.codespaceName) return;
6105
+ await new Promise((resolve2) => {
6106
+ const proc = (0, import_child_process5.spawn)(
6107
+ "gh",
6108
+ [
6109
+ "api",
6110
+ "-X",
6111
+ "PATCH",
6112
+ `/user/codespaces/${ctx.codespaceName}`,
6113
+ "-F",
6114
+ `idle_timeout_minutes=${minutes}`
6115
+ ],
6116
+ { stdio: "ignore", detached: true }
6117
+ );
6118
+ proc.unref();
6119
+ proc.on("exit", () => resolve2());
6120
+ proc.on("error", () => resolve2());
6121
+ });
6122
+ }
6123
+ return {
6124
+ apply(enabled) {
6125
+ if (timer) {
6126
+ clearInterval(timer);
6127
+ timer = null;
6128
+ }
6129
+ if (!ctx.inCodespace || !ctx.codespaceName) return;
6130
+ if (!enabled) {
6131
+ void setIdleTimeout(30);
6132
+ return;
6133
+ }
6134
+ void setIdleTimeout(240);
6135
+ timer = setInterval(() => {
6136
+ void setIdleTimeout(240);
6137
+ }, 30 * 60 * 1e3);
6138
+ }
6139
+ };
6140
+ }
6141
+
6142
+ // src/commands/start/handlers.ts
6143
+ var fs9 = __toESM(require("fs"));
6144
+ var os8 = __toESM(require("os"));
6145
+ var path12 = __toESM(require("path"));
6146
+ var import_crypto = require("crypto");
6147
+ var import_child_process7 = require("child_process");
6148
+
5837
6149
  // src/lib/payload.ts
5838
6150
  var import_zod2 = require("zod");
5839
6151
  var fileEntrySchema = import_zod2.z.object({
@@ -5869,8 +6181,8 @@ function parsePayload(schema, raw) {
5869
6181
  }
5870
6182
 
5871
6183
  // src/services/file-ops.service.ts
5872
- var fs6 = __toESM(require("fs/promises"));
5873
- var path9 = __toESM(require("path"));
6184
+ var fs7 = __toESM(require("fs/promises"));
6185
+ var path10 = __toESM(require("path"));
5874
6186
  var MAX_FILE_BYTES = 5 * 1024 * 1024;
5875
6187
  var MAX_WALK_DEPTH = 6;
5876
6188
  var MAX_VISITED_DIRS = 5e3;
@@ -5905,12 +6217,12 @@ var SUBDIR_IGNORE = /* @__PURE__ */ new Set([
5905
6217
  "__pycache__"
5906
6218
  ]);
5907
6219
  function isUnder(parent, candidate) {
5908
- const rel = path9.relative(parent, candidate);
5909
- return rel === "" || !rel.startsWith("..") && !path9.isAbsolute(rel);
6220
+ const rel = path10.relative(parent, candidate);
6221
+ return rel === "" || !rel.startsWith("..") && !path10.isAbsolute(rel);
5910
6222
  }
5911
6223
  async function isExistingFile(absPath) {
5912
6224
  try {
5913
- const stat3 = await fs6.stat(absPath);
6225
+ const stat3 = await fs7.stat(absPath);
5914
6226
  return stat3.isFile();
5915
6227
  } catch {
5916
6228
  return false;
@@ -5923,13 +6235,13 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
5923
6235
  ctx.visited++;
5924
6236
  let entries = [];
5925
6237
  try {
5926
- entries = await fs6.readdir(dir, { withFileTypes: true });
6238
+ entries = await fs7.readdir(dir, { withFileTypes: true });
5927
6239
  } catch {
5928
6240
  return;
5929
6241
  }
5930
6242
  for (const e of entries) {
5931
6243
  if (!e.isFile()) continue;
5932
- const full = path9.join(dir, e.name);
6244
+ const full = path10.join(dir, e.name);
5933
6245
  if (needleVariants.some((needle) => full.endsWith(needle))) {
5934
6246
  ctx.matches.push(full);
5935
6247
  if (ctx.matches.length >= ctx.cap) return;
@@ -5939,21 +6251,21 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
5939
6251
  if (!e.isDirectory()) continue;
5940
6252
  if (SUBDIR_IGNORE.has(e.name)) continue;
5941
6253
  if (e.name.startsWith(".") && SUBDIR_IGNORE.has(e.name)) continue;
5942
- await walkForSuffix(path9.join(dir, e.name), needleVariants, depth + 1, ctx);
6254
+ await walkForSuffix(path10.join(dir, e.name), needleVariants, depth + 1, ctx);
5943
6255
  if (ctx.matches.length >= ctx.cap) return;
5944
6256
  }
5945
6257
  }
5946
6258
  async function findFile(rawPath) {
5947
6259
  const cwd = process.cwd();
5948
- if (path9.isAbsolute(rawPath)) {
5949
- const abs = path9.normalize(rawPath);
6260
+ if (path10.isAbsolute(rawPath)) {
6261
+ const abs = path10.normalize(rawPath);
5950
6262
  if (isUnder(cwd, abs) && await isExistingFile(abs)) return abs;
5951
6263
  }
5952
- const direct = path9.resolve(cwd, rawPath);
6264
+ const direct = path10.resolve(cwd, rawPath);
5953
6265
  if (isUnder(cwd, direct) && await isExistingFile(direct)) return direct;
5954
- const normalized = path9.normalize(rawPath).replace(/^[./\\]+/, "");
6266
+ const normalized = path10.normalize(rawPath).replace(/^[./\\]+/, "");
5955
6267
  const needles = [
5956
- `${path9.sep}${normalized}`,
6268
+ `${path10.sep}${normalized}`,
5957
6269
  `/${normalized}`
5958
6270
  ].filter((v, i, a) => a.indexOf(v) === i);
5959
6271
  const ctx = { visited: 0, matches: [], cap: 16 };
@@ -5967,7 +6279,7 @@ async function findWriteTarget(rawPath) {
5967
6279
  const found = await findFile(rawPath);
5968
6280
  if (found) return found;
5969
6281
  const cwd = process.cwd();
5970
- const fallback = path9.isAbsolute(rawPath) ? path9.normalize(rawPath) : path9.resolve(cwd, rawPath);
6282
+ const fallback = path10.isAbsolute(rawPath) ? path10.normalize(rawPath) : path10.resolve(cwd, rawPath);
5971
6283
  if (!isUnder(cwd, fallback)) return null;
5972
6284
  return fallback;
5973
6285
  }
@@ -5984,11 +6296,11 @@ async function readProjectFile(rawPath) {
5984
6296
  if (!abs) {
5985
6297
  return { error: `File not found in the project tree: ${rawPath}` };
5986
6298
  }
5987
- const stat3 = await fs6.stat(abs);
6299
+ const stat3 = await fs7.stat(abs);
5988
6300
  if (stat3.size > MAX_FILE_BYTES) {
5989
6301
  return { error: `File too large (${(stat3.size / 1024 / 1024).toFixed(1)} MB > ${MAX_FILE_BYTES / 1024 / 1024} MB).` };
5990
6302
  }
5991
- const buf = await fs6.readFile(abs);
6303
+ const buf = await fs7.readFile(abs);
5992
6304
  if (looksBinary(buf)) {
5993
6305
  return { error: "Binary file \u2014 refusing to open in a code editor." };
5994
6306
  }
@@ -6007,8 +6319,8 @@ async function writeProjectFile(rawPath, content) {
6007
6319
  if (Buffer.byteLength(content, "utf-8") > MAX_FILE_BYTES) {
6008
6320
  return { error: "Content too large." };
6009
6321
  }
6010
- await fs6.mkdir(path9.dirname(abs), { recursive: true });
6011
- await fs6.writeFile(abs, content, "utf-8");
6322
+ await fs7.mkdir(path10.dirname(abs), { recursive: true });
6323
+ await fs7.writeFile(abs, content, "utf-8");
6012
6324
  return { ok: true };
6013
6325
  } catch (e) {
6014
6326
  const msg = e instanceof Error ? e.message : "Write failed";
@@ -6017,11 +6329,11 @@ async function writeProjectFile(rawPath, content) {
6017
6329
  }
6018
6330
 
6019
6331
  // src/services/project-ops.service.ts
6020
- var import_child_process4 = require("child_process");
6332
+ var import_child_process6 = require("child_process");
6021
6333
  var import_util = require("util");
6022
- var fs7 = __toESM(require("fs/promises"));
6023
- var path10 = __toESM(require("path"));
6024
- var execFileP = (0, import_util.promisify)(import_child_process4.execFile);
6334
+ var fs8 = __toESM(require("fs/promises"));
6335
+ var path11 = __toESM(require("path"));
6336
+ var execFileP = (0, import_util.promisify)(import_child_process6.execFile);
6025
6337
  var PROJECT_IGNORE = /* @__PURE__ */ new Set([
6026
6338
  "node_modules",
6027
6339
  ".git",
@@ -6068,7 +6380,7 @@ async function listProjectFiles(opts = {}) {
6068
6380
  }
6069
6381
  let entries = [];
6070
6382
  try {
6071
- entries = await fs7.readdir(dir, { withFileTypes: true });
6383
+ entries = await fs8.readdir(dir, { withFileTypes: true });
6072
6384
  } catch {
6073
6385
  return;
6074
6386
  }
@@ -6078,18 +6390,18 @@ async function listProjectFiles(opts = {}) {
6078
6390
  return;
6079
6391
  }
6080
6392
  if (PROJECT_IGNORE.has(e.name)) continue;
6081
- const full = path10.join(dir, e.name);
6393
+ const full = path11.join(dir, e.name);
6082
6394
  if (e.isDirectory()) {
6083
6395
  if (depth >= 12) continue;
6084
6396
  await walk(full, depth + 1);
6085
6397
  } else if (e.isFile()) {
6086
- const rel = path10.relative(root, full);
6398
+ const rel = path11.relative(root, full);
6087
6399
  if (q2 && !rel.toLowerCase().includes(q2) && !e.name.toLowerCase().includes(q2)) {
6088
6400
  continue;
6089
6401
  }
6090
6402
  let size = 0;
6091
6403
  try {
6092
- const st3 = await fs7.stat(full);
6404
+ const st3 = await fs8.stat(full);
6093
6405
  size = st3.size;
6094
6406
  } catch {
6095
6407
  }
@@ -6191,8 +6503,8 @@ async function gitStatus(cwd) {
6191
6503
  let hasMergeInProgress = false;
6192
6504
  try {
6193
6505
  const gitDir = (await git(["rev-parse", "--git-dir"], root)).stdout.trim();
6194
- const mergeHead = path10.isAbsolute(gitDir) ? path10.join(gitDir, "MERGE_HEAD") : path10.join(root, gitDir, "MERGE_HEAD");
6195
- await fs7.access(mergeHead);
6506
+ const mergeHead = path11.isAbsolute(gitDir) ? path11.join(gitDir, "MERGE_HEAD") : path11.join(root, gitDir, "MERGE_HEAD");
6507
+ await fs8.access(mergeHead);
6196
6508
  hasMergeInProgress = true;
6197
6509
  } catch {
6198
6510
  }
@@ -6265,15 +6577,261 @@ async function gitResolve(file, side, cwd) {
6265
6577
  return { ok: true };
6266
6578
  }
6267
6579
 
6268
- // src/commands/start.ts
6580
+ // src/commands/start/handlers.ts
6269
6581
  function saveFilesTemp(files) {
6270
6582
  return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
6271
6583
  const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
6272
- const tmpPath = path11.join(os7.tmpdir(), `codeam-${(0, import_crypto.randomUUID)()}-${safeName}`);
6273
- fs8.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
6584
+ const tmpPath = path12.join(os8.tmpdir(), `codeam-${(0, import_crypto.randomUUID)()}-${safeName}`);
6585
+ fs9.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
6274
6586
  return tmpPath;
6275
6587
  });
6276
6588
  }
6589
+ function dispatchPrompt(ctx, prompt) {
6590
+ ctx.outputSvc.newTurn();
6591
+ ctx.claude.sendCommand(prompt);
6592
+ }
6593
+ var startTask = (ctx, _cmd, parsed) => {
6594
+ const { prompt, files } = parsed;
6595
+ const effectivePrompt = prompt ?? "";
6596
+ if (files && files.length > 0) {
6597
+ const paths = saveFilesTemp(files);
6598
+ const atRefs = paths.map((p2) => `@${p2}`).join(" ");
6599
+ ctx.outputSvc.newTurn();
6600
+ ctx.claude.sendCommand(`${atRefs} ${effectivePrompt}`.trim());
6601
+ setTimeout(() => {
6602
+ for (const p2 of paths) {
6603
+ try {
6604
+ fs9.unlinkSync(p2);
6605
+ } catch {
6606
+ }
6607
+ }
6608
+ }, 12e4);
6609
+ } else if (effectivePrompt) {
6610
+ dispatchPrompt(ctx, effectivePrompt);
6611
+ }
6612
+ };
6613
+ var provideInput = (ctx, _cmd, parsed) => {
6614
+ if (parsed.input) dispatchPrompt(ctx, parsed.input);
6615
+ };
6616
+ var selectOption = (ctx, _cmd, parsed) => {
6617
+ const index = parsed.index ?? 0;
6618
+ const from = parsed.from ?? 0;
6619
+ ctx.outputSvc.newTurn();
6620
+ ctx.claude.selectOption(index, from);
6621
+ };
6622
+ var escapeKey = (ctx) => {
6623
+ ctx.outputSvc.newTurn();
6624
+ ctx.claude.sendEscape();
6625
+ };
6626
+ var stopTask = (ctx) => {
6627
+ ctx.claude.interrupt();
6628
+ };
6629
+ var resumeSession = async (ctx, _cmd, parsed) => {
6630
+ const { id, auto } = parsed;
6631
+ if (!id) return;
6632
+ ctx.historySvc.setCurrentConversationId(id);
6633
+ await ctx.historySvc.loadConversation(id);
6634
+ await ctx.outputSvc.newTurnResume(id);
6635
+ ctx.claude.restart(id, auto ?? false);
6636
+ };
6637
+ var getContext = async (ctx, cmd) => {
6638
+ const usage = ctx.historySvc.getCurrentUsage();
6639
+ const monthlyCost = ctx.historySvc.getMonthlyEstimatedCost();
6640
+ const rateLimitReset = ctx.historySvc.getRateLimitReset();
6641
+ const quotaPercent = ctx.historySvc.getQuotaPercent();
6642
+ const base = usage ? { ...usage, monthlyCost } : { used: 0, total: 2e5, percent: 0, model: null, outputTokens: 0, cacheReadTokens: 0, monthlyCost, error: "No usage data found" };
6643
+ const result = {
6644
+ ...base,
6645
+ ...rateLimitReset ? { rateLimitReset } : {},
6646
+ ...quotaPercent !== null ? { quotaPercent } : {}
6647
+ };
6648
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6649
+ };
6650
+ var getConversation = async (ctx, cmd) => {
6651
+ const currentId = ctx.historySvc.getCurrentConversationId();
6652
+ if (!currentId) {
6653
+ await ctx.relay.sendResult(cmd.id, "completed", { conversationId: null });
6654
+ return;
6655
+ }
6656
+ try {
6657
+ await ctx.historySvc.loadConversation(currentId);
6658
+ await ctx.relay.sendResult(cmd.id, "completed", { conversationId: currentId });
6659
+ } catch {
6660
+ await ctx.relay.sendResult(cmd.id, "failed", {});
6661
+ }
6662
+ };
6663
+ var listModels = async (ctx, cmd) => {
6664
+ const models = [
6665
+ { id: "claude-opus-4-7", label: "Claude Opus 4.7", description: "Most capable", family: "claude", vendor: "anthropic", isDefault: false },
6666
+ { id: "claude-opus-4-6", label: "Claude Opus 4.6", description: "Top tier", family: "claude", vendor: "anthropic", isDefault: false },
6667
+ { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", description: "Balanced", family: "claude", vendor: "anthropic", isDefault: true },
6668
+ { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5", description: "Fastest", family: "claude", vendor: "anthropic", isDefault: false }
6669
+ ];
6670
+ await ctx.relay.sendResult(cmd.id, "completed", { models });
6671
+ };
6672
+ var setKeepAlive = async (ctx, cmd) => {
6673
+ const enabled = !!cmd.payload.enabled;
6674
+ ctx.setKeepAlive(enabled);
6675
+ try {
6676
+ await ctx.relay.sendResult(
6677
+ cmd.id,
6678
+ "success",
6679
+ {
6680
+ enabled,
6681
+ applied: enabled && ctx.keepAliveCtx.inCodespace,
6682
+ runtime: ctx.keepAliveCtx.inCodespace ? "github-codespaces" : "local"
6683
+ }
6684
+ );
6685
+ } catch {
6686
+ }
6687
+ };
6688
+ var sessionTerminated = (ctx) => {
6689
+ showInfo("Session was deleted from the app \u2014 exiting.");
6690
+ try {
6691
+ ctx.claude.kill();
6692
+ } catch {
6693
+ }
6694
+ try {
6695
+ const proc = (0, import_child_process7.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
6696
+ detached: true,
6697
+ stdio: "ignore"
6698
+ });
6699
+ proc.unref();
6700
+ } catch {
6701
+ }
6702
+ ctx.outputSvc.dispose();
6703
+ ctx.relay.stop();
6704
+ process.exit(0);
6705
+ };
6706
+ var shutdownSession = async (ctx, cmd) => {
6707
+ try {
6708
+ await ctx.relay.sendResult(cmd.id, "success", { ok: true });
6709
+ } catch {
6710
+ }
6711
+ try {
6712
+ ctx.claude.kill();
6713
+ } catch {
6714
+ }
6715
+ if (ctx.keepAliveCtx.inCodespace && ctx.keepAliveCtx.codespaceName) {
6716
+ try {
6717
+ const stopProc = (0, import_child_process7.spawn)(
6718
+ "bash",
6719
+ ["-lc", `sleep 1; gh codespace stop -c ${JSON.stringify(ctx.keepAliveCtx.codespaceName)} >/dev/null 2>&1 || true`],
6720
+ { detached: true, stdio: "ignore" }
6721
+ );
6722
+ stopProc.unref();
6723
+ } catch {
6724
+ }
6725
+ }
6726
+ try {
6727
+ const proc = (0, import_child_process7.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
6728
+ detached: true,
6729
+ stdio: "ignore"
6730
+ });
6731
+ proc.unref();
6732
+ } catch {
6733
+ }
6734
+ ctx.outputSvc.dispose();
6735
+ ctx.relay.stop();
6736
+ process.exit(0);
6737
+ };
6738
+ var readFile2 = async (ctx, cmd, parsed) => {
6739
+ if (!parsed.path) {
6740
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path" });
6741
+ return;
6742
+ }
6743
+ const result = await readProjectFile(parsed.path);
6744
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6745
+ };
6746
+ var writeFile2 = async (ctx, cmd, parsed) => {
6747
+ if (!parsed.path || typeof parsed.content !== "string") {
6748
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path or content" });
6749
+ return;
6750
+ }
6751
+ const result = await writeProjectFile(parsed.path, parsed.content);
6752
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6753
+ };
6754
+ var listFiles = async (ctx, cmd, parsed) => {
6755
+ const result = await listProjectFiles({ query: parsed.query });
6756
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6757
+ };
6758
+ var gitStatusH = async (ctx, cmd) => {
6759
+ const result = await gitStatus();
6760
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6761
+ };
6762
+ var gitDiffH = async (ctx, cmd, parsed) => {
6763
+ const result = await gitDiff(parsed.path ?? null);
6764
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6765
+ };
6766
+ var gitDiffStagedH = async (ctx, cmd, parsed) => {
6767
+ const result = await gitDiffStaged(parsed.path ?? null);
6768
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6769
+ };
6770
+ var gitLogH = async (ctx, cmd, parsed) => {
6771
+ const result = await gitLog(parsed.limit ?? 30);
6772
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6773
+ };
6774
+ var gitCommitH = async (ctx, cmd, parsed) => {
6775
+ if (!parsed.message) {
6776
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing message" });
6777
+ return;
6778
+ }
6779
+ const result = await gitCommit(parsed.message, parsed.paths);
6780
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6781
+ };
6782
+ var gitPushH = async (ctx, cmd) => {
6783
+ const result = await gitPush();
6784
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6785
+ };
6786
+ var gitPullH = async (ctx, cmd) => {
6787
+ const result = await gitPull();
6788
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6789
+ };
6790
+ var gitResolveH = async (ctx, cmd, parsed) => {
6791
+ if (!parsed.path || !parsed.side) {
6792
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path or side" });
6793
+ return;
6794
+ }
6795
+ const result = await gitResolve(parsed.path, parsed.side);
6796
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6797
+ };
6798
+ var handlers = {
6799
+ start_task: startTask,
6800
+ provide_input: provideInput,
6801
+ select_option: selectOption,
6802
+ escape_key: escapeKey,
6803
+ stop_task: stopTask,
6804
+ resume_session: resumeSession,
6805
+ get_context: getContext,
6806
+ get_conversation: getConversation,
6807
+ list_models: listModels,
6808
+ set_keep_alive: setKeepAlive,
6809
+ session_terminated: sessionTerminated,
6810
+ shutdown_session: shutdownSession,
6811
+ read_file: readFile2,
6812
+ write_file: writeFile2,
6813
+ list_files: listFiles,
6814
+ git_status: gitStatusH,
6815
+ git_diff: gitDiffH,
6816
+ git_diff_staged: gitDiffStagedH,
6817
+ git_log: gitLogH,
6818
+ git_commit: gitCommitH,
6819
+ git_push: gitPushH,
6820
+ git_pull: gitPullH,
6821
+ git_resolve: gitResolveH
6822
+ };
6823
+ async function dispatchCommand(ctx, cmd) {
6824
+ const parsed = parsePayload(startCommandSchema, cmd.payload);
6825
+ if (!parsed) {
6826
+ showInfo(`Ignoring malformed ${cmd.type} payload.`);
6827
+ return;
6828
+ }
6829
+ const handler = handlers[cmd.type];
6830
+ if (!handler) return;
6831
+ await handler(ctx, cmd, parsed);
6832
+ }
6833
+
6834
+ // src/commands/start.ts
6277
6835
  async function start() {
6278
6836
  showIntro();
6279
6837
  const session = getActiveSession();
@@ -6287,434 +6845,32 @@ async function start() {
6287
6845
  showInfo(`${session.userName} \xB7 ${import_picocolors2.default.cyan(session.plan)}`);
6288
6846
  showInfo("Launching Claude Code...\n");
6289
6847
  const cwd = process.cwd();
6290
- const ws = new WebSocketService(session.id, pluginId);
6291
6848
  const historySvc = new HistoryService(pluginId, cwd);
6292
- let quotaFetchInProgress = false;
6293
- function fetchQuotaUsage() {
6294
- if (quotaFetchInProgress) return;
6295
- quotaFetchInProgress = true;
6296
- const claudeCmd = findInPath("claude") ? "claude" : "claude-code";
6297
- if (!claudeCmd) {
6298
- quotaFetchInProgress = false;
6299
- return;
6300
- }
6301
- const helperScript = `import os,pty,sys,select,signal,struct,fcntl,termios,errno
6302
- m,s=pty.openpty()
6303
- try:
6304
- fcntl.ioctl(s,termios.TIOCSWINSZ,struct.pack('HHHH',30,120,0,0))
6305
- except Exception:pass
6306
- pid=os.fork()
6307
- if pid==0:
6308
- os.close(m);os.setsid()
6309
- try:fcntl.ioctl(s,termios.TIOCSCTTY,0)
6310
- except Exception:pass
6311
- for fd in[0,1,2]:os.dup2(s,fd)
6312
- if s>2:os.close(s)
6313
- os.execvp(sys.argv[1],sys.argv[1:])
6314
- sys.exit(127)
6315
- os.close(s)
6316
- done=[False]
6317
- def onchld(n,f):
6318
- try:os.waitpid(pid,os.WNOHANG)
6319
- except Exception:pass
6320
- done[0]=True
6321
- signal.signal(signal.SIGCHLD,onchld)
6322
- i=sys.stdin.fileno();o=sys.stdout.fileno()
6323
- while not done[0]:
6324
- try:r,_,_=select.select([i,m],[],[],0.1)
6325
- except OSError as e:
6326
- if e.errno==errno.EINTR:continue
6327
- break
6328
- if i in r:
6329
- try:
6330
- d=os.read(i,4096)
6331
- if d:os.write(m,d)
6332
- else:break
6333
- except OSError:break
6334
- if m in r:
6335
- try:
6336
- d=os.read(m,4096)
6337
- if d:os.write(o,d)
6338
- except OSError:done[0]=True
6339
- try:os.kill(pid,signal.SIGTERM)
6340
- except Exception:pass
6341
- try:
6342
- _,st=os.waitpid(pid,0)
6343
- sys.exit((st>>8)&0xFF)
6344
- except Exception:sys.exit(0)
6345
- `;
6346
- const helperPath = path11.join(os7.tmpdir(), "codeam-quota-helper.py");
6347
- fs8.writeFileSync(helperPath, helperScript, { mode: 420 });
6348
- const python = findInPath("python3") ?? findInPath("python");
6349
- if (!python) {
6350
- quotaFetchInProgress = false;
6351
- return;
6352
- }
6353
- const proc = (0, import_child_process5.spawn)(python, [helperPath, claudeCmd, "--tools", ""], {
6354
- stdio: ["pipe", "pipe", "ignore"],
6355
- cwd: process.cwd(),
6356
- env: { ...process.env, TERM: "dumb", COLUMNS: "120", LINES: "30" }
6357
- });
6358
- let output = "";
6359
- proc.stdout?.on("data", (chunk) => {
6360
- output += chunk.toString("utf8");
6361
- });
6362
- setTimeout(() => {
6363
- proc.stdin?.write("/usage\r");
6849
+ const keepAliveCtx = {
6850
+ inCodespace: process.env.CODESPACES === "true",
6851
+ codespaceName: process.env.CODESPACE_NAME
6852
+ };
6853
+ const { apply: setKeepAlive2 } = buildKeepAlive(keepAliveCtx);
6854
+ const outputSvc = new OutputService(
6855
+ session.id,
6856
+ pluginId,
6857
+ (conversationId) => historySvc.setCurrentConversationId(conversationId),
6858
+ (reset) => historySvc.setRateLimitReset(reset),
6859
+ () => {
6860
+ if (historySvc.isQuotaStale()) fetchQuotaUsage(historySvc);
6364
6861
  setTimeout(() => {
6365
- const clean = output.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, " ").replace(/\s+/g, " ");
6366
- const weekMatch = clean.match(/(\d+)%\s*used/i) || clean.match(/(\d+)\s*%/);
6367
- if (weekMatch) {
6368
- historySvc.setQuotaPercent(parseInt(weekMatch[1], 10));
6369
- }
6370
- const resetMatch = clean.match(/resets\s+(.+?)(?:\s*\(|$)/im);
6371
- if (resetMatch) {
6372
- historySvc.setRateLimitReset(resetMatch[1].trim());
6373
- }
6374
- try {
6375
- proc.kill();
6376
- } catch {
6377
- }
6378
- try {
6379
- fs8.unlinkSync(helperPath);
6380
- } catch {
6381
- }
6382
- quotaFetchInProgress = false;
6383
- }, 5e3);
6384
- }, 8e3);
6385
- proc.on("exit", () => {
6386
- quotaFetchInProgress = false;
6387
- });
6388
- setTimeout(() => {
6389
- try {
6390
- proc.kill();
6391
- } catch {
6392
- }
6393
- }, 2e4);
6394
- }
6395
- const outputSvc = new OutputService(session.id, pluginId, (conversationId) => {
6396
- historySvc.setCurrentConversationId(conversationId);
6397
- }, (reset) => {
6398
- historySvc.setRateLimitReset(reset);
6399
- }, () => {
6400
- if (historySvc.isQuotaStale()) {
6401
- fetchQuotaUsage();
6402
- }
6403
- setTimeout(() => {
6404
- historySvc.uploadDelta().catch(() => {
6405
- });
6406
- }, 400);
6407
- }, () => {
6408
- const prevCount = historySvc.getCurrentMessageCount();
6409
- historySvc.waitForNewUserMessage(prevCount).then((userText) => outputSvc.startTerminalTurn(userText ?? void 0)).catch(() => outputSvc.startTerminalTurn(void 0));
6410
- }, session.pluginAuthToken);
6411
- function sendPrompt(prompt) {
6412
- outputSvc.newTurn();
6413
- claude.sendCommand(prompt);
6414
- }
6415
- const relay = new CommandRelayService(pluginId, async (cmd) => {
6416
- const parsed = parsePayload(startCommandSchema, cmd.payload);
6417
- if (!parsed) {
6418
- showInfo(`Ignoring malformed ${cmd.type} payload.`);
6419
- return;
6420
- }
6421
- switch (cmd.type) {
6422
- case "start_task": {
6423
- const { prompt, files } = parsed;
6424
- const effectivePrompt = prompt ?? "";
6425
- if (files && files.length > 0) {
6426
- const paths = saveFilesTemp(files);
6427
- const atRefs = paths.map((p2) => `@${p2}`).join(" ");
6428
- outputSvc.newTurn();
6429
- claude.sendCommand(`${atRefs} ${effectivePrompt}`.trim());
6430
- setTimeout(() => {
6431
- for (const p2 of paths) {
6432
- try {
6433
- fs8.unlinkSync(p2);
6434
- } catch {
6435
- }
6436
- }
6437
- }, 12e4);
6438
- } else if (effectivePrompt) {
6439
- sendPrompt(effectivePrompt);
6440
- }
6441
- break;
6442
- }
6443
- case "provide_input": {
6444
- const { input } = parsed;
6445
- if (input) sendPrompt(input);
6446
- break;
6447
- }
6448
- case "select_option": {
6449
- const index = parsed.index ?? 0;
6450
- const from = parsed.from ?? 0;
6451
- outputSvc.newTurn();
6452
- claude.selectOption(index, from);
6453
- break;
6454
- }
6455
- case "escape_key":
6456
- outputSvc.newTurn();
6457
- claude.sendEscape();
6458
- break;
6459
- case "stop_task":
6460
- claude.interrupt();
6461
- break;
6462
- case "set_keep_alive": {
6463
- const enabled = !!cmd.payload.enabled;
6464
- const inCodespaceEnv = process.env.CODESPACES === "true";
6465
- setKeepAlive(enabled);
6466
- try {
6467
- await relay.sendResult(
6468
- cmd.id,
6469
- "success",
6470
- { enabled, applied: enabled && inCodespaceEnv, runtime: inCodespaceEnv ? "github-codespaces" : "local" }
6471
- );
6472
- } catch {
6473
- }
6474
- break;
6475
- }
6476
- case "session_terminated": {
6477
- showInfo("Session was deleted from the app \u2014 exiting.");
6478
- try {
6479
- claude.kill();
6480
- } catch {
6481
- }
6482
- try {
6483
- const proc = (0, import_child_process5.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
6484
- detached: true,
6485
- stdio: "ignore"
6486
- });
6487
- proc.unref();
6488
- } catch {
6489
- }
6490
- outputSvc.dispose();
6491
- relay.stop();
6492
- ws.disconnect();
6493
- process.exit(0);
6494
- }
6495
- case "shutdown_session": {
6496
- try {
6497
- await relay.sendResult(cmd.id, "success", { ok: true });
6498
- } catch {
6499
- }
6500
- try {
6501
- claude.kill();
6502
- } catch {
6503
- }
6504
- const codespaceName2 = process.env.CODESPACE_NAME;
6505
- if (codespaceName2 && process.env.CODESPACES === "true") {
6506
- try {
6507
- const stopProc = (0, import_child_process5.spawn)(
6508
- "bash",
6509
- ["-lc", `sleep 1; gh codespace stop -c ${JSON.stringify(codespaceName2)} >/dev/null 2>&1 || true`],
6510
- { detached: true, stdio: "ignore" }
6511
- );
6512
- stopProc.unref();
6513
- } catch {
6514
- }
6515
- }
6516
- try {
6517
- const proc = (0, import_child_process5.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
6518
- detached: true,
6519
- stdio: "ignore"
6520
- });
6521
- proc.unref();
6522
- } catch {
6523
- }
6524
- outputSvc.dispose();
6525
- relay.stop();
6526
- ws.disconnect();
6527
- process.exit(0);
6528
- }
6529
- case "get_context": {
6530
- const usage = historySvc.getCurrentUsage();
6531
- const monthlyCost = historySvc.getMonthlyEstimatedCost();
6532
- const rateLimitReset = historySvc.getRateLimitReset();
6533
- const quotaPercent = historySvc.getQuotaPercent();
6534
- const base = usage ? { ...usage, monthlyCost } : { used: 0, total: 2e5, percent: 0, model: null, outputTokens: 0, cacheReadTokens: 0, monthlyCost, error: "No usage data found" };
6535
- const result = { ...base, ...rateLimitReset ? { rateLimitReset } : {}, ...quotaPercent !== null ? { quotaPercent } : {} };
6536
- await relay.sendResult(cmd.id, "completed", result);
6537
- break;
6538
- }
6539
- case "resume_session": {
6540
- const { id, auto } = parsed;
6541
- if (!id) break;
6542
- historySvc.setCurrentConversationId(id);
6543
- await historySvc.loadConversation(id);
6544
- await outputSvc.newTurnResume(id);
6545
- claude.restart(id, auto ?? false);
6546
- break;
6547
- }
6548
- case "get_conversation": {
6549
- const currentId = historySvc.getCurrentConversationId();
6550
- if (currentId) {
6551
- try {
6552
- await historySvc.loadConversation(currentId);
6553
- await relay.sendResult(cmd.id, "completed", { conversationId: currentId });
6554
- } catch {
6555
- await relay.sendResult(cmd.id, "failed", {});
6556
- }
6557
- } else {
6558
- await relay.sendResult(cmd.id, "completed", { conversationId: null });
6559
- }
6560
- break;
6561
- }
6562
- case "list_models": {
6563
- const models = [
6564
- { id: "claude-opus-4-7", label: "Claude Opus 4.7", description: "Most capable", family: "claude", vendor: "anthropic", isDefault: false },
6565
- { id: "claude-opus-4-6", label: "Claude Opus 4.6", description: "Top tier", family: "claude", vendor: "anthropic", isDefault: false },
6566
- { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", description: "Balanced", family: "claude", vendor: "anthropic", isDefault: true },
6567
- { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5", description: "Fastest", family: "claude", vendor: "anthropic", isDefault: false }
6568
- ];
6569
- await relay.sendResult(cmd.id, "completed", { models });
6570
- break;
6571
- }
6572
- case "read_file": {
6573
- const { path: filePath } = parsed;
6574
- if (!filePath) {
6575
- await relay.sendResult(cmd.id, "failed", { error: "Missing path" });
6576
- break;
6577
- }
6578
- const result = await readProjectFile(filePath);
6579
- await relay.sendResult(cmd.id, "completed", result);
6580
- break;
6581
- }
6582
- case "write_file": {
6583
- const { path: filePath, content } = parsed;
6584
- if (!filePath || typeof content !== "string") {
6585
- await relay.sendResult(cmd.id, "failed", { error: "Missing path or content" });
6586
- break;
6587
- }
6588
- const result = await writeProjectFile(filePath, content);
6589
- await relay.sendResult(cmd.id, "completed", result);
6590
- break;
6591
- }
6592
- case "list_files": {
6593
- const result = await listProjectFiles({ query: parsed.query });
6594
- await relay.sendResult(cmd.id, "completed", result);
6595
- break;
6596
- }
6597
- case "git_status": {
6598
- const result = await gitStatus();
6599
- await relay.sendResult(cmd.id, "completed", result);
6600
- break;
6601
- }
6602
- case "git_diff": {
6603
- const { path: filePath } = parsed;
6604
- const result = await gitDiff(filePath ?? null);
6605
- await relay.sendResult(cmd.id, "completed", result);
6606
- break;
6607
- }
6608
- case "git_diff_staged": {
6609
- const { path: filePath } = parsed;
6610
- const result = await gitDiffStaged(filePath ?? null);
6611
- await relay.sendResult(cmd.id, "completed", result);
6612
- break;
6613
- }
6614
- case "git_log": {
6615
- const result = await gitLog(parsed.limit ?? 30);
6616
- await relay.sendResult(cmd.id, "completed", result);
6617
- break;
6618
- }
6619
- case "git_commit": {
6620
- if (!parsed.message) {
6621
- await relay.sendResult(cmd.id, "failed", { error: "Missing message" });
6622
- break;
6623
- }
6624
- const result = await gitCommit(parsed.message, parsed.paths);
6625
- await relay.sendResult(cmd.id, "completed", result);
6626
- break;
6627
- }
6628
- case "git_push": {
6629
- const result = await gitPush();
6630
- await relay.sendResult(cmd.id, "completed", result);
6631
- break;
6632
- }
6633
- case "git_pull": {
6634
- const result = await gitPull();
6635
- await relay.sendResult(cmd.id, "completed", result);
6636
- break;
6637
- }
6638
- case "git_resolve": {
6639
- const { path: filePath, side } = parsed;
6640
- if (!filePath || !side) {
6641
- await relay.sendResult(cmd.id, "failed", { error: "Missing path or side" });
6642
- break;
6643
- }
6644
- const result = await gitResolve(filePath, side);
6645
- await relay.sendResult(cmd.id, "completed", result);
6646
- break;
6647
- }
6648
- }
6649
- });
6650
- ws.addHandler({
6651
- onConnected() {
6862
+ historySvc.uploadDelta().catch(() => {
6863
+ });
6864
+ }, 400);
6652
6865
  },
6653
- onDisconnected() {
6866
+ () => {
6867
+ const prevCount = historySvc.getCurrentMessageCount();
6868
+ historySvc.waitForNewUserMessage(prevCount).then((userText) => outputSvc.startTerminalTurn(userText ?? void 0)).catch(() => outputSvc.startTerminalTurn(void 0));
6654
6869
  },
6655
- onMessage(type, payload) {
6656
- if (type !== "agent_command") return;
6657
- const cmdType = typeof payload.type === "string" ? payload.type : null;
6658
- if (!cmdType) return;
6659
- const parsed = parsePayload(startCommandSchema, payload.payload ?? {});
6660
- if (!parsed) {
6661
- showInfo(`Ignoring malformed ${cmdType} payload (ws).`);
6662
- return;
6663
- }
6664
- if (cmdType === "start_task") {
6665
- const { prompt, files } = parsed;
6666
- const effectivePrompt = prompt ?? "";
6667
- if (files && files.length > 0) {
6668
- const paths = saveFilesTemp(files);
6669
- const atRefs = paths.map((p2) => `@${p2}`).join(" ");
6670
- outputSvc.newTurn();
6671
- claude.sendCommand(`${atRefs} ${effectivePrompt}`.trim());
6672
- setTimeout(() => {
6673
- for (const p2 of paths) {
6674
- try {
6675
- fs8.unlinkSync(p2);
6676
- } catch {
6677
- }
6678
- }
6679
- }, 12e4);
6680
- } else if (effectivePrompt) {
6681
- sendPrompt(effectivePrompt);
6682
- }
6683
- } else if (cmdType === "provide_input") {
6684
- const { input } = parsed;
6685
- if (input) sendPrompt(input);
6686
- } else if (cmdType === "select_option") {
6687
- const index = parsed.index ?? 0;
6688
- const from = parsed.from ?? 0;
6689
- outputSvc.newTurn();
6690
- claude.selectOption(index, from);
6691
- } else if (cmdType === "escape_key") {
6692
- outputSvc.newTurn();
6693
- claude.sendEscape();
6694
- } else if (cmdType === "stop_task") {
6695
- claude.interrupt();
6696
- } else if (cmdType === "get_conversation") {
6697
- const currentId = historySvc.getCurrentConversationId();
6698
- if (currentId) {
6699
- historySvc.loadConversation(currentId).catch(() => {
6700
- });
6701
- }
6702
- } else if (cmdType === "resume_session") {
6703
- const { id, auto } = parsed;
6704
- if (id) {
6705
- const autoFlag = auto ?? false;
6706
- historySvc.loadConversation(id).then(() => outputSvc.newTurnResume(id)).then(() => {
6707
- claude.restart(id, autoFlag);
6708
- }).catch(() => {
6709
- });
6710
- }
6711
- }
6712
- }
6713
- });
6714
- ws.connect();
6715
- relay.start();
6870
+ session.pluginAuthToken
6871
+ );
6716
6872
  const claude = new ClaudeService({
6717
- cwd: process.cwd(),
6873
+ cwd,
6718
6874
  onData(raw) {
6719
6875
  outputSvc.push(raw);
6720
6876
  },
@@ -6722,19 +6878,30 @@ except Exception:sys.exit(0)
6722
6878
  process.removeListener("SIGINT", sigintHandler);
6723
6879
  outputSvc.dispose();
6724
6880
  relay.stop();
6725
- ws.disconnect();
6726
6881
  process.exit(code);
6727
6882
  }
6728
6883
  });
6884
+ const ctx = {
6885
+ outputSvc,
6886
+ claude,
6887
+ historySvc,
6888
+ relay: void 0,
6889
+ setKeepAlive: setKeepAlive2,
6890
+ keepAliveCtx
6891
+ };
6892
+ const relay = new CommandRelayService(pluginId, async (cmd) => {
6893
+ await dispatchCommand(ctx, cmd);
6894
+ });
6895
+ ctx.relay = relay;
6729
6896
  function sigintHandler() {
6730
6897
  claude.kill();
6731
6898
  outputSvc.dispose();
6732
6899
  relay.stop();
6733
- ws.disconnect();
6734
6900
  process.exit(0);
6735
6901
  }
6736
6902
  process.once("SIGINT", sigintHandler);
6737
6903
  await claude.spawn();
6904
+ relay.start();
6738
6905
  setTimeout(() => {
6739
6906
  historySvc.detectCurrentConversation();
6740
6907
  historySvc.load().catch(() => {
@@ -6745,47 +6912,7 @@ except Exception:sys.exit(0)
6745
6912
  });
6746
6913
  }
6747
6914
  }, 2e3);
6748
- setTimeout(() => {
6749
- fetchQuotaUsage();
6750
- }, 5e3);
6751
- const inCodespace = process.env.CODESPACES === "true";
6752
- const codespaceName = process.env.CODESPACE_NAME;
6753
- let keepAliveTimer = null;
6754
- async function setIdleTimeout(minutes) {
6755
- if (!inCodespace || !codespaceName) return;
6756
- await new Promise((resolve2) => {
6757
- const proc = (0, import_child_process5.spawn)(
6758
- "gh",
6759
- [
6760
- "api",
6761
- "-X",
6762
- "PATCH",
6763
- `/user/codespaces/${codespaceName}`,
6764
- "-F",
6765
- `idle_timeout_minutes=${minutes}`
6766
- ],
6767
- { stdio: "ignore", detached: true }
6768
- );
6769
- proc.unref();
6770
- proc.on("exit", () => resolve2());
6771
- proc.on("error", () => resolve2());
6772
- });
6773
- }
6774
- function setKeepAlive(enabled) {
6775
- if (keepAliveTimer) {
6776
- clearInterval(keepAliveTimer);
6777
- keepAliveTimer = null;
6778
- }
6779
- if (!inCodespace || !codespaceName) return;
6780
- if (!enabled) {
6781
- void setIdleTimeout(30);
6782
- return;
6783
- }
6784
- void setIdleTimeout(240);
6785
- keepAliveTimer = setInterval(() => {
6786
- void setIdleTimeout(240);
6787
- }, 30 * 60 * 1e3);
6788
- }
6915
+ setTimeout(() => fetchQuotaUsage(historySvc), 5e3);
6789
6916
  }
6790
6917
 
6791
6918
  // src/commands/pair.ts
@@ -6966,19 +7093,19 @@ async function logout() {
6966
7093
  }
6967
7094
 
6968
7095
  // src/commands/deploy.ts
6969
- var import_child_process10 = require("child_process");
6970
- var fs9 = __toESM(require("fs"));
6971
- var os8 = __toESM(require("os"));
6972
- var path16 = __toESM(require("path"));
7096
+ var import_child_process12 = require("child_process");
7097
+ var fs10 = __toESM(require("fs"));
7098
+ var os9 = __toESM(require("os"));
7099
+ var path17 = __toESM(require("path"));
6973
7100
  var import_util6 = require("util");
6974
7101
  var import_picocolors9 = __toESM(require("picocolors"));
6975
7102
 
6976
7103
  // src/services/providers/github-codespaces.ts
6977
- var import_child_process6 = require("child_process");
7104
+ var import_child_process8 = require("child_process");
6978
7105
  var import_util2 = require("util");
6979
7106
  var import_picocolors7 = __toESM(require("picocolors"));
6980
- var path12 = __toESM(require("path"));
6981
- var execFileP2 = (0, import_util2.promisify)(import_child_process6.execFile);
7107
+ var path13 = __toESM(require("path"));
7108
+ var execFileP2 = (0, import_util2.promisify)(import_child_process8.execFile);
6982
7109
  var MAX_BUFFER = 8 * 1024 * 1024;
6983
7110
  function resetStdinForChild() {
6984
7111
  if (process.stdin.isTTY) {
@@ -7022,7 +7149,7 @@ var GitHubCodespacesProvider = class {
7022
7149
  if (!isAuthed) {
7023
7150
  resetStdinForChild();
7024
7151
  await new Promise((resolve2, reject) => {
7025
- const proc = (0, import_child_process6.spawn)("gh", ["auth", "login", "-s", "codespace,repo,read:user"], {
7152
+ const proc = (0, import_child_process8.spawn)("gh", ["auth", "login", "-s", "codespace,repo,read:user"], {
7026
7153
  stdio: "inherit"
7027
7154
  });
7028
7155
  proc.on("exit", (code) => {
@@ -7056,7 +7183,7 @@ var GitHubCodespacesProvider = class {
7056
7183
  wt(noteLines.join("\n"), "One more permission needed");
7057
7184
  resetStdinForChild();
7058
7185
  const refreshCode = await new Promise((resolve2, reject) => {
7059
- const proc = (0, import_child_process6.spawn)(
7186
+ const proc = (0, import_child_process8.spawn)(
7060
7187
  "gh",
7061
7188
  ["auth", "refresh", "-h", "github.com", "-s", "codespace"],
7062
7189
  { stdio: "inherit" }
@@ -7206,7 +7333,7 @@ var GitHubCodespacesProvider = class {
7206
7333
  O2.step(`Installing gh via ${installCmd.describe}\u2026`);
7207
7334
  resetStdinForChild();
7208
7335
  const ok = await new Promise((resolve2) => {
7209
- const proc = (0, import_child_process6.spawn)(installCmd.exe, installCmd.args, { stdio: "inherit" });
7336
+ const proc = (0, import_child_process8.spawn)(installCmd.exe, installCmd.args, { stdio: "inherit" });
7210
7337
  proc.on("exit", (code) => resolve2(code === 0));
7211
7338
  proc.on("error", () => resolve2(false));
7212
7339
  });
@@ -7233,7 +7360,7 @@ var GitHubCodespacesProvider = class {
7233
7360
  );
7234
7361
  resetStdinForChild();
7235
7362
  await new Promise((resolve2, reject) => {
7236
- const proc = (0, import_child_process6.spawn)(
7363
+ const proc = (0, import_child_process8.spawn)(
7237
7364
  "gh",
7238
7365
  ["auth", "refresh", "-h", "github.com", "-s", "repo,read:org"],
7239
7366
  { stdio: "inherit" }
@@ -7411,7 +7538,7 @@ var GitHubCodespacesProvider = class {
7411
7538
  async streamCommand(workspaceId, command2) {
7412
7539
  resetStdinForChild();
7413
7540
  return new Promise((resolve2, reject) => {
7414
- const proc = (0, import_child_process6.spawn)(
7541
+ const proc = (0, import_child_process8.spawn)(
7415
7542
  "gh",
7416
7543
  ["codespace", "ssh", "-c", workspaceId, "--", "-tt", command2],
7417
7544
  { stdio: "inherit" }
@@ -7438,11 +7565,11 @@ var GitHubCodespacesProvider = class {
7438
7565
  `mkdir -p ${shellQuote(remoteDir)} && tar -xzf - -C ${shellQuote(remoteDir)}`
7439
7566
  ];
7440
7567
  await new Promise((resolve2, reject) => {
7441
- const tar = (0, import_child_process6.spawn)("tar", tarArgs, {
7568
+ const tar = (0, import_child_process8.spawn)("tar", tarArgs, {
7442
7569
  stdio: ["ignore", "pipe", "pipe"],
7443
7570
  env: tarEnv
7444
7571
  });
7445
- const ssh = (0, import_child_process6.spawn)("gh", sshArgs, {
7572
+ const ssh = (0, import_child_process8.spawn)("gh", sshArgs, {
7446
7573
  stdio: [tar.stdout, "pipe", "pipe"]
7447
7574
  });
7448
7575
  let tarErr = "";
@@ -7466,7 +7593,7 @@ var GitHubCodespacesProvider = class {
7466
7593
  });
7467
7594
  }
7468
7595
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
7469
- const remoteDir = path12.posix.dirname(remotePath);
7596
+ const remoteDir = path13.posix.dirname(remotePath);
7470
7597
  const parts = [
7471
7598
  `mkdir -p ${shellQuote(remoteDir)}`,
7472
7599
  `cat > ${shellQuote(remotePath)}`
@@ -7476,7 +7603,7 @@ var GitHubCodespacesProvider = class {
7476
7603
  }
7477
7604
  const cmd = parts.join(" && ");
7478
7605
  await new Promise((resolve2, reject) => {
7479
- const proc = (0, import_child_process6.spawn)(
7606
+ const proc = (0, import_child_process8.spawn)(
7480
7607
  "gh",
7481
7608
  ["codespace", "ssh", "-c", workspaceId, "--", cmd],
7482
7609
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -7534,11 +7661,11 @@ function shellQuote(s) {
7534
7661
  }
7535
7662
 
7536
7663
  // src/services/providers/gitpod.ts
7537
- var import_child_process7 = require("child_process");
7664
+ var import_child_process9 = require("child_process");
7538
7665
  var import_util3 = require("util");
7539
- var path13 = __toESM(require("path"));
7666
+ var path14 = __toESM(require("path"));
7540
7667
  var import_picocolors8 = __toESM(require("picocolors"));
7541
- var execFileP3 = (0, import_util3.promisify)(import_child_process7.execFile);
7668
+ var execFileP3 = (0, import_util3.promisify)(import_child_process9.execFile);
7542
7669
  var MAX_BUFFER2 = 8 * 1024 * 1024;
7543
7670
  function resetStdinForChild2() {
7544
7671
  if (process.stdin.isTTY) {
@@ -7578,7 +7705,7 @@ var GitpodProvider = class {
7578
7705
  );
7579
7706
  resetStdinForChild2();
7580
7707
  await new Promise((resolve2, reject) => {
7581
- const proc = (0, import_child_process7.spawn)("gitpod", ["login"], { stdio: "inherit" });
7708
+ const proc = (0, import_child_process9.spawn)("gitpod", ["login"], { stdio: "inherit" });
7582
7709
  proc.on("exit", (code) => {
7583
7710
  if (code === 0) resolve2();
7584
7711
  else reject(new Error("gitpod login failed."));
@@ -7730,7 +7857,7 @@ var GitpodProvider = class {
7730
7857
  async streamCommand(workspaceId, command2) {
7731
7858
  resetStdinForChild2();
7732
7859
  return new Promise((resolve2, reject) => {
7733
- const proc = (0, import_child_process7.spawn)(
7860
+ const proc = (0, import_child_process9.spawn)(
7734
7861
  "gitpod",
7735
7862
  ["workspace", "ssh", workspaceId, "--", "-tt", command2],
7736
7863
  { stdio: "inherit" }
@@ -7750,11 +7877,11 @@ var GitpodProvider = class {
7750
7877
  const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
7751
7878
  const remoteCmd = `mkdir -p ${shellQuote2(remoteDir)} && tar -xzf - -C ${shellQuote2(remoteDir)}`;
7752
7879
  await new Promise((resolve2, reject) => {
7753
- const tar = (0, import_child_process7.spawn)("tar", tarArgs, {
7880
+ const tar = (0, import_child_process9.spawn)("tar", tarArgs, {
7754
7881
  stdio: ["ignore", "pipe", "pipe"],
7755
7882
  env: tarEnv
7756
7883
  });
7757
- const ssh = (0, import_child_process7.spawn)(
7884
+ const ssh = (0, import_child_process9.spawn)(
7758
7885
  "gitpod",
7759
7886
  ["workspace", "ssh", workspaceId, "--", remoteCmd],
7760
7887
  { stdio: [tar.stdout, "pipe", "pipe"] }
@@ -7776,7 +7903,7 @@ var GitpodProvider = class {
7776
7903
  });
7777
7904
  }
7778
7905
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
7779
- const remoteDir = path13.posix.dirname(remotePath);
7906
+ const remoteDir = path14.posix.dirname(remotePath);
7780
7907
  const parts = [
7781
7908
  `mkdir -p ${shellQuote2(remoteDir)}`,
7782
7909
  `cat > ${shellQuote2(remotePath)}`
@@ -7786,7 +7913,7 @@ var GitpodProvider = class {
7786
7913
  }
7787
7914
  const cmd = parts.join(" && ");
7788
7915
  await new Promise((resolve2, reject) => {
7789
- const proc = (0, import_child_process7.spawn)(
7916
+ const proc = (0, import_child_process9.spawn)(
7790
7917
  "gitpod",
7791
7918
  ["workspace", "ssh", workspaceId, "--", cmd],
7792
7919
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -7810,10 +7937,10 @@ function shellQuote2(s) {
7810
7937
  }
7811
7938
 
7812
7939
  // src/services/providers/gitlab-workspaces.ts
7813
- var import_child_process8 = require("child_process");
7940
+ var import_child_process10 = require("child_process");
7814
7941
  var import_util4 = require("util");
7815
- var path14 = __toESM(require("path"));
7816
- var execFileP4 = (0, import_util4.promisify)(import_child_process8.execFile);
7942
+ var path15 = __toESM(require("path"));
7943
+ var execFileP4 = (0, import_util4.promisify)(import_child_process10.execFile);
7817
7944
  var MAX_BUFFER3 = 8 * 1024 * 1024;
7818
7945
  var GITLAB_API_BASE = process.env.CODEAM_GITLAB_API_URL ?? "https://gitlab.com/api/v4";
7819
7946
  function resetStdinForChild3() {
@@ -7855,7 +7982,7 @@ var GitLabWorkspacesProvider = class {
7855
7982
  );
7856
7983
  resetStdinForChild3();
7857
7984
  await new Promise((resolve2, reject) => {
7858
- const proc = (0, import_child_process8.spawn)(
7985
+ const proc = (0, import_child_process10.spawn)(
7859
7986
  "glab",
7860
7987
  ["auth", "login", "--scopes", "api,read_user,read_repository"],
7861
7988
  { stdio: "inherit" }
@@ -8027,7 +8154,7 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
8027
8154
  const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
8028
8155
  resetStdinForChild3();
8029
8156
  return new Promise((resolve2, reject) => {
8030
- const proc = (0, import_child_process8.spawn)(
8157
+ const proc = (0, import_child_process10.spawn)(
8031
8158
  "ssh",
8032
8159
  ["-tt", "-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, command2],
8033
8160
  { stdio: "inherit" }
@@ -8048,8 +8175,8 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
8048
8175
  const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
8049
8176
  const remoteCmd = `mkdir -p ${shellQuote3(remoteDir)} && tar -xzf - -C ${shellQuote3(remoteDir)}`;
8050
8177
  await new Promise((resolve2, reject) => {
8051
- const tar = (0, import_child_process8.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
8052
- const ssh = (0, import_child_process8.spawn)(
8178
+ const tar = (0, import_child_process10.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
8179
+ const ssh = (0, import_child_process10.spawn)(
8053
8180
  "ssh",
8054
8181
  ["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, remoteCmd],
8055
8182
  { stdio: [tar.stdout, "pipe", "pipe"] }
@@ -8072,14 +8199,14 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
8072
8199
  }
8073
8200
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
8074
8201
  const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
8075
- const remoteDir = path14.posix.dirname(remotePath);
8202
+ const remoteDir = path15.posix.dirname(remotePath);
8076
8203
  const parts = [`mkdir -p ${shellQuote3(remoteDir)}`, `cat > ${shellQuote3(remotePath)}`];
8077
8204
  if (options.mode != null) {
8078
8205
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote3(remotePath)}`);
8079
8206
  }
8080
8207
  const cmd = parts.join(" && ");
8081
8208
  await new Promise((resolve2, reject) => {
8082
- const proc = (0, import_child_process8.spawn)(
8209
+ const proc = (0, import_child_process10.spawn)(
8083
8210
  "ssh",
8084
8211
  ["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, cmd],
8085
8212
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -8138,10 +8265,10 @@ function shellQuote3(s) {
8138
8265
  }
8139
8266
 
8140
8267
  // src/services/providers/railway.ts
8141
- var import_child_process9 = require("child_process");
8268
+ var import_child_process11 = require("child_process");
8142
8269
  var import_util5 = require("util");
8143
- var path15 = __toESM(require("path"));
8144
- var execFileP5 = (0, import_util5.promisify)(import_child_process9.execFile);
8270
+ var path16 = __toESM(require("path"));
8271
+ var execFileP5 = (0, import_util5.promisify)(import_child_process11.execFile);
8145
8272
  var MAX_BUFFER4 = 8 * 1024 * 1024;
8146
8273
  function resetStdinForChild4() {
8147
8274
  if (process.stdin.isTTY) {
@@ -8182,7 +8309,7 @@ var RailwayProvider = class {
8182
8309
  );
8183
8310
  resetStdinForChild4();
8184
8311
  await new Promise((resolve2, reject) => {
8185
- const proc = (0, import_child_process9.spawn)("railway", ["login"], { stdio: "inherit" });
8312
+ const proc = (0, import_child_process11.spawn)("railway", ["login"], { stdio: "inherit" });
8186
8313
  proc.on("exit", (code) => {
8187
8314
  if (code === 0) resolve2();
8188
8315
  else reject(new Error("railway login failed."));
@@ -8325,7 +8452,7 @@ var RailwayProvider = class {
8325
8452
  }
8326
8453
  resetStdinForChild4();
8327
8454
  return new Promise((resolve2, reject) => {
8328
- const proc = (0, import_child_process9.spawn)(
8455
+ const proc = (0, import_child_process11.spawn)(
8329
8456
  "railway",
8330
8457
  ["shell", "--project", projectId, "--service", serviceId, "--command", command2],
8331
8458
  { stdio: "inherit" }
@@ -8349,8 +8476,8 @@ var RailwayProvider = class {
8349
8476
  const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
8350
8477
  const remoteCmd = `mkdir -p ${shellQuote4(remoteDir)} && tar -xzf - -C ${shellQuote4(remoteDir)}`;
8351
8478
  await new Promise((resolve2, reject) => {
8352
- const tar = (0, import_child_process9.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
8353
- const sh = (0, import_child_process9.spawn)(
8479
+ const tar = (0, import_child_process11.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
8480
+ const sh = (0, import_child_process11.spawn)(
8354
8481
  "railway",
8355
8482
  ["shell", "--project", projectId, "--service", serviceId, "--command", remoteCmd],
8356
8483
  { stdio: [tar.stdout, "pipe", "pipe"] }
@@ -8376,14 +8503,14 @@ var RailwayProvider = class {
8376
8503
  if (!projectId || !serviceId) {
8377
8504
  throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
8378
8505
  }
8379
- const remoteDir = path15.posix.dirname(remotePath);
8506
+ const remoteDir = path16.posix.dirname(remotePath);
8380
8507
  const parts = [`mkdir -p ${shellQuote4(remoteDir)}`, `cat > ${shellQuote4(remotePath)}`];
8381
8508
  if (options.mode != null) {
8382
8509
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote4(remotePath)}`);
8383
8510
  }
8384
8511
  const cmd = parts.join(" && ");
8385
8512
  await new Promise((resolve2, reject) => {
8386
- const proc = (0, import_child_process9.spawn)(
8513
+ const proc = (0, import_child_process11.spawn)(
8387
8514
  "railway",
8388
8515
  ["shell", "--project", projectId, "--service", serviceId, "--command", cmd],
8389
8516
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -8415,7 +8542,7 @@ var PROVIDERS = [
8415
8542
  ];
8416
8543
 
8417
8544
  // src/commands/deploy.ts
8418
- var execFileP6 = (0, import_util6.promisify)(import_child_process10.execFile);
8545
+ var execFileP6 = (0, import_util6.promisify)(import_child_process12.execFile);
8419
8546
  async function deploy() {
8420
8547
  console.log();
8421
8548
  mt(import_picocolors9.default.bgMagenta(import_picocolors9.default.white(" codeam deploy ")));
@@ -8568,7 +8695,7 @@ async function deploy() {
8568
8695
  process.exit(1);
8569
8696
  }
8570
8697
  }
8571
- const localClaudeDir = path16.join(os8.homedir(), ".claude");
8698
+ const localClaudeDir = path17.join(os9.homedir(), ".claude");
8572
8699
  const localCredsKind = await detectLocalClaudeCredentials(localClaudeDir);
8573
8700
  let bridged = "none";
8574
8701
  if (localCredsKind !== "none") {
@@ -8612,7 +8739,7 @@ async function deploy() {
8612
8739
  process.exit(1);
8613
8740
  }
8614
8741
  claudeStep.stop("\u2713 Claude CLI installed");
8615
- const haveLocalClaude = fs9.existsSync(localClaudeDir) && fs9.statSync(localClaudeDir).isDirectory();
8742
+ const haveLocalClaude = fs10.existsSync(localClaudeDir) && fs10.statSync(localClaudeDir).isDirectory();
8616
8743
  if (haveLocalClaude) {
8617
8744
  const copyStep = fe();
8618
8745
  copyStep.start("Copying local Claude config to workspace\u2026");
@@ -8666,10 +8793,10 @@ async function deploy() {
8666
8793
  }
8667
8794
  }
8668
8795
  if (bridged !== "none") {
8669
- const localClaudeJson = path16.join(os8.homedir(), ".claude.json");
8670
- if (fs9.existsSync(localClaudeJson)) {
8796
+ const localClaudeJson = path17.join(os9.homedir(), ".claude.json");
8797
+ if (fs10.existsSync(localClaudeJson)) {
8671
8798
  try {
8672
- const contents = fs9.readFileSync(localClaudeJson);
8799
+ const contents = fs10.readFileSync(localClaudeJson);
8673
8800
  await provider.uploadFile(
8674
8801
  workspace.id,
8675
8802
  "/home/codespace/.claude.json",
@@ -8859,7 +8986,7 @@ async function runRemoteClaudeLogin(provider, workspaceId) {
8859
8986
  }
8860
8987
  }
8861
8988
  async function detectLocalClaudeCredentials(localClaudeDir) {
8862
- if (fs9.existsSync(path16.join(localClaudeDir, ".credentials.json"))) {
8989
+ if (fs10.existsSync(path17.join(localClaudeDir, ".credentials.json"))) {
8863
8990
  return "flat-file";
8864
8991
  }
8865
8992
  if (process.platform === "darwin") {
@@ -8892,8 +9019,8 @@ async function verifyClaudeAuth(provider, workspaceId) {
8892
9019
  }
8893
9020
  }
8894
9021
  async function bridgeClaudeCredentials(provider, workspaceId, localClaudeDir) {
8895
- const fileBased = path16.join(localClaudeDir, ".credentials.json");
8896
- if (fs9.existsSync(fileBased)) return "flat-file";
9022
+ const fileBased = path17.join(localClaudeDir, ".credentials.json");
9023
+ if (fs10.existsSync(fileBased)) return "flat-file";
8897
9024
  if (process.platform === "darwin") {
8898
9025
  try {
8899
9026
  const { stdout } = await execFileP6(
@@ -9101,7 +9228,7 @@ async function stopWorkspaceFromLocal(target) {
9101
9228
  // src/commands/version.ts
9102
9229
  var import_picocolors11 = __toESM(require("picocolors"));
9103
9230
  function version() {
9104
- const v = true ? "2.4.39" : "unknown";
9231
+ const v = true ? "2.5.1" : "unknown";
9105
9232
  console.log(`${import_picocolors11.default.bold("codeam-cli")} ${import_picocolors11.default.cyan(v)}`);
9106
9233
  }
9107
9234
 
@@ -9138,22 +9265,22 @@ function help() {
9138
9265
  }
9139
9266
 
9140
9267
  // src/lib/updateNotifier.ts
9141
- var fs10 = __toESM(require("fs"));
9142
- var os9 = __toESM(require("os"));
9143
- var path17 = __toESM(require("path"));
9144
- var https4 = __toESM(require("https"));
9268
+ var fs11 = __toESM(require("fs"));
9269
+ var os10 = __toESM(require("os"));
9270
+ var path18 = __toESM(require("path"));
9271
+ var https5 = __toESM(require("https"));
9145
9272
  var import_picocolors13 = __toESM(require("picocolors"));
9146
9273
  var PKG_NAME = "codeam-cli";
9147
9274
  var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
9148
9275
  var TTL_MS = 24 * 60 * 60 * 1e3;
9149
9276
  var REQUEST_TIMEOUT_MS = 1500;
9150
9277
  function cachePath() {
9151
- const dir = path17.join(os9.homedir(), ".codeam");
9152
- return path17.join(dir, "update-check.json");
9278
+ const dir = path18.join(os10.homedir(), ".codeam");
9279
+ return path18.join(dir, "update-check.json");
9153
9280
  }
9154
9281
  function readCache() {
9155
9282
  try {
9156
- const raw = fs10.readFileSync(cachePath(), "utf8");
9283
+ const raw = fs11.readFileSync(cachePath(), "utf8");
9157
9284
  const parsed = JSON.parse(raw);
9158
9285
  if (typeof parsed.fetchedAt !== "number" || typeof parsed.latest !== "string") return null;
9159
9286
  return parsed;
@@ -9164,8 +9291,8 @@ function readCache() {
9164
9291
  function writeCache(cache) {
9165
9292
  try {
9166
9293
  const file = cachePath();
9167
- fs10.mkdirSync(path17.dirname(file), { recursive: true });
9168
- fs10.writeFileSync(file, JSON.stringify(cache));
9294
+ fs11.mkdirSync(path18.dirname(file), { recursive: true });
9295
+ fs11.writeFileSync(file, JSON.stringify(cache));
9169
9296
  } catch {
9170
9297
  }
9171
9298
  }
@@ -9184,7 +9311,7 @@ function compareSemver(a, b) {
9184
9311
  }
9185
9312
  function fetchLatest() {
9186
9313
  return new Promise((resolve2) => {
9187
- const req = https4.get(
9314
+ const req = https5.get(
9188
9315
  REGISTRY_URL,
9189
9316
  { headers: { Accept: "application/json" }, timeout: REQUEST_TIMEOUT_MS },
9190
9317
  (res) => {
@@ -9236,7 +9363,7 @@ function checkForUpdates() {
9236
9363
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
9237
9364
  if (process.env.CI) return;
9238
9365
  if (!process.stdout.isTTY) return;
9239
- const current = true ? "2.4.39" : null;
9366
+ const current = true ? "2.5.1" : null;
9240
9367
  if (!current) return;
9241
9368
  const cache = readCache();
9242
9369
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;