codeam-cli 2.4.38 → 2.5.0

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 (2) hide show
  1. package/dist/index.js +1230 -1078
  2. 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.38",
1480
+ version: "2.5.0",
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
@@ -4645,10 +4639,6 @@ var ClaudeService = class {
4645
4639
  }
4646
4640
  };
4647
4641
 
4648
- // src/services/output.service.ts
4649
- var https2 = __toESM(require("https"));
4650
- var http2 = __toESM(require("http"));
4651
-
4652
4642
  // ../../packages/shared/src/protocol/parseChrome.ts
4653
4643
  var SPINNER_RE = /^(?:[✳✢✶✻✽✴✷✸✹⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏◐◑◒◓▁▂▃▄▅▆▇█]|🔴|🟠|🟡|🟢|🔵|🟣|🟤|⚫|⚪|🌀|💭|✨)\s/u;
4654
4644
  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 +5009,358 @@ function getContextWindow(model) {
5019
5009
  return DEFAULT_CONTEXT_WINDOW;
5020
5010
  }
5021
5011
 
5012
+ // src/services/output/chrome-tracker.ts
5013
+ var ChromeStepTracker = class {
5014
+ history = [];
5015
+ sentCount = 0;
5016
+ reset() {
5017
+ this.history = [];
5018
+ this.sentCount = 0;
5019
+ }
5020
+ /** Parse the rendered lines, append unseen steps to the cumulative history. */
5021
+ ingest(lines) {
5022
+ const visible = lines.filter((l) => isChromeLine(l)).map((l) => parseChromeLine(l)).filter((s) => s !== null);
5023
+ if (visible.length === 0) return;
5024
+ for (const step of visible) {
5025
+ const exists = this.history.some(
5026
+ (s) => s.tool === step.tool && s.label === step.label
5027
+ );
5028
+ if (!exists) this.history.push(step);
5029
+ }
5030
+ }
5031
+ /**
5032
+ * Returns the steps that have NOT yet been shipped on the wire,
5033
+ * marking them as shipped. Empty array means nothing new since
5034
+ * the last call. Caller forwards this as the chunk's
5035
+ * `appendSteps` payload.
5036
+ */
5037
+ consumeDelta() {
5038
+ if (this.history.length === this.sentCount) return [];
5039
+ const delta = this.history.slice(this.sentCount);
5040
+ this.sentCount = this.history.length;
5041
+ return delta;
5042
+ }
5043
+ /** Snapshot of the cumulative unique-step history (debug + tests). */
5044
+ get cumulativeHistory() {
5045
+ return this.history;
5046
+ }
5047
+ };
5048
+
5049
+ // src/services/output/chunk-emitter.ts
5050
+ var https3 = __toESM(require("https"));
5051
+ var http3 = __toESM(require("http"));
5052
+ var API_BASE3 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
5053
+ var ChunkEmitter = class {
5054
+ constructor(opts) {
5055
+ this.opts = opts;
5056
+ this.headers = {
5057
+ "Content-Type": "application/json",
5058
+ // Tell the backend which wire-format version we speak so
5059
+ // it can route legacy translations / 426 us when we're
5060
+ // too far behind. Bumped to 2.0.0 with the discriminated-
5061
+ // chunk + delta-chrome refactor in this release.
5062
+ "X-Codeam-Protocol-Version": "2.0.0"
5063
+ };
5064
+ if (opts.pluginAuthToken) {
5065
+ this.headers["X-Plugin-Auth-Token"] = opts.pluginAuthToken;
5066
+ }
5067
+ }
5068
+ opts;
5069
+ url = `${API_BASE3}/api/commands/output`;
5070
+ headers;
5071
+ /**
5072
+ * Send a chunk. `body` is the chunk fields minus `sessionId` /
5073
+ * `pluginId` — the emitter splices those in. `critical = true`
5074
+ * triggers up to 3 retries with linear backoff (200/400/600 ms);
5075
+ * non-critical sends are best-effort (a transient miss gets
5076
+ * superseded by the next tick's emission).
5077
+ */
5078
+ async send(body, opts = {}) {
5079
+ const payload = JSON.stringify({
5080
+ sessionId: this.opts.sessionId,
5081
+ pluginId: this.opts.pluginId,
5082
+ ...body
5083
+ });
5084
+ const maxRetries = opts.critical ? 3 : 0;
5085
+ log.trace(
5086
+ "chunkEmitter",
5087
+ `send type=${body.type ?? "(clear)"} bytes=${payload.length}`
5088
+ );
5089
+ return new Promise((resolve2) => {
5090
+ const attempt = (attemptsLeft) => {
5091
+ _transport2.post(this.url, this.headers, payload).then(({ statusCode, body: resBody }) => {
5092
+ if (statusCode === 410 || statusCode === 404 && /SESSION_NOT_FOUND|SESSION_GONE/.test(resBody)) {
5093
+ process.stderr.write("[codeam] session was deleted/disconnected \u2014 stopping output stream.\n");
5094
+ resolve2({ dead: true });
5095
+ return;
5096
+ }
5097
+ if (statusCode >= 400) {
5098
+ process.stderr.write(`[codeam] output API error ${statusCode}: ${resBody}
5099
+ `);
5100
+ }
5101
+ log.trace("chunkEmitter", `status=${statusCode}`);
5102
+ resolve2({ dead: false });
5103
+ }).catch((err) => {
5104
+ log.trace(
5105
+ "chunkEmitter",
5106
+ `error retries-left=${attemptsLeft}`,
5107
+ err
5108
+ );
5109
+ if (attemptsLeft > 0) {
5110
+ const delay = 200 * (maxRetries - attemptsLeft + 1);
5111
+ setTimeout(() => attempt(attemptsLeft - 1), delay);
5112
+ } else {
5113
+ resolve2({ dead: false });
5114
+ }
5115
+ });
5116
+ };
5117
+ attempt(maxRetries);
5118
+ });
5119
+ }
5120
+ };
5121
+ var _transport2 = {
5122
+ post: _post
5123
+ };
5124
+ function _post(url, headers, payload) {
5125
+ return new Promise((resolve2, reject) => {
5126
+ let settled = false;
5127
+ const u2 = new URL(url);
5128
+ const transport = u2.protocol === "https:" ? https3 : http3;
5129
+ const req = transport.request(
5130
+ {
5131
+ hostname: u2.hostname,
5132
+ port: u2.port || (u2.protocol === "https:" ? 443 : 80),
5133
+ path: u2.pathname,
5134
+ method: "POST",
5135
+ headers: {
5136
+ ...headers,
5137
+ "Content-Length": Buffer.byteLength(payload)
5138
+ },
5139
+ timeout: 8e3
5140
+ },
5141
+ (res) => {
5142
+ let resData = "";
5143
+ res.on("data", (c2) => {
5144
+ resData += c2.toString();
5145
+ });
5146
+ res.on("end", () => {
5147
+ if (settled) return;
5148
+ settled = true;
5149
+ resolve2({ statusCode: res.statusCode ?? 0, body: resData });
5150
+ });
5151
+ }
5152
+ );
5153
+ req.on("error", (err) => {
5154
+ if (settled) return;
5155
+ settled = true;
5156
+ reject(err);
5157
+ });
5158
+ req.on("timeout", () => {
5159
+ req.destroy();
5160
+ });
5161
+ req.write(payload);
5162
+ req.end();
5163
+ });
5164
+ }
5165
+
5166
+ // src/services/output/pty-buffer.ts
5167
+ var PtyBuffer = class {
5168
+ raw = "";
5169
+ active = false;
5170
+ lastPushAt = 0;
5171
+ terminalInputPending = false;
5172
+ /** Whether to absorb pushes (`true`) or only watch for terminal input (`false`). */
5173
+ get isActive() {
5174
+ return this.active;
5175
+ }
5176
+ /** Bytes accumulated since the last reset. */
5177
+ get content() {
5178
+ return this.raw;
5179
+ }
5180
+ /** Wall-clock of the most recent printable push (`0` if none yet this turn). */
5181
+ get lastPushTime() {
5182
+ return this.lastPushAt;
5183
+ }
5184
+ /** Length of the accumulated buffer in raw bytes (debug + tests). */
5185
+ get size() {
5186
+ return this.raw.length;
5187
+ }
5188
+ activate() {
5189
+ this.active = true;
5190
+ this.raw = "";
5191
+ this.lastPushAt = 0;
5192
+ this.terminalInputPending = false;
5193
+ }
5194
+ deactivate() {
5195
+ this.active = false;
5196
+ }
5197
+ reset() {
5198
+ this.raw = "";
5199
+ this.lastPushAt = 0;
5200
+ }
5201
+ /**
5202
+ * Ingest a raw PTY frame. Returns whether the buffer was active
5203
+ * at the time (caller cares because rendering only matters for
5204
+ * active frames) and whether this push triggered the
5205
+ * terminal-initiated-turn signal.
5206
+ */
5207
+ push(raw) {
5208
+ if (!this.active) {
5209
+ let terminalInputDetected = false;
5210
+ if (!this.terminalInputPending && hasPrintable(raw)) {
5211
+ this.terminalInputPending = true;
5212
+ terminalInputDetected = true;
5213
+ }
5214
+ return { active: false, terminalInputDetected };
5215
+ }
5216
+ this.raw += raw;
5217
+ if (hasPrintable(raw)) this.lastPushAt = Date.now();
5218
+ return { active: true, terminalInputDetected: false };
5219
+ }
5220
+ };
5221
+ function hasPrintable(raw) {
5222
+ const stripped = raw.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, "");
5223
+ return stripped.trim().length > 0;
5224
+ }
5225
+
5226
+ // src/services/output/turn-renderer.ts
5227
+ function renderLines(buffer) {
5228
+ return renderToLines(buffer);
5229
+ }
5230
+ function detectAnySelector(lines) {
5231
+ return detectSelector(lines) ?? detectListSelector(lines);
5232
+ }
5233
+ function extractContent(lines) {
5234
+ return filterChrome(lines).join("\n").replace(/\n{3,}/g, "\n\n").trim();
5235
+ }
5236
+
5022
5237
  // src/services/output.service.ts
5023
- var API_BASE4 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
5024
5238
  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 = "";
5239
+ pty = new PtyBuffer();
5240
+ steps = new ChromeStepTracker();
5241
+ emitter;
5038
5242
  lastSentContent = "";
5039
- lastSentChromeStepsJson = "";
5040
- chromeStepsHistory = [];
5041
5243
  pollTimer = null;
5042
5244
  startTime = 0;
5043
- active = false;
5044
5245
  terminalTurnPending = false;
5045
- lastPushTime = 0;
5046
5246
  onSessionIdDetected;
5047
5247
  onRateLimitDetected;
5048
5248
  onTurnComplete;
5049
5249
  onTerminalTurnDetected;
5250
+ /** Tick cadence — every 1 s while a turn is active. */
5050
5251
  static POLL_MS = 1e3;
5252
+ /** Idle threshold for "the agent's text settled, finalize the turn". */
5051
5253
  static IDLE_MS = 3e3;
5052
- /** Shorter idle threshold for selector detection (UI is ready immediately). */
5254
+ /** Same threshold but tighter for selectors (UI is ready to interact immediately). */
5053
5255
  static SELECTOR_IDLE_MS = 1500;
5054
5256
  /**
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).
5257
+ * Grace period before tick processes anything — Claude needs ~100-
5258
+ * 200 ms after `\r` to clear the input echo and re-render the TUI.
5259
+ * 1.5 s is a comfortable margin on loaded machines.
5059
5260
  */
5060
5261
  static WARMUP_MS = 1500;
5061
- /** Max idle with no visible content (spinner only) before finalizing. */
5262
+ /** Max idle with chrome-only output before we stop waiting on the agent. */
5062
5263
  static EMPTY_TIMEOUT_MS = 6e4;
5264
+ /** Hard turn cap — pathological no-op turns get cut after 2 minutes. */
5063
5265
  static MAX_MS = 12e4;
5266
+ constructor(sessionId, pluginId, onSessionIdDetected, onRateLimitDetected, onTurnComplete, onTerminalTurnDetected, pluginAuthToken) {
5267
+ this.onSessionIdDetected = onSessionIdDetected;
5268
+ this.onRateLimitDetected = onRateLimitDetected;
5269
+ this.onTurnComplete = onTurnComplete;
5270
+ this.onTerminalTurnDetected = onTerminalTurnDetected;
5271
+ this.emitter = new ChunkEmitter({
5272
+ sessionId,
5273
+ pluginId,
5274
+ pluginAuthToken
5275
+ });
5276
+ }
5277
+ // ─── Turn lifecycle ──────────────────────────────────────────────
5278
+ /**
5279
+ * Begin a turn driven by a mobile-side prompt. Resets the buffer
5280
+ * and emits the boundary chunks (clear → new_turn) that tell
5281
+ * clients to wipe the prior agent reply and show "Agent is
5282
+ * typing…".
5283
+ */
5284
+ newTurn() {
5285
+ log.trace("outputSvc", "newTurn() \u2014 activating output stream");
5286
+ this.beginTurn();
5287
+ this.send({ type: "clear" }, { critical: true }).then(() => this.send({ type: "new_turn", done: false }, { critical: true })).catch(() => {
5288
+ });
5289
+ }
5064
5290
  /**
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.
5291
+ * Begin a turn driven by the user typing locally in their
5292
+ * terminal. Same shape as `newTurn` but additionally sends a
5293
+ * `user_message` so collaborators see the prompt attributed
5294
+ * correctly. `userText` is the prompt text scraped from the
5295
+ * Claude JSONL by `historySvc.waitForNewUserMessage`.
5069
5296
  */
5070
5297
  async startTerminalTurn(userText) {
5071
5298
  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 });
5299
+ this.beginTurn();
5300
+ await this.send({ type: "clear" }, { critical: true });
5081
5301
  if (userText) {
5082
- await this.postChunk({ type: "user_message", content: userText, done: true });
5302
+ await this.send({ type: "user_message", content: userText, done: true }, { critical: true });
5083
5303
  }
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);
5304
+ await this.send({ type: "new_turn", done: false }, { critical: true });
5101
5305
  }
5102
5306
  /**
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.
5307
+ * Begin a turn after a `resume_session` request. Includes the
5308
+ * `resumedSessionId` so the client wipes its history and
5309
+ * re-fetches from the JSONL via `get_conversation`.
5106
5310
  */
5107
5311
  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);
5312
+ this.beginTurn();
5313
+ await this.send({ type: "clear" }, { critical: true });
5314
+ await this.send(
5315
+ { type: "new_turn", done: false, resumedSessionId },
5316
+ { critical: true }
5317
+ );
5119
5318
  }
5319
+ // ─── Pump ────────────────────────────────────────────────────────
5120
5320
  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
- }
5321
+ const result = this.pty.push(raw);
5322
+ if (!result.active) {
5323
+ if (result.terminalInputDetected && !this.terminalTurnPending) {
5324
+ this.terminalTurnPending = true;
5325
+ this.onTerminalTurnDetected?.();
5129
5326
  }
5130
5327
  log.trace("outputSvc", `push dropped (inactive, ${raw.length}B)`);
5131
5328
  return;
5132
5329
  }
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
5330
  log.trace(
5141
5331
  "outputSvc",
5142
- `push +${raw.length}B (buf=${this.rawBuffer.length}B printable=${printable.trim().length})`
5332
+ `push +${raw.length}B (buf=${this.pty.size}B)`
5143
5333
  );
5334
+ this.tryExtractSessionId(raw);
5335
+ this.tryDetectRateLimit(raw);
5144
5336
  }
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
- }
5337
+ dispose() {
5338
+ this.stopPoll();
5339
+ this.pty.deactivate();
5160
5340
  }
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());
5341
+ // ─── Internals ───────────────────────────────────────────────────
5342
+ beginTurn() {
5343
+ this.stopPoll();
5344
+ this.pty.activate();
5345
+ this.steps.reset();
5346
+ this.lastSentContent = "";
5347
+ this.startTime = Date.now();
5348
+ this.pollTimer = setInterval(() => this.tick(), _OutputService.POLL_MS);
5349
+ }
5350
+ async send(body, opts = {}) {
5351
+ const outcome = await this.emitter.send(body, opts);
5352
+ if (outcome.dead && this.pty.isActive) {
5353
+ this.dispose();
5166
5354
  }
5167
5355
  }
5168
- dispose() {
5169
- this.stopPoll();
5170
- this.active = false;
5356
+ stopPoll() {
5357
+ if (this.pollTimer) {
5358
+ clearInterval(this.pollTimer);
5359
+ this.pollTimer = null;
5360
+ }
5171
5361
  }
5172
5362
  tick() {
5173
- if (!this.active) return;
5363
+ if (!this.pty.isActive) return;
5174
5364
  const now = Date.now();
5175
5365
  const elapsed = now - this.startTime;
5176
5366
  if (elapsed >= _OutputService.MAX_MS) {
@@ -5178,36 +5368,51 @@ var OutputService = class _OutputService {
5178
5368
  return;
5179
5369
  }
5180
5370
  if (elapsed < _OutputService.WARMUP_MS) return;
5181
- const lines = renderToLines(this.rawBuffer);
5182
- this.postChromeSteps(lines);
5183
- const selector = detectSelector(lines) ?? detectListSelector(lines);
5371
+ const lines = renderLines(this.pty.content);
5372
+ this.steps.ingest(lines);
5373
+ const stepsDelta = this.steps.consumeDelta();
5374
+ if (stepsDelta.length > 0) {
5375
+ this.send({ type: "chrome_steps", appendSteps: stepsDelta }).catch(() => {
5376
+ });
5377
+ }
5378
+ const selector = detectAnySelector(lines);
5184
5379
  if (selector) {
5185
- const idleMs2 = this.lastPushTime > 0 ? now - this.lastPushTime : elapsed;
5380
+ const idleMs2 = this.pty.lastPushTime > 0 ? now - this.pty.lastPushTime : elapsed;
5186
5381
  log.trace(
5187
5382
  "outputSvc",
5188
5383
  `tick selector found (idleMs=${idleMs2}, options=${selector.options.length})`
5189
5384
  );
5190
5385
  if (idleMs2 >= _OutputService.SELECTOR_IDLE_MS) {
5191
5386
  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(() => {
5387
+ this.pty.deactivate();
5388
+ this.send(
5389
+ {
5390
+ type: "select_prompt",
5391
+ content: selector.question,
5392
+ options: selector.options,
5393
+ optionDescriptions: selector.optionDescriptions,
5394
+ currentIndex: selector.currentIndex,
5395
+ done: true
5396
+ },
5397
+ { critical: true }
5398
+ ).catch(() => {
5194
5399
  });
5195
5400
  }
5196
5401
  return;
5197
5402
  }
5198
- const content = filterChrome(lines).join("\n").replace(/\n{3,}/g, "\n\n").trim();
5403
+ const content = extractContent(lines);
5199
5404
  if (!content) {
5200
5405
  log.trace(
5201
5406
  "outputSvc",
5202
- `tick empty content (raw=${this.rawBuffer.length}B lines=${lines.length} elapsed=${elapsed}ms)`
5407
+ `tick empty content (raw=${this.pty.size}B lines=${lines.length} elapsed=${elapsed}ms)`
5203
5408
  );
5204
5409
  if (elapsed >= _OutputService.EMPTY_TIMEOUT_MS) this.finalize();
5205
5410
  return;
5206
5411
  }
5207
- const idleMs = this.lastPushTime > 0 ? now - this.lastPushTime : elapsed;
5412
+ const idleMs = this.pty.lastPushTime > 0 ? now - this.pty.lastPushTime : elapsed;
5208
5413
  log.trace(
5209
5414
  "outputSvc",
5210
- `tick content (raw=${this.rawBuffer.length}B lines=${lines.length} content=${content.length} idleMs=${idleMs})`
5415
+ `tick content (raw=${this.pty.size}B lines=${lines.length} content=${content.length} idleMs=${idleMs})`
5211
5416
  );
5212
5417
  if (idleMs >= _OutputService.IDLE_MS) {
5213
5418
  this.finalize();
@@ -5215,161 +5420,78 @@ var OutputService = class _OutputService {
5215
5420
  }
5216
5421
  if (content !== this.lastSentContent) {
5217
5422
  this.lastSentContent = content;
5218
- this.postChunk({ type: "text", content, done: false }).catch(() => {
5423
+ this.send({ type: "text", content, done: false }).catch(() => {
5219
5424
  });
5220
5425
  }
5221
5426
  }
5222
5427
  finalize() {
5223
- const lines = renderToLines(this.rawBuffer);
5224
- this.postChromeSteps(lines);
5225
- const selector = detectSelector(lines) ?? detectListSelector(lines);
5226
- this.stopPoll();
5227
- this.active = false;
5228
- if (selector) {
5229
- this.postChunk({ type: "select_prompt", content: selector.question, options: selector.options, optionDescriptions: selector.optionDescriptions, currentIndex: selector.currentIndex, done: true }).catch(() => {
5230
- });
5231
- } else {
5232
- const content = filterChrome(lines).join("\n").replace(/\n{3,}/g, "\n\n").trim();
5233
- this.postChunk({ type: "text", content, done: true }).catch(() => {
5428
+ const lines = renderLines(this.pty.content);
5429
+ this.steps.ingest(lines);
5430
+ const stepsDelta = this.steps.consumeDelta();
5431
+ if (stepsDelta.length > 0) {
5432
+ this.send({ type: "chrome_steps", appendSteps: stepsDelta }).catch(() => {
5234
5433
  });
5235
- this.onTurnComplete?.();
5236
5434
  }
5237
- }
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;
5255
- }
5256
- }
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
- }
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}`);
5286
- }
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
- }
5321
- };
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)
5435
+ const selector = detectAnySelector(lines);
5436
+ this.stopPoll();
5437
+ this.pty.deactivate();
5438
+ if (selector) {
5439
+ this.send(
5440
+ {
5441
+ type: "select_prompt",
5442
+ content: selector.question,
5443
+ options: selector.options,
5444
+ optionDescriptions: selector.optionDescriptions,
5445
+ currentIndex: selector.currentIndex,
5446
+ done: true
5339
5447
  },
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
- });
5448
+ { critical: true }
5449
+ ).catch(() => {
5450
+ });
5451
+ } else {
5452
+ const content = extractContent(lines);
5453
+ this.send(
5454
+ { type: "text", content, done: true },
5455
+ { critical: true }
5456
+ ).catch(() => {
5457
+ });
5458
+ this.onTurnComplete?.();
5459
+ }
5460
+ }
5461
+ // ─── Side-channel observation (session id + rate limit) ──────────
5462
+ tryExtractSessionId(text) {
5463
+ if (!this.onSessionIdDetected) return;
5464
+ const printable = text.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, "");
5465
+ const patterns = [
5466
+ /Resuming session[:\s]+([a-f0-9-]{36})/i,
5467
+ /Session[:\s]+([a-f0-9-]{36})/i,
5468
+ /Conversation[:\s]+([a-f0-9-]{36})/i,
5469
+ /Session\s+ID[:\s]+([a-f0-9-]{36})/i
5470
+ ];
5471
+ for (const pattern of patterns) {
5472
+ const match = printable.match(pattern);
5473
+ if (match) {
5474
+ this.onSessionIdDetected(match[1]);
5475
+ return;
5352
5476
  }
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
- }
5477
+ }
5478
+ }
5479
+ tryDetectRateLimit(text) {
5480
+ if (!this.onRateLimitDetected) return;
5481
+ const printable = text.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, "");
5482
+ const match = printable.match(/hit your limit.*resets\s+(.+?)(?:\s*\(|$)/i) ?? printable.match(/rate.?limit.*resets\s+(.+?)(?:\s*\(|$)/i);
5483
+ if (match) {
5484
+ this.onRateLimitDetected(match[1].trim());
5485
+ }
5486
+ }
5487
+ };
5366
5488
 
5367
5489
  // src/services/history.service.ts
5368
5490
  var fs5 = __toESM(require("fs"));
5369
5491
  var path8 = __toESM(require("path"));
5370
5492
  var os6 = __toESM(require("os"));
5371
- var https3 = __toESM(require("https"));
5372
- var http3 = __toESM(require("http"));
5493
+ var https4 = __toESM(require("https"));
5494
+ var http4 = __toESM(require("http"));
5373
5495
  var import_zod = require("zod");
5374
5496
  var historyRecordSchema = import_zod.z.object({
5375
5497
  type: import_zod.z.string().optional(),
@@ -5381,7 +5503,7 @@ var historyRecordSchema = import_zod.z.object({
5381
5503
  content: import_zod.z.union([import_zod.z.string(), import_zod.z.array(import_zod.z.unknown())]).optional()
5382
5504
  }).passthrough().optional()
5383
5505
  }).passthrough();
5384
- var API_BASE5 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
5506
+ var API_BASE4 = process.env.CODEAM_API_URL ?? "https://codeagent-mobile-api.vercel.app";
5385
5507
  function encodeCwd(cwd) {
5386
5508
  return cwd.replace(/[\\/:]/g, "-");
5387
5509
  }
@@ -5454,8 +5576,8 @@ function parseJsonl(filePath) {
5454
5576
  function post(endpoint, body) {
5455
5577
  return new Promise((resolve2) => {
5456
5578
  const payload = JSON.stringify(body);
5457
- const u2 = new URL(`${API_BASE5}${endpoint}`);
5458
- const transport = u2.protocol === "https:" ? https3 : http3;
5579
+ const u2 = new URL(`${API_BASE4}${endpoint}`);
5580
+ const transport = u2.protocol === "https:" ? https4 : http4;
5459
5581
  const req = transport.request(
5460
5582
  {
5461
5583
  hostname: u2.hostname,
@@ -5499,6 +5621,14 @@ var HistoryService = class {
5499
5621
  _rateLimitReset = null;
5500
5622
  _quotaPercent = null;
5501
5623
  _quotaFetchedAt = 0;
5624
+ /**
5625
+ * Per-conversation marker of the last message uuid we successfully
5626
+ * uploaded to the backend. `uploadDelta()` reads the JSONL,
5627
+ * filters out everything up to and including this uuid, and
5628
+ * uploads only the tail. Resets per conversation so a session
5629
+ * resume re-uploads the full transcript on first call.
5630
+ */
5631
+ lastUploadedUuid = /* @__PURE__ */ new Map();
5502
5632
  /** Store rate limit reset info detected from Claude Code output */
5503
5633
  setRateLimitReset(reset) {
5504
5634
  this._rateLimitReset = reset;
@@ -5775,9 +5905,212 @@ var HistoryService = class {
5775
5905
  throw new Error(`Failed to upload conversation batch ${i + 1}/${totalBatches} after all retries`);
5776
5906
  }
5777
5907
  }
5908
+ const last = messages[messages.length - 1];
5909
+ if (last) this.lastUploadedUuid.set(sessionId, last.id);
5910
+ }
5911
+ /**
5912
+ * Incremental upload — ships only the messages added since the last
5913
+ * `loadConversation` / `uploadDelta` call for this conversation.
5914
+ * Used by `onTurnComplete` after every turn so the backend's
5915
+ * conversation table stays fresh enough for the SSE consumers
5916
+ * (mobile + web dashboard) to fetch the canonical markdown via
5917
+ * `?last=N` and replace the streaming-from-PTY approximation —
5918
+ * which lacks the markdown ``` fences the parser needs to surface
5919
+ * the rich CodeBlock / DiffBlock / etc. components.
5920
+ *
5921
+ * Posts under `mode: 'append'` so the server merges by uuid
5922
+ * instead of replacing the full conversation. Idempotent — if
5923
+ * called twice in a row the second call sees zero new messages
5924
+ * and is a no-op.
5925
+ *
5926
+ * Returns the number of messages uploaded (0 means nothing new).
5927
+ */
5928
+ async uploadDelta() {
5929
+ if (!this.currentConversationId) return 0;
5930
+ const sessionId = this.currentConversationId;
5931
+ const filePath = path8.join(this.projectDir, `${sessionId}.jsonl`);
5932
+ const messages = parseJsonl(filePath);
5933
+ if (messages.length === 0) return 0;
5934
+ const marker = this.lastUploadedUuid.get(sessionId);
5935
+ let newMessages = messages;
5936
+ if (marker) {
5937
+ const idx = messages.findIndex((m) => m.id === marker);
5938
+ if (idx >= 0) {
5939
+ newMessages = messages.slice(idx + 1);
5940
+ }
5941
+ }
5942
+ if (newMessages.length === 0) return 0;
5943
+ const body = {
5944
+ pluginId: this.pluginId,
5945
+ sessionId,
5946
+ messages: newMessages,
5947
+ mode: "append"
5948
+ };
5949
+ const ok = await post("/api/sessions/claude-conversation", body);
5950
+ if (ok) {
5951
+ const last = newMessages[newMessages.length - 1];
5952
+ this.lastUploadedUuid.set(sessionId, last.id);
5953
+ return newMessages.length;
5954
+ }
5955
+ return 0;
5778
5956
  }
5779
5957
  };
5780
5958
 
5959
+ // src/commands/start/quota-fetcher.ts
5960
+ var fs6 = __toESM(require("fs"));
5961
+ var os7 = __toESM(require("os"));
5962
+ var path9 = __toESM(require("path"));
5963
+ var import_child_process4 = require("child_process");
5964
+ var inProgress = false;
5965
+ var HELPER_SCRIPT = `import os,pty,sys,select,signal,struct,fcntl,termios,errno
5966
+ m,s=pty.openpty()
5967
+ try:
5968
+ fcntl.ioctl(s,termios.TIOCSWINSZ,struct.pack('HHHH',30,120,0,0))
5969
+ except Exception:pass
5970
+ pid=os.fork()
5971
+ if pid==0:
5972
+ os.close(m);os.setsid()
5973
+ try:fcntl.ioctl(s,termios.TIOCSCTTY,0)
5974
+ except Exception:pass
5975
+ for fd in[0,1,2]:os.dup2(s,fd)
5976
+ if s>2:os.close(s)
5977
+ os.execvp(sys.argv[1],sys.argv[1:])
5978
+ sys.exit(127)
5979
+ os.close(s)
5980
+ done=[False]
5981
+ def onchld(n,f):
5982
+ try:os.waitpid(pid,os.WNOHANG)
5983
+ except Exception:pass
5984
+ done[0]=True
5985
+ signal.signal(signal.SIGCHLD,onchld)
5986
+ i=sys.stdin.fileno();o=sys.stdout.fileno()
5987
+ while not done[0]:
5988
+ try:r,_,_=select.select([i,m],[],[],0.1)
5989
+ except OSError as e:
5990
+ if e.errno==errno.EINTR:continue
5991
+ break
5992
+ if i in r:
5993
+ try:
5994
+ d=os.read(i,4096)
5995
+ if d:os.write(m,d)
5996
+ else:break
5997
+ except OSError:break
5998
+ if m in r:
5999
+ try:
6000
+ d=os.read(m,4096)
6001
+ if d:os.write(o,d)
6002
+ except OSError:done[0]=True
6003
+ try:os.kill(pid,signal.SIGTERM)
6004
+ except Exception:pass
6005
+ try:
6006
+ _,st=os.waitpid(pid,0)
6007
+ sys.exit((st>>8)&0xFF)
6008
+ except Exception:sys.exit(0)
6009
+ `;
6010
+ function fetchQuotaUsage(historySvc) {
6011
+ if (inProgress) return;
6012
+ inProgress = true;
6013
+ const claudeCmd = findInPath("claude") ? "claude" : "claude-code";
6014
+ if (!claudeCmd) {
6015
+ inProgress = false;
6016
+ return;
6017
+ }
6018
+ const helperPath = path9.join(os7.tmpdir(), "codeam-quota-helper.py");
6019
+ fs6.writeFileSync(helperPath, HELPER_SCRIPT, { mode: 420 });
6020
+ const python = findInPath("python3") ?? findInPath("python");
6021
+ if (!python) {
6022
+ inProgress = false;
6023
+ return;
6024
+ }
6025
+ const proc = (0, import_child_process4.spawn)(python, [helperPath, claudeCmd, "--tools", ""], {
6026
+ stdio: ["pipe", "pipe", "ignore"],
6027
+ cwd: process.cwd(),
6028
+ env: { ...process.env, TERM: "dumb", COLUMNS: "120", LINES: "30" }
6029
+ });
6030
+ let output = "";
6031
+ proc.stdout?.on("data", (chunk) => {
6032
+ output += chunk.toString("utf8");
6033
+ });
6034
+ setTimeout(() => {
6035
+ proc.stdin?.write("/usage\r");
6036
+ setTimeout(() => {
6037
+ const clean = output.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, " ").replace(/\s+/g, " ");
6038
+ const weekMatch = clean.match(/(\d+)%\s*used/i) || clean.match(/(\d+)\s*%/);
6039
+ if (weekMatch) historySvc.setQuotaPercent(parseInt(weekMatch[1], 10));
6040
+ const resetMatch = clean.match(/resets\s+(.+?)(?:\s*\(|$)/im);
6041
+ if (resetMatch) historySvc.setRateLimitReset(resetMatch[1].trim());
6042
+ try {
6043
+ proc.kill();
6044
+ } catch {
6045
+ }
6046
+ try {
6047
+ fs6.unlinkSync(helperPath);
6048
+ } catch {
6049
+ }
6050
+ inProgress = false;
6051
+ }, 5e3);
6052
+ }, 8e3);
6053
+ proc.on("exit", () => {
6054
+ inProgress = false;
6055
+ });
6056
+ setTimeout(() => {
6057
+ try {
6058
+ proc.kill();
6059
+ } catch {
6060
+ }
6061
+ }, 2e4);
6062
+ }
6063
+
6064
+ // src/commands/start/keep-alive.ts
6065
+ var import_child_process5 = require("child_process");
6066
+ function buildKeepAlive(ctx) {
6067
+ let timer = null;
6068
+ async function setIdleTimeout(minutes) {
6069
+ if (!ctx.inCodespace || !ctx.codespaceName) return;
6070
+ await new Promise((resolve2) => {
6071
+ const proc = (0, import_child_process5.spawn)(
6072
+ "gh",
6073
+ [
6074
+ "api",
6075
+ "-X",
6076
+ "PATCH",
6077
+ `/user/codespaces/${ctx.codespaceName}`,
6078
+ "-F",
6079
+ `idle_timeout_minutes=${minutes}`
6080
+ ],
6081
+ { stdio: "ignore", detached: true }
6082
+ );
6083
+ proc.unref();
6084
+ proc.on("exit", () => resolve2());
6085
+ proc.on("error", () => resolve2());
6086
+ });
6087
+ }
6088
+ return {
6089
+ apply(enabled) {
6090
+ if (timer) {
6091
+ clearInterval(timer);
6092
+ timer = null;
6093
+ }
6094
+ if (!ctx.inCodespace || !ctx.codespaceName) return;
6095
+ if (!enabled) {
6096
+ void setIdleTimeout(30);
6097
+ return;
6098
+ }
6099
+ void setIdleTimeout(240);
6100
+ timer = setInterval(() => {
6101
+ void setIdleTimeout(240);
6102
+ }, 30 * 60 * 1e3);
6103
+ }
6104
+ };
6105
+ }
6106
+
6107
+ // src/commands/start/handlers.ts
6108
+ var fs9 = __toESM(require("fs"));
6109
+ var os8 = __toESM(require("os"));
6110
+ var path12 = __toESM(require("path"));
6111
+ var import_crypto = require("crypto");
6112
+ var import_child_process7 = require("child_process");
6113
+
5781
6114
  // src/lib/payload.ts
5782
6115
  var import_zod2 = require("zod");
5783
6116
  var fileEntrySchema = import_zod2.z.object({
@@ -5813,8 +6146,8 @@ function parsePayload(schema, raw) {
5813
6146
  }
5814
6147
 
5815
6148
  // src/services/file-ops.service.ts
5816
- var fs6 = __toESM(require("fs/promises"));
5817
- var path9 = __toESM(require("path"));
6149
+ var fs7 = __toESM(require("fs/promises"));
6150
+ var path10 = __toESM(require("path"));
5818
6151
  var MAX_FILE_BYTES = 5 * 1024 * 1024;
5819
6152
  var MAX_WALK_DEPTH = 6;
5820
6153
  var MAX_VISITED_DIRS = 5e3;
@@ -5849,12 +6182,12 @@ var SUBDIR_IGNORE = /* @__PURE__ */ new Set([
5849
6182
  "__pycache__"
5850
6183
  ]);
5851
6184
  function isUnder(parent, candidate) {
5852
- const rel = path9.relative(parent, candidate);
5853
- return rel === "" || !rel.startsWith("..") && !path9.isAbsolute(rel);
6185
+ const rel = path10.relative(parent, candidate);
6186
+ return rel === "" || !rel.startsWith("..") && !path10.isAbsolute(rel);
5854
6187
  }
5855
6188
  async function isExistingFile(absPath) {
5856
6189
  try {
5857
- const stat3 = await fs6.stat(absPath);
6190
+ const stat3 = await fs7.stat(absPath);
5858
6191
  return stat3.isFile();
5859
6192
  } catch {
5860
6193
  return false;
@@ -5867,13 +6200,13 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
5867
6200
  ctx.visited++;
5868
6201
  let entries = [];
5869
6202
  try {
5870
- entries = await fs6.readdir(dir, { withFileTypes: true });
6203
+ entries = await fs7.readdir(dir, { withFileTypes: true });
5871
6204
  } catch {
5872
6205
  return;
5873
6206
  }
5874
6207
  for (const e of entries) {
5875
6208
  if (!e.isFile()) continue;
5876
- const full = path9.join(dir, e.name);
6209
+ const full = path10.join(dir, e.name);
5877
6210
  if (needleVariants.some((needle) => full.endsWith(needle))) {
5878
6211
  ctx.matches.push(full);
5879
6212
  if (ctx.matches.length >= ctx.cap) return;
@@ -5883,21 +6216,21 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
5883
6216
  if (!e.isDirectory()) continue;
5884
6217
  if (SUBDIR_IGNORE.has(e.name)) continue;
5885
6218
  if (e.name.startsWith(".") && SUBDIR_IGNORE.has(e.name)) continue;
5886
- await walkForSuffix(path9.join(dir, e.name), needleVariants, depth + 1, ctx);
6219
+ await walkForSuffix(path10.join(dir, e.name), needleVariants, depth + 1, ctx);
5887
6220
  if (ctx.matches.length >= ctx.cap) return;
5888
6221
  }
5889
6222
  }
5890
6223
  async function findFile(rawPath) {
5891
6224
  const cwd = process.cwd();
5892
- if (path9.isAbsolute(rawPath)) {
5893
- const abs = path9.normalize(rawPath);
6225
+ if (path10.isAbsolute(rawPath)) {
6226
+ const abs = path10.normalize(rawPath);
5894
6227
  if (isUnder(cwd, abs) && await isExistingFile(abs)) return abs;
5895
6228
  }
5896
- const direct = path9.resolve(cwd, rawPath);
6229
+ const direct = path10.resolve(cwd, rawPath);
5897
6230
  if (isUnder(cwd, direct) && await isExistingFile(direct)) return direct;
5898
- const normalized = path9.normalize(rawPath).replace(/^[./\\]+/, "");
6231
+ const normalized = path10.normalize(rawPath).replace(/^[./\\]+/, "");
5899
6232
  const needles = [
5900
- `${path9.sep}${normalized}`,
6233
+ `${path10.sep}${normalized}`,
5901
6234
  `/${normalized}`
5902
6235
  ].filter((v, i, a) => a.indexOf(v) === i);
5903
6236
  const ctx = { visited: 0, matches: [], cap: 16 };
@@ -5911,7 +6244,7 @@ async function findWriteTarget(rawPath) {
5911
6244
  const found = await findFile(rawPath);
5912
6245
  if (found) return found;
5913
6246
  const cwd = process.cwd();
5914
- const fallback = path9.isAbsolute(rawPath) ? path9.normalize(rawPath) : path9.resolve(cwd, rawPath);
6247
+ const fallback = path10.isAbsolute(rawPath) ? path10.normalize(rawPath) : path10.resolve(cwd, rawPath);
5915
6248
  if (!isUnder(cwd, fallback)) return null;
5916
6249
  return fallback;
5917
6250
  }
@@ -5928,11 +6261,11 @@ async function readProjectFile(rawPath) {
5928
6261
  if (!abs) {
5929
6262
  return { error: `File not found in the project tree: ${rawPath}` };
5930
6263
  }
5931
- const stat3 = await fs6.stat(abs);
6264
+ const stat3 = await fs7.stat(abs);
5932
6265
  if (stat3.size > MAX_FILE_BYTES) {
5933
6266
  return { error: `File too large (${(stat3.size / 1024 / 1024).toFixed(1)} MB > ${MAX_FILE_BYTES / 1024 / 1024} MB).` };
5934
6267
  }
5935
- const buf = await fs6.readFile(abs);
6268
+ const buf = await fs7.readFile(abs);
5936
6269
  if (looksBinary(buf)) {
5937
6270
  return { error: "Binary file \u2014 refusing to open in a code editor." };
5938
6271
  }
@@ -5951,8 +6284,8 @@ async function writeProjectFile(rawPath, content) {
5951
6284
  if (Buffer.byteLength(content, "utf-8") > MAX_FILE_BYTES) {
5952
6285
  return { error: "Content too large." };
5953
6286
  }
5954
- await fs6.mkdir(path9.dirname(abs), { recursive: true });
5955
- await fs6.writeFile(abs, content, "utf-8");
6287
+ await fs7.mkdir(path10.dirname(abs), { recursive: true });
6288
+ await fs7.writeFile(abs, content, "utf-8");
5956
6289
  return { ok: true };
5957
6290
  } catch (e) {
5958
6291
  const msg = e instanceof Error ? e.message : "Write failed";
@@ -5961,11 +6294,11 @@ async function writeProjectFile(rawPath, content) {
5961
6294
  }
5962
6295
 
5963
6296
  // src/services/project-ops.service.ts
5964
- var import_child_process4 = require("child_process");
6297
+ var import_child_process6 = require("child_process");
5965
6298
  var import_util = require("util");
5966
- var fs7 = __toESM(require("fs/promises"));
5967
- var path10 = __toESM(require("path"));
5968
- var execFileP = (0, import_util.promisify)(import_child_process4.execFile);
6299
+ var fs8 = __toESM(require("fs/promises"));
6300
+ var path11 = __toESM(require("path"));
6301
+ var execFileP = (0, import_util.promisify)(import_child_process6.execFile);
5969
6302
  var PROJECT_IGNORE = /* @__PURE__ */ new Set([
5970
6303
  "node_modules",
5971
6304
  ".git",
@@ -6012,7 +6345,7 @@ async function listProjectFiles(opts = {}) {
6012
6345
  }
6013
6346
  let entries = [];
6014
6347
  try {
6015
- entries = await fs7.readdir(dir, { withFileTypes: true });
6348
+ entries = await fs8.readdir(dir, { withFileTypes: true });
6016
6349
  } catch {
6017
6350
  return;
6018
6351
  }
@@ -6022,18 +6355,18 @@ async function listProjectFiles(opts = {}) {
6022
6355
  return;
6023
6356
  }
6024
6357
  if (PROJECT_IGNORE.has(e.name)) continue;
6025
- const full = path10.join(dir, e.name);
6358
+ const full = path11.join(dir, e.name);
6026
6359
  if (e.isDirectory()) {
6027
6360
  if (depth >= 12) continue;
6028
6361
  await walk(full, depth + 1);
6029
6362
  } else if (e.isFile()) {
6030
- const rel = path10.relative(root, full);
6363
+ const rel = path11.relative(root, full);
6031
6364
  if (q2 && !rel.toLowerCase().includes(q2) && !e.name.toLowerCase().includes(q2)) {
6032
6365
  continue;
6033
6366
  }
6034
6367
  let size = 0;
6035
6368
  try {
6036
- const st3 = await fs7.stat(full);
6369
+ const st3 = await fs8.stat(full);
6037
6370
  size = st3.size;
6038
6371
  } catch {
6039
6372
  }
@@ -6135,8 +6468,8 @@ async function gitStatus(cwd) {
6135
6468
  let hasMergeInProgress = false;
6136
6469
  try {
6137
6470
  const gitDir = (await git(["rev-parse", "--git-dir"], root)).stdout.trim();
6138
- const mergeHead = path10.isAbsolute(gitDir) ? path10.join(gitDir, "MERGE_HEAD") : path10.join(root, gitDir, "MERGE_HEAD");
6139
- await fs7.access(mergeHead);
6471
+ const mergeHead = path11.isAbsolute(gitDir) ? path11.join(gitDir, "MERGE_HEAD") : path11.join(root, gitDir, "MERGE_HEAD");
6472
+ await fs8.access(mergeHead);
6140
6473
  hasMergeInProgress = true;
6141
6474
  } catch {
6142
6475
  }
@@ -6209,15 +6542,261 @@ async function gitResolve(file, side, cwd) {
6209
6542
  return { ok: true };
6210
6543
  }
6211
6544
 
6545
+ // src/commands/start/handlers.ts
6546
+ function saveFilesTemp(files) {
6547
+ return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
6548
+ const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
6549
+ const tmpPath = path12.join(os8.tmpdir(), `codeam-${(0, import_crypto.randomUUID)()}-${safeName}`);
6550
+ fs9.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
6551
+ return tmpPath;
6552
+ });
6553
+ }
6554
+ function dispatchPrompt(ctx, prompt) {
6555
+ ctx.outputSvc.newTurn();
6556
+ ctx.claude.sendCommand(prompt);
6557
+ }
6558
+ var startTask = (ctx, _cmd, parsed) => {
6559
+ const { prompt, files } = parsed;
6560
+ const effectivePrompt = prompt ?? "";
6561
+ if (files && files.length > 0) {
6562
+ const paths = saveFilesTemp(files);
6563
+ const atRefs = paths.map((p2) => `@${p2}`).join(" ");
6564
+ ctx.outputSvc.newTurn();
6565
+ ctx.claude.sendCommand(`${atRefs} ${effectivePrompt}`.trim());
6566
+ setTimeout(() => {
6567
+ for (const p2 of paths) {
6568
+ try {
6569
+ fs9.unlinkSync(p2);
6570
+ } catch {
6571
+ }
6572
+ }
6573
+ }, 12e4);
6574
+ } else if (effectivePrompt) {
6575
+ dispatchPrompt(ctx, effectivePrompt);
6576
+ }
6577
+ };
6578
+ var provideInput = (ctx, _cmd, parsed) => {
6579
+ if (parsed.input) dispatchPrompt(ctx, parsed.input);
6580
+ };
6581
+ var selectOption = (ctx, _cmd, parsed) => {
6582
+ const index = parsed.index ?? 0;
6583
+ const from = parsed.from ?? 0;
6584
+ ctx.outputSvc.newTurn();
6585
+ ctx.claude.selectOption(index, from);
6586
+ };
6587
+ var escapeKey = (ctx) => {
6588
+ ctx.outputSvc.newTurn();
6589
+ ctx.claude.sendEscape();
6590
+ };
6591
+ var stopTask = (ctx) => {
6592
+ ctx.claude.interrupt();
6593
+ };
6594
+ var resumeSession = async (ctx, _cmd, parsed) => {
6595
+ const { id, auto } = parsed;
6596
+ if (!id) return;
6597
+ ctx.historySvc.setCurrentConversationId(id);
6598
+ await ctx.historySvc.loadConversation(id);
6599
+ await ctx.outputSvc.newTurnResume(id);
6600
+ ctx.claude.restart(id, auto ?? false);
6601
+ };
6602
+ var getContext = async (ctx, cmd) => {
6603
+ const usage = ctx.historySvc.getCurrentUsage();
6604
+ const monthlyCost = ctx.historySvc.getMonthlyEstimatedCost();
6605
+ const rateLimitReset = ctx.historySvc.getRateLimitReset();
6606
+ const quotaPercent = ctx.historySvc.getQuotaPercent();
6607
+ const base = usage ? { ...usage, monthlyCost } : { used: 0, total: 2e5, percent: 0, model: null, outputTokens: 0, cacheReadTokens: 0, monthlyCost, error: "No usage data found" };
6608
+ const result = {
6609
+ ...base,
6610
+ ...rateLimitReset ? { rateLimitReset } : {},
6611
+ ...quotaPercent !== null ? { quotaPercent } : {}
6612
+ };
6613
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6614
+ };
6615
+ var getConversation = async (ctx, cmd) => {
6616
+ const currentId = ctx.historySvc.getCurrentConversationId();
6617
+ if (!currentId) {
6618
+ await ctx.relay.sendResult(cmd.id, "completed", { conversationId: null });
6619
+ return;
6620
+ }
6621
+ try {
6622
+ await ctx.historySvc.loadConversation(currentId);
6623
+ await ctx.relay.sendResult(cmd.id, "completed", { conversationId: currentId });
6624
+ } catch {
6625
+ await ctx.relay.sendResult(cmd.id, "failed", {});
6626
+ }
6627
+ };
6628
+ var listModels = async (ctx, cmd) => {
6629
+ const models = [
6630
+ { id: "claude-opus-4-7", label: "Claude Opus 4.7", description: "Most capable", family: "claude", vendor: "anthropic", isDefault: false },
6631
+ { id: "claude-opus-4-6", label: "Claude Opus 4.6", description: "Top tier", family: "claude", vendor: "anthropic", isDefault: false },
6632
+ { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", description: "Balanced", family: "claude", vendor: "anthropic", isDefault: true },
6633
+ { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5", description: "Fastest", family: "claude", vendor: "anthropic", isDefault: false }
6634
+ ];
6635
+ await ctx.relay.sendResult(cmd.id, "completed", { models });
6636
+ };
6637
+ var setKeepAlive = async (ctx, cmd) => {
6638
+ const enabled = !!cmd.payload.enabled;
6639
+ ctx.setKeepAlive(enabled);
6640
+ try {
6641
+ await ctx.relay.sendResult(
6642
+ cmd.id,
6643
+ "success",
6644
+ {
6645
+ enabled,
6646
+ applied: enabled && ctx.keepAliveCtx.inCodespace,
6647
+ runtime: ctx.keepAliveCtx.inCodespace ? "github-codespaces" : "local"
6648
+ }
6649
+ );
6650
+ } catch {
6651
+ }
6652
+ };
6653
+ var sessionTerminated = (ctx) => {
6654
+ showInfo("Session was deleted from the app \u2014 exiting.");
6655
+ try {
6656
+ ctx.claude.kill();
6657
+ } catch {
6658
+ }
6659
+ try {
6660
+ const proc = (0, import_child_process7.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
6661
+ detached: true,
6662
+ stdio: "ignore"
6663
+ });
6664
+ proc.unref();
6665
+ } catch {
6666
+ }
6667
+ ctx.outputSvc.dispose();
6668
+ ctx.relay.stop();
6669
+ process.exit(0);
6670
+ };
6671
+ var shutdownSession = async (ctx, cmd) => {
6672
+ try {
6673
+ await ctx.relay.sendResult(cmd.id, "success", { ok: true });
6674
+ } catch {
6675
+ }
6676
+ try {
6677
+ ctx.claude.kill();
6678
+ } catch {
6679
+ }
6680
+ if (ctx.keepAliveCtx.inCodespace && ctx.keepAliveCtx.codespaceName) {
6681
+ try {
6682
+ const stopProc = (0, import_child_process7.spawn)(
6683
+ "bash",
6684
+ ["-lc", `sleep 1; gh codespace stop -c ${JSON.stringify(ctx.keepAliveCtx.codespaceName)} >/dev/null 2>&1 || true`],
6685
+ { detached: true, stdio: "ignore" }
6686
+ );
6687
+ stopProc.unref();
6688
+ } catch {
6689
+ }
6690
+ }
6691
+ try {
6692
+ const proc = (0, import_child_process7.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
6693
+ detached: true,
6694
+ stdio: "ignore"
6695
+ });
6696
+ proc.unref();
6697
+ } catch {
6698
+ }
6699
+ ctx.outputSvc.dispose();
6700
+ ctx.relay.stop();
6701
+ process.exit(0);
6702
+ };
6703
+ var readFile2 = async (ctx, cmd, parsed) => {
6704
+ if (!parsed.path) {
6705
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path" });
6706
+ return;
6707
+ }
6708
+ const result = await readProjectFile(parsed.path);
6709
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6710
+ };
6711
+ var writeFile2 = async (ctx, cmd, parsed) => {
6712
+ if (!parsed.path || typeof parsed.content !== "string") {
6713
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path or content" });
6714
+ return;
6715
+ }
6716
+ const result = await writeProjectFile(parsed.path, parsed.content);
6717
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6718
+ };
6719
+ var listFiles = async (ctx, cmd, parsed) => {
6720
+ const result = await listProjectFiles({ query: parsed.query });
6721
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6722
+ };
6723
+ var gitStatusH = async (ctx, cmd) => {
6724
+ const result = await gitStatus();
6725
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6726
+ };
6727
+ var gitDiffH = async (ctx, cmd, parsed) => {
6728
+ const result = await gitDiff(parsed.path ?? null);
6729
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6730
+ };
6731
+ var gitDiffStagedH = async (ctx, cmd, parsed) => {
6732
+ const result = await gitDiffStaged(parsed.path ?? null);
6733
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6734
+ };
6735
+ var gitLogH = async (ctx, cmd, parsed) => {
6736
+ const result = await gitLog(parsed.limit ?? 30);
6737
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6738
+ };
6739
+ var gitCommitH = async (ctx, cmd, parsed) => {
6740
+ if (!parsed.message) {
6741
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing message" });
6742
+ return;
6743
+ }
6744
+ const result = await gitCommit(parsed.message, parsed.paths);
6745
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6746
+ };
6747
+ var gitPushH = async (ctx, cmd) => {
6748
+ const result = await gitPush();
6749
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6750
+ };
6751
+ var gitPullH = async (ctx, cmd) => {
6752
+ const result = await gitPull();
6753
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6754
+ };
6755
+ var gitResolveH = async (ctx, cmd, parsed) => {
6756
+ if (!parsed.path || !parsed.side) {
6757
+ await ctx.relay.sendResult(cmd.id, "failed", { error: "Missing path or side" });
6758
+ return;
6759
+ }
6760
+ const result = await gitResolve(parsed.path, parsed.side);
6761
+ await ctx.relay.sendResult(cmd.id, "completed", result);
6762
+ };
6763
+ var handlers = {
6764
+ start_task: startTask,
6765
+ provide_input: provideInput,
6766
+ select_option: selectOption,
6767
+ escape_key: escapeKey,
6768
+ stop_task: stopTask,
6769
+ resume_session: resumeSession,
6770
+ get_context: getContext,
6771
+ get_conversation: getConversation,
6772
+ list_models: listModels,
6773
+ set_keep_alive: setKeepAlive,
6774
+ session_terminated: sessionTerminated,
6775
+ shutdown_session: shutdownSession,
6776
+ read_file: readFile2,
6777
+ write_file: writeFile2,
6778
+ list_files: listFiles,
6779
+ git_status: gitStatusH,
6780
+ git_diff: gitDiffH,
6781
+ git_diff_staged: gitDiffStagedH,
6782
+ git_log: gitLogH,
6783
+ git_commit: gitCommitH,
6784
+ git_push: gitPushH,
6785
+ git_pull: gitPullH,
6786
+ git_resolve: gitResolveH
6787
+ };
6788
+ async function dispatchCommand(ctx, cmd) {
6789
+ const parsed = parsePayload(startCommandSchema, cmd.payload);
6790
+ if (!parsed) {
6791
+ showInfo(`Ignoring malformed ${cmd.type} payload.`);
6792
+ return;
6793
+ }
6794
+ const handler = handlers[cmd.type];
6795
+ if (!handler) return;
6796
+ await handler(ctx, cmd, parsed);
6797
+ }
6798
+
6212
6799
  // src/commands/start.ts
6213
- function saveFilesTemp(files) {
6214
- return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
6215
- const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
6216
- const tmpPath = path11.join(os7.tmpdir(), `codeam-${(0, import_crypto.randomUUID)()}-${safeName}`);
6217
- fs8.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
6218
- return tmpPath;
6219
- });
6220
- }
6221
6800
  async function start() {
6222
6801
  showIntro();
6223
6802
  const session = getActiveSession();
@@ -6231,430 +6810,32 @@ async function start() {
6231
6810
  showInfo(`${session.userName} \xB7 ${import_picocolors2.default.cyan(session.plan)}`);
6232
6811
  showInfo("Launching Claude Code...\n");
6233
6812
  const cwd = process.cwd();
6234
- const ws = new WebSocketService(session.id, pluginId);
6235
6813
  const historySvc = new HistoryService(pluginId, cwd);
6236
- let quotaFetchInProgress = false;
6237
- function fetchQuotaUsage() {
6238
- if (quotaFetchInProgress) return;
6239
- quotaFetchInProgress = true;
6240
- const claudeCmd = findInPath("claude") ? "claude" : "claude-code";
6241
- if (!claudeCmd) {
6242
- quotaFetchInProgress = false;
6243
- return;
6244
- }
6245
- const helperScript = `import os,pty,sys,select,signal,struct,fcntl,termios,errno
6246
- m,s=pty.openpty()
6247
- try:
6248
- fcntl.ioctl(s,termios.TIOCSWINSZ,struct.pack('HHHH',30,120,0,0))
6249
- except Exception:pass
6250
- pid=os.fork()
6251
- if pid==0:
6252
- os.close(m);os.setsid()
6253
- try:fcntl.ioctl(s,termios.TIOCSCTTY,0)
6254
- except Exception:pass
6255
- for fd in[0,1,2]:os.dup2(s,fd)
6256
- if s>2:os.close(s)
6257
- os.execvp(sys.argv[1],sys.argv[1:])
6258
- sys.exit(127)
6259
- os.close(s)
6260
- done=[False]
6261
- def onchld(n,f):
6262
- try:os.waitpid(pid,os.WNOHANG)
6263
- except Exception:pass
6264
- done[0]=True
6265
- signal.signal(signal.SIGCHLD,onchld)
6266
- i=sys.stdin.fileno();o=sys.stdout.fileno()
6267
- while not done[0]:
6268
- try:r,_,_=select.select([i,m],[],[],0.1)
6269
- except OSError as e:
6270
- if e.errno==errno.EINTR:continue
6271
- break
6272
- if i in r:
6273
- try:
6274
- d=os.read(i,4096)
6275
- if d:os.write(m,d)
6276
- else:break
6277
- except OSError:break
6278
- if m in r:
6279
- try:
6280
- d=os.read(m,4096)
6281
- if d:os.write(o,d)
6282
- except OSError:done[0]=True
6283
- try:os.kill(pid,signal.SIGTERM)
6284
- except Exception:pass
6285
- try:
6286
- _,st=os.waitpid(pid,0)
6287
- sys.exit((st>>8)&0xFF)
6288
- except Exception:sys.exit(0)
6289
- `;
6290
- const helperPath = path11.join(os7.tmpdir(), "codeam-quota-helper.py");
6291
- fs8.writeFileSync(helperPath, helperScript, { mode: 420 });
6292
- const python = findInPath("python3") ?? findInPath("python");
6293
- if (!python) {
6294
- quotaFetchInProgress = false;
6295
- return;
6296
- }
6297
- const proc = (0, import_child_process5.spawn)(python, [helperPath, claudeCmd, "--tools", ""], {
6298
- stdio: ["pipe", "pipe", "ignore"],
6299
- cwd: process.cwd(),
6300
- env: { ...process.env, TERM: "dumb", COLUMNS: "120", LINES: "30" }
6301
- });
6302
- let output = "";
6303
- proc.stdout?.on("data", (chunk) => {
6304
- output += chunk.toString("utf8");
6305
- });
6306
- setTimeout(() => {
6307
- proc.stdin?.write("/usage\r");
6814
+ const keepAliveCtx = {
6815
+ inCodespace: process.env.CODESPACES === "true",
6816
+ codespaceName: process.env.CODESPACE_NAME
6817
+ };
6818
+ const { apply: setKeepAlive2 } = buildKeepAlive(keepAliveCtx);
6819
+ const outputSvc = new OutputService(
6820
+ session.id,
6821
+ pluginId,
6822
+ (conversationId) => historySvc.setCurrentConversationId(conversationId),
6823
+ (reset) => historySvc.setRateLimitReset(reset),
6824
+ () => {
6825
+ if (historySvc.isQuotaStale()) fetchQuotaUsage(historySvc);
6308
6826
  setTimeout(() => {
6309
- const clean = output.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, " ").replace(/\s+/g, " ");
6310
- const weekMatch = clean.match(/(\d+)%\s*used/i) || clean.match(/(\d+)\s*%/);
6311
- if (weekMatch) {
6312
- historySvc.setQuotaPercent(parseInt(weekMatch[1], 10));
6313
- }
6314
- const resetMatch = clean.match(/resets\s+(.+?)(?:\s*\(|$)/im);
6315
- if (resetMatch) {
6316
- historySvc.setRateLimitReset(resetMatch[1].trim());
6317
- }
6318
- try {
6319
- proc.kill();
6320
- } catch {
6321
- }
6322
- try {
6323
- fs8.unlinkSync(helperPath);
6324
- } catch {
6325
- }
6326
- quotaFetchInProgress = false;
6327
- }, 5e3);
6328
- }, 8e3);
6329
- proc.on("exit", () => {
6330
- quotaFetchInProgress = false;
6331
- });
6332
- setTimeout(() => {
6333
- try {
6334
- proc.kill();
6335
- } catch {
6336
- }
6337
- }, 2e4);
6338
- }
6339
- const outputSvc = new OutputService(session.id, pluginId, (conversationId) => {
6340
- historySvc.setCurrentConversationId(conversationId);
6341
- }, (reset) => {
6342
- historySvc.setRateLimitReset(reset);
6343
- }, () => {
6344
- if (historySvc.isQuotaStale()) {
6345
- fetchQuotaUsage();
6346
- }
6347
- }, () => {
6348
- const prevCount = historySvc.getCurrentMessageCount();
6349
- historySvc.waitForNewUserMessage(prevCount).then((userText) => outputSvc.startTerminalTurn(userText ?? void 0)).catch(() => outputSvc.startTerminalTurn(void 0));
6350
- }, session.pluginAuthToken);
6351
- function sendPrompt(prompt) {
6352
- outputSvc.newTurn();
6353
- claude.sendCommand(prompt);
6354
- }
6355
- const relay = new CommandRelayService(pluginId, async (cmd) => {
6356
- const parsed = parsePayload(startCommandSchema, cmd.payload);
6357
- if (!parsed) {
6358
- showInfo(`Ignoring malformed ${cmd.type} payload.`);
6359
- return;
6360
- }
6361
- switch (cmd.type) {
6362
- case "start_task": {
6363
- const { prompt, files } = parsed;
6364
- const effectivePrompt = prompt ?? "";
6365
- if (files && files.length > 0) {
6366
- const paths = saveFilesTemp(files);
6367
- const atRefs = paths.map((p2) => `@${p2}`).join(" ");
6368
- outputSvc.newTurn();
6369
- claude.sendCommand(`${atRefs} ${effectivePrompt}`.trim());
6370
- setTimeout(() => {
6371
- for (const p2 of paths) {
6372
- try {
6373
- fs8.unlinkSync(p2);
6374
- } catch {
6375
- }
6376
- }
6377
- }, 12e4);
6378
- } else if (effectivePrompt) {
6379
- sendPrompt(effectivePrompt);
6380
- }
6381
- break;
6382
- }
6383
- case "provide_input": {
6384
- const { input } = parsed;
6385
- if (input) sendPrompt(input);
6386
- break;
6387
- }
6388
- case "select_option": {
6389
- const index = parsed.index ?? 0;
6390
- const from = parsed.from ?? 0;
6391
- outputSvc.newTurn();
6392
- claude.selectOption(index, from);
6393
- break;
6394
- }
6395
- case "escape_key":
6396
- outputSvc.newTurn();
6397
- claude.sendEscape();
6398
- break;
6399
- case "stop_task":
6400
- claude.interrupt();
6401
- break;
6402
- case "set_keep_alive": {
6403
- const enabled = !!cmd.payload.enabled;
6404
- const inCodespaceEnv = process.env.CODESPACES === "true";
6405
- setKeepAlive(enabled);
6406
- try {
6407
- await relay.sendResult(
6408
- cmd.id,
6409
- "success",
6410
- { enabled, applied: enabled && inCodespaceEnv, runtime: inCodespaceEnv ? "github-codespaces" : "local" }
6411
- );
6412
- } catch {
6413
- }
6414
- break;
6415
- }
6416
- case "session_terminated": {
6417
- showInfo("Session was deleted from the app \u2014 exiting.");
6418
- try {
6419
- claude.kill();
6420
- } catch {
6421
- }
6422
- try {
6423
- const proc = (0, import_child_process5.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
6424
- detached: true,
6425
- stdio: "ignore"
6426
- });
6427
- proc.unref();
6428
- } catch {
6429
- }
6430
- outputSvc.dispose();
6431
- relay.stop();
6432
- ws.disconnect();
6433
- process.exit(0);
6434
- }
6435
- case "shutdown_session": {
6436
- try {
6437
- await relay.sendResult(cmd.id, "success", { ok: true });
6438
- } catch {
6439
- }
6440
- try {
6441
- claude.kill();
6442
- } catch {
6443
- }
6444
- const codespaceName2 = process.env.CODESPACE_NAME;
6445
- if (codespaceName2 && process.env.CODESPACES === "true") {
6446
- try {
6447
- const stopProc = (0, import_child_process5.spawn)(
6448
- "bash",
6449
- ["-lc", `sleep 1; gh codespace stop -c ${JSON.stringify(codespaceName2)} >/dev/null 2>&1 || true`],
6450
- { detached: true, stdio: "ignore" }
6451
- );
6452
- stopProc.unref();
6453
- } catch {
6454
- }
6455
- }
6456
- try {
6457
- const proc = (0, import_child_process5.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
6458
- detached: true,
6459
- stdio: "ignore"
6460
- });
6461
- proc.unref();
6462
- } catch {
6463
- }
6464
- outputSvc.dispose();
6465
- relay.stop();
6466
- ws.disconnect();
6467
- process.exit(0);
6468
- }
6469
- case "get_context": {
6470
- const usage = historySvc.getCurrentUsage();
6471
- const monthlyCost = historySvc.getMonthlyEstimatedCost();
6472
- const rateLimitReset = historySvc.getRateLimitReset();
6473
- const quotaPercent = historySvc.getQuotaPercent();
6474
- const base = usage ? { ...usage, monthlyCost } : { used: 0, total: 2e5, percent: 0, model: null, outputTokens: 0, cacheReadTokens: 0, monthlyCost, error: "No usage data found" };
6475
- const result = { ...base, ...rateLimitReset ? { rateLimitReset } : {}, ...quotaPercent !== null ? { quotaPercent } : {} };
6476
- await relay.sendResult(cmd.id, "completed", result);
6477
- break;
6478
- }
6479
- case "resume_session": {
6480
- const { id, auto } = parsed;
6481
- if (!id) break;
6482
- historySvc.setCurrentConversationId(id);
6483
- await historySvc.loadConversation(id);
6484
- await outputSvc.newTurnResume(id);
6485
- claude.restart(id, auto ?? false);
6486
- break;
6487
- }
6488
- case "get_conversation": {
6489
- const currentId = historySvc.getCurrentConversationId();
6490
- if (currentId) {
6491
- try {
6492
- await historySvc.loadConversation(currentId);
6493
- await relay.sendResult(cmd.id, "completed", { conversationId: currentId });
6494
- } catch {
6495
- await relay.sendResult(cmd.id, "failed", {});
6496
- }
6497
- } else {
6498
- await relay.sendResult(cmd.id, "completed", { conversationId: null });
6499
- }
6500
- break;
6501
- }
6502
- case "list_models": {
6503
- const models = [
6504
- { id: "claude-opus-4-7", label: "Claude Opus 4.7", description: "Most capable", family: "claude", vendor: "anthropic", isDefault: false },
6505
- { id: "claude-opus-4-6", label: "Claude Opus 4.6", description: "Top tier", family: "claude", vendor: "anthropic", isDefault: false },
6506
- { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", description: "Balanced", family: "claude", vendor: "anthropic", isDefault: true },
6507
- { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5", description: "Fastest", family: "claude", vendor: "anthropic", isDefault: false }
6508
- ];
6509
- await relay.sendResult(cmd.id, "completed", { models });
6510
- break;
6511
- }
6512
- case "read_file": {
6513
- const { path: filePath } = parsed;
6514
- if (!filePath) {
6515
- await relay.sendResult(cmd.id, "failed", { error: "Missing path" });
6516
- break;
6517
- }
6518
- const result = await readProjectFile(filePath);
6519
- await relay.sendResult(cmd.id, "completed", result);
6520
- break;
6521
- }
6522
- case "write_file": {
6523
- const { path: filePath, content } = parsed;
6524
- if (!filePath || typeof content !== "string") {
6525
- await relay.sendResult(cmd.id, "failed", { error: "Missing path or content" });
6526
- break;
6527
- }
6528
- const result = await writeProjectFile(filePath, content);
6529
- await relay.sendResult(cmd.id, "completed", result);
6530
- break;
6531
- }
6532
- case "list_files": {
6533
- const result = await listProjectFiles({ query: parsed.query });
6534
- await relay.sendResult(cmd.id, "completed", result);
6535
- break;
6536
- }
6537
- case "git_status": {
6538
- const result = await gitStatus();
6539
- await relay.sendResult(cmd.id, "completed", result);
6540
- break;
6541
- }
6542
- case "git_diff": {
6543
- const { path: filePath } = parsed;
6544
- const result = await gitDiff(filePath ?? null);
6545
- await relay.sendResult(cmd.id, "completed", result);
6546
- break;
6547
- }
6548
- case "git_diff_staged": {
6549
- const { path: filePath } = parsed;
6550
- const result = await gitDiffStaged(filePath ?? null);
6551
- await relay.sendResult(cmd.id, "completed", result);
6552
- break;
6553
- }
6554
- case "git_log": {
6555
- const result = await gitLog(parsed.limit ?? 30);
6556
- await relay.sendResult(cmd.id, "completed", result);
6557
- break;
6558
- }
6559
- case "git_commit": {
6560
- if (!parsed.message) {
6561
- await relay.sendResult(cmd.id, "failed", { error: "Missing message" });
6562
- break;
6563
- }
6564
- const result = await gitCommit(parsed.message, parsed.paths);
6565
- await relay.sendResult(cmd.id, "completed", result);
6566
- break;
6567
- }
6568
- case "git_push": {
6569
- const result = await gitPush();
6570
- await relay.sendResult(cmd.id, "completed", result);
6571
- break;
6572
- }
6573
- case "git_pull": {
6574
- const result = await gitPull();
6575
- await relay.sendResult(cmd.id, "completed", result);
6576
- break;
6577
- }
6578
- case "git_resolve": {
6579
- const { path: filePath, side } = parsed;
6580
- if (!filePath || !side) {
6581
- await relay.sendResult(cmd.id, "failed", { error: "Missing path or side" });
6582
- break;
6583
- }
6584
- const result = await gitResolve(filePath, side);
6585
- await relay.sendResult(cmd.id, "completed", result);
6586
- break;
6587
- }
6588
- }
6589
- });
6590
- ws.addHandler({
6591
- onConnected() {
6827
+ historySvc.uploadDelta().catch(() => {
6828
+ });
6829
+ }, 400);
6592
6830
  },
6593
- onDisconnected() {
6831
+ () => {
6832
+ const prevCount = historySvc.getCurrentMessageCount();
6833
+ historySvc.waitForNewUserMessage(prevCount).then((userText) => outputSvc.startTerminalTurn(userText ?? void 0)).catch(() => outputSvc.startTerminalTurn(void 0));
6594
6834
  },
6595
- onMessage(type, payload) {
6596
- if (type !== "agent_command") return;
6597
- const cmdType = typeof payload.type === "string" ? payload.type : null;
6598
- if (!cmdType) return;
6599
- const parsed = parsePayload(startCommandSchema, payload.payload ?? {});
6600
- if (!parsed) {
6601
- showInfo(`Ignoring malformed ${cmdType} payload (ws).`);
6602
- return;
6603
- }
6604
- if (cmdType === "start_task") {
6605
- const { prompt, files } = parsed;
6606
- const effectivePrompt = prompt ?? "";
6607
- if (files && files.length > 0) {
6608
- const paths = saveFilesTemp(files);
6609
- const atRefs = paths.map((p2) => `@${p2}`).join(" ");
6610
- outputSvc.newTurn();
6611
- claude.sendCommand(`${atRefs} ${effectivePrompt}`.trim());
6612
- setTimeout(() => {
6613
- for (const p2 of paths) {
6614
- try {
6615
- fs8.unlinkSync(p2);
6616
- } catch {
6617
- }
6618
- }
6619
- }, 12e4);
6620
- } else if (effectivePrompt) {
6621
- sendPrompt(effectivePrompt);
6622
- }
6623
- } else if (cmdType === "provide_input") {
6624
- const { input } = parsed;
6625
- if (input) sendPrompt(input);
6626
- } else if (cmdType === "select_option") {
6627
- const index = parsed.index ?? 0;
6628
- const from = parsed.from ?? 0;
6629
- outputSvc.newTurn();
6630
- claude.selectOption(index, from);
6631
- } else if (cmdType === "escape_key") {
6632
- outputSvc.newTurn();
6633
- claude.sendEscape();
6634
- } else if (cmdType === "stop_task") {
6635
- claude.interrupt();
6636
- } else if (cmdType === "get_conversation") {
6637
- const currentId = historySvc.getCurrentConversationId();
6638
- if (currentId) {
6639
- historySvc.loadConversation(currentId).catch(() => {
6640
- });
6641
- }
6642
- } else if (cmdType === "resume_session") {
6643
- const { id, auto } = parsed;
6644
- if (id) {
6645
- const autoFlag = auto ?? false;
6646
- historySvc.loadConversation(id).then(() => outputSvc.newTurnResume(id)).then(() => {
6647
- claude.restart(id, autoFlag);
6648
- }).catch(() => {
6649
- });
6650
- }
6651
- }
6652
- }
6653
- });
6654
- ws.connect();
6655
- relay.start();
6835
+ session.pluginAuthToken
6836
+ );
6656
6837
  const claude = new ClaudeService({
6657
- cwd: process.cwd(),
6838
+ cwd,
6658
6839
  onData(raw) {
6659
6840
  outputSvc.push(raw);
6660
6841
  },
@@ -6662,18 +6843,29 @@ except Exception:sys.exit(0)
6662
6843
  process.removeListener("SIGINT", sigintHandler);
6663
6844
  outputSvc.dispose();
6664
6845
  relay.stop();
6665
- ws.disconnect();
6666
6846
  process.exit(code);
6667
6847
  }
6668
6848
  });
6849
+ const ctx = {
6850
+ outputSvc,
6851
+ claude,
6852
+ historySvc,
6853
+ relay: void 0,
6854
+ setKeepAlive: setKeepAlive2,
6855
+ keepAliveCtx
6856
+ };
6857
+ const relay = new CommandRelayService(pluginId, async (cmd) => {
6858
+ await dispatchCommand(ctx, cmd);
6859
+ });
6860
+ ctx.relay = relay;
6669
6861
  function sigintHandler() {
6670
6862
  claude.kill();
6671
6863
  outputSvc.dispose();
6672
6864
  relay.stop();
6673
- ws.disconnect();
6674
6865
  process.exit(0);
6675
6866
  }
6676
6867
  process.once("SIGINT", sigintHandler);
6868
+ relay.start();
6677
6869
  await claude.spawn();
6678
6870
  setTimeout(() => {
6679
6871
  historySvc.detectCurrentConversation();
@@ -6685,47 +6877,7 @@ except Exception:sys.exit(0)
6685
6877
  });
6686
6878
  }
6687
6879
  }, 2e3);
6688
- setTimeout(() => {
6689
- fetchQuotaUsage();
6690
- }, 5e3);
6691
- const inCodespace = process.env.CODESPACES === "true";
6692
- const codespaceName = process.env.CODESPACE_NAME;
6693
- let keepAliveTimer = null;
6694
- async function setIdleTimeout(minutes) {
6695
- if (!inCodespace || !codespaceName) return;
6696
- await new Promise((resolve2) => {
6697
- const proc = (0, import_child_process5.spawn)(
6698
- "gh",
6699
- [
6700
- "api",
6701
- "-X",
6702
- "PATCH",
6703
- `/user/codespaces/${codespaceName}`,
6704
- "-F",
6705
- `idle_timeout_minutes=${minutes}`
6706
- ],
6707
- { stdio: "ignore", detached: true }
6708
- );
6709
- proc.unref();
6710
- proc.on("exit", () => resolve2());
6711
- proc.on("error", () => resolve2());
6712
- });
6713
- }
6714
- function setKeepAlive(enabled) {
6715
- if (keepAliveTimer) {
6716
- clearInterval(keepAliveTimer);
6717
- keepAliveTimer = null;
6718
- }
6719
- if (!inCodespace || !codespaceName) return;
6720
- if (!enabled) {
6721
- void setIdleTimeout(30);
6722
- return;
6723
- }
6724
- void setIdleTimeout(240);
6725
- keepAliveTimer = setInterval(() => {
6726
- void setIdleTimeout(240);
6727
- }, 30 * 60 * 1e3);
6728
- }
6880
+ setTimeout(() => fetchQuotaUsage(historySvc), 5e3);
6729
6881
  }
6730
6882
 
6731
6883
  // src/commands/pair.ts
@@ -6906,19 +7058,19 @@ async function logout() {
6906
7058
  }
6907
7059
 
6908
7060
  // src/commands/deploy.ts
6909
- var import_child_process10 = require("child_process");
6910
- var fs9 = __toESM(require("fs"));
6911
- var os8 = __toESM(require("os"));
6912
- var path16 = __toESM(require("path"));
7061
+ var import_child_process12 = require("child_process");
7062
+ var fs10 = __toESM(require("fs"));
7063
+ var os9 = __toESM(require("os"));
7064
+ var path17 = __toESM(require("path"));
6913
7065
  var import_util6 = require("util");
6914
7066
  var import_picocolors9 = __toESM(require("picocolors"));
6915
7067
 
6916
7068
  // src/services/providers/github-codespaces.ts
6917
- var import_child_process6 = require("child_process");
7069
+ var import_child_process8 = require("child_process");
6918
7070
  var import_util2 = require("util");
6919
7071
  var import_picocolors7 = __toESM(require("picocolors"));
6920
- var path12 = __toESM(require("path"));
6921
- var execFileP2 = (0, import_util2.promisify)(import_child_process6.execFile);
7072
+ var path13 = __toESM(require("path"));
7073
+ var execFileP2 = (0, import_util2.promisify)(import_child_process8.execFile);
6922
7074
  var MAX_BUFFER = 8 * 1024 * 1024;
6923
7075
  function resetStdinForChild() {
6924
7076
  if (process.stdin.isTTY) {
@@ -6962,7 +7114,7 @@ var GitHubCodespacesProvider = class {
6962
7114
  if (!isAuthed) {
6963
7115
  resetStdinForChild();
6964
7116
  await new Promise((resolve2, reject) => {
6965
- const proc = (0, import_child_process6.spawn)("gh", ["auth", "login", "-s", "codespace,repo,read:user"], {
7117
+ const proc = (0, import_child_process8.spawn)("gh", ["auth", "login", "-s", "codespace,repo,read:user"], {
6966
7118
  stdio: "inherit"
6967
7119
  });
6968
7120
  proc.on("exit", (code) => {
@@ -6996,7 +7148,7 @@ var GitHubCodespacesProvider = class {
6996
7148
  wt(noteLines.join("\n"), "One more permission needed");
6997
7149
  resetStdinForChild();
6998
7150
  const refreshCode = await new Promise((resolve2, reject) => {
6999
- const proc = (0, import_child_process6.spawn)(
7151
+ const proc = (0, import_child_process8.spawn)(
7000
7152
  "gh",
7001
7153
  ["auth", "refresh", "-h", "github.com", "-s", "codespace"],
7002
7154
  { stdio: "inherit" }
@@ -7146,7 +7298,7 @@ var GitHubCodespacesProvider = class {
7146
7298
  O2.step(`Installing gh via ${installCmd.describe}\u2026`);
7147
7299
  resetStdinForChild();
7148
7300
  const ok = await new Promise((resolve2) => {
7149
- const proc = (0, import_child_process6.spawn)(installCmd.exe, installCmd.args, { stdio: "inherit" });
7301
+ const proc = (0, import_child_process8.spawn)(installCmd.exe, installCmd.args, { stdio: "inherit" });
7150
7302
  proc.on("exit", (code) => resolve2(code === 0));
7151
7303
  proc.on("error", () => resolve2(false));
7152
7304
  });
@@ -7173,7 +7325,7 @@ var GitHubCodespacesProvider = class {
7173
7325
  );
7174
7326
  resetStdinForChild();
7175
7327
  await new Promise((resolve2, reject) => {
7176
- const proc = (0, import_child_process6.spawn)(
7328
+ const proc = (0, import_child_process8.spawn)(
7177
7329
  "gh",
7178
7330
  ["auth", "refresh", "-h", "github.com", "-s", "repo,read:org"],
7179
7331
  { stdio: "inherit" }
@@ -7351,7 +7503,7 @@ var GitHubCodespacesProvider = class {
7351
7503
  async streamCommand(workspaceId, command2) {
7352
7504
  resetStdinForChild();
7353
7505
  return new Promise((resolve2, reject) => {
7354
- const proc = (0, import_child_process6.spawn)(
7506
+ const proc = (0, import_child_process8.spawn)(
7355
7507
  "gh",
7356
7508
  ["codespace", "ssh", "-c", workspaceId, "--", "-tt", command2],
7357
7509
  { stdio: "inherit" }
@@ -7378,11 +7530,11 @@ var GitHubCodespacesProvider = class {
7378
7530
  `mkdir -p ${shellQuote(remoteDir)} && tar -xzf - -C ${shellQuote(remoteDir)}`
7379
7531
  ];
7380
7532
  await new Promise((resolve2, reject) => {
7381
- const tar = (0, import_child_process6.spawn)("tar", tarArgs, {
7533
+ const tar = (0, import_child_process8.spawn)("tar", tarArgs, {
7382
7534
  stdio: ["ignore", "pipe", "pipe"],
7383
7535
  env: tarEnv
7384
7536
  });
7385
- const ssh = (0, import_child_process6.spawn)("gh", sshArgs, {
7537
+ const ssh = (0, import_child_process8.spawn)("gh", sshArgs, {
7386
7538
  stdio: [tar.stdout, "pipe", "pipe"]
7387
7539
  });
7388
7540
  let tarErr = "";
@@ -7406,7 +7558,7 @@ var GitHubCodespacesProvider = class {
7406
7558
  });
7407
7559
  }
7408
7560
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
7409
- const remoteDir = path12.posix.dirname(remotePath);
7561
+ const remoteDir = path13.posix.dirname(remotePath);
7410
7562
  const parts = [
7411
7563
  `mkdir -p ${shellQuote(remoteDir)}`,
7412
7564
  `cat > ${shellQuote(remotePath)}`
@@ -7416,7 +7568,7 @@ var GitHubCodespacesProvider = class {
7416
7568
  }
7417
7569
  const cmd = parts.join(" && ");
7418
7570
  await new Promise((resolve2, reject) => {
7419
- const proc = (0, import_child_process6.spawn)(
7571
+ const proc = (0, import_child_process8.spawn)(
7420
7572
  "gh",
7421
7573
  ["codespace", "ssh", "-c", workspaceId, "--", cmd],
7422
7574
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -7474,11 +7626,11 @@ function shellQuote(s) {
7474
7626
  }
7475
7627
 
7476
7628
  // src/services/providers/gitpod.ts
7477
- var import_child_process7 = require("child_process");
7629
+ var import_child_process9 = require("child_process");
7478
7630
  var import_util3 = require("util");
7479
- var path13 = __toESM(require("path"));
7631
+ var path14 = __toESM(require("path"));
7480
7632
  var import_picocolors8 = __toESM(require("picocolors"));
7481
- var execFileP3 = (0, import_util3.promisify)(import_child_process7.execFile);
7633
+ var execFileP3 = (0, import_util3.promisify)(import_child_process9.execFile);
7482
7634
  var MAX_BUFFER2 = 8 * 1024 * 1024;
7483
7635
  function resetStdinForChild2() {
7484
7636
  if (process.stdin.isTTY) {
@@ -7518,7 +7670,7 @@ var GitpodProvider = class {
7518
7670
  );
7519
7671
  resetStdinForChild2();
7520
7672
  await new Promise((resolve2, reject) => {
7521
- const proc = (0, import_child_process7.spawn)("gitpod", ["login"], { stdio: "inherit" });
7673
+ const proc = (0, import_child_process9.spawn)("gitpod", ["login"], { stdio: "inherit" });
7522
7674
  proc.on("exit", (code) => {
7523
7675
  if (code === 0) resolve2();
7524
7676
  else reject(new Error("gitpod login failed."));
@@ -7670,7 +7822,7 @@ var GitpodProvider = class {
7670
7822
  async streamCommand(workspaceId, command2) {
7671
7823
  resetStdinForChild2();
7672
7824
  return new Promise((resolve2, reject) => {
7673
- const proc = (0, import_child_process7.spawn)(
7825
+ const proc = (0, import_child_process9.spawn)(
7674
7826
  "gitpod",
7675
7827
  ["workspace", "ssh", workspaceId, "--", "-tt", command2],
7676
7828
  { stdio: "inherit" }
@@ -7690,11 +7842,11 @@ var GitpodProvider = class {
7690
7842
  const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
7691
7843
  const remoteCmd = `mkdir -p ${shellQuote2(remoteDir)} && tar -xzf - -C ${shellQuote2(remoteDir)}`;
7692
7844
  await new Promise((resolve2, reject) => {
7693
- const tar = (0, import_child_process7.spawn)("tar", tarArgs, {
7845
+ const tar = (0, import_child_process9.spawn)("tar", tarArgs, {
7694
7846
  stdio: ["ignore", "pipe", "pipe"],
7695
7847
  env: tarEnv
7696
7848
  });
7697
- const ssh = (0, import_child_process7.spawn)(
7849
+ const ssh = (0, import_child_process9.spawn)(
7698
7850
  "gitpod",
7699
7851
  ["workspace", "ssh", workspaceId, "--", remoteCmd],
7700
7852
  { stdio: [tar.stdout, "pipe", "pipe"] }
@@ -7716,7 +7868,7 @@ var GitpodProvider = class {
7716
7868
  });
7717
7869
  }
7718
7870
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
7719
- const remoteDir = path13.posix.dirname(remotePath);
7871
+ const remoteDir = path14.posix.dirname(remotePath);
7720
7872
  const parts = [
7721
7873
  `mkdir -p ${shellQuote2(remoteDir)}`,
7722
7874
  `cat > ${shellQuote2(remotePath)}`
@@ -7726,7 +7878,7 @@ var GitpodProvider = class {
7726
7878
  }
7727
7879
  const cmd = parts.join(" && ");
7728
7880
  await new Promise((resolve2, reject) => {
7729
- const proc = (0, import_child_process7.spawn)(
7881
+ const proc = (0, import_child_process9.spawn)(
7730
7882
  "gitpod",
7731
7883
  ["workspace", "ssh", workspaceId, "--", cmd],
7732
7884
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -7750,10 +7902,10 @@ function shellQuote2(s) {
7750
7902
  }
7751
7903
 
7752
7904
  // src/services/providers/gitlab-workspaces.ts
7753
- var import_child_process8 = require("child_process");
7905
+ var import_child_process10 = require("child_process");
7754
7906
  var import_util4 = require("util");
7755
- var path14 = __toESM(require("path"));
7756
- var execFileP4 = (0, import_util4.promisify)(import_child_process8.execFile);
7907
+ var path15 = __toESM(require("path"));
7908
+ var execFileP4 = (0, import_util4.promisify)(import_child_process10.execFile);
7757
7909
  var MAX_BUFFER3 = 8 * 1024 * 1024;
7758
7910
  var GITLAB_API_BASE = process.env.CODEAM_GITLAB_API_URL ?? "https://gitlab.com/api/v4";
7759
7911
  function resetStdinForChild3() {
@@ -7795,7 +7947,7 @@ var GitLabWorkspacesProvider = class {
7795
7947
  );
7796
7948
  resetStdinForChild3();
7797
7949
  await new Promise((resolve2, reject) => {
7798
- const proc = (0, import_child_process8.spawn)(
7950
+ const proc = (0, import_child_process10.spawn)(
7799
7951
  "glab",
7800
7952
  ["auth", "login", "--scopes", "api,read_user,read_repository"],
7801
7953
  { stdio: "inherit" }
@@ -7967,7 +8119,7 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
7967
8119
  const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
7968
8120
  resetStdinForChild3();
7969
8121
  return new Promise((resolve2, reject) => {
7970
- const proc = (0, import_child_process8.spawn)(
8122
+ const proc = (0, import_child_process10.spawn)(
7971
8123
  "ssh",
7972
8124
  ["-tt", "-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, command2],
7973
8125
  { stdio: "inherit" }
@@ -7988,8 +8140,8 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
7988
8140
  const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
7989
8141
  const remoteCmd = `mkdir -p ${shellQuote3(remoteDir)} && tar -xzf - -C ${shellQuote3(remoteDir)}`;
7990
8142
  await new Promise((resolve2, reject) => {
7991
- const tar = (0, import_child_process8.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
7992
- const ssh = (0, import_child_process8.spawn)(
8143
+ const tar = (0, import_child_process10.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
8144
+ const ssh = (0, import_child_process10.spawn)(
7993
8145
  "ssh",
7994
8146
  ["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, remoteCmd],
7995
8147
  { stdio: [tar.stdout, "pipe", "pipe"] }
@@ -8012,14 +8164,14 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
8012
8164
  }
8013
8165
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
8014
8166
  const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
8015
- const remoteDir = path14.posix.dirname(remotePath);
8167
+ const remoteDir = path15.posix.dirname(remotePath);
8016
8168
  const parts = [`mkdir -p ${shellQuote3(remoteDir)}`, `cat > ${shellQuote3(remotePath)}`];
8017
8169
  if (options.mode != null) {
8018
8170
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote3(remotePath)}`);
8019
8171
  }
8020
8172
  const cmd = parts.join(" && ");
8021
8173
  await new Promise((resolve2, reject) => {
8022
- const proc = (0, import_child_process8.spawn)(
8174
+ const proc = (0, import_child_process10.spawn)(
8023
8175
  "ssh",
8024
8176
  ["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, cmd],
8025
8177
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -8078,10 +8230,10 @@ function shellQuote3(s) {
8078
8230
  }
8079
8231
 
8080
8232
  // src/services/providers/railway.ts
8081
- var import_child_process9 = require("child_process");
8233
+ var import_child_process11 = require("child_process");
8082
8234
  var import_util5 = require("util");
8083
- var path15 = __toESM(require("path"));
8084
- var execFileP5 = (0, import_util5.promisify)(import_child_process9.execFile);
8235
+ var path16 = __toESM(require("path"));
8236
+ var execFileP5 = (0, import_util5.promisify)(import_child_process11.execFile);
8085
8237
  var MAX_BUFFER4 = 8 * 1024 * 1024;
8086
8238
  function resetStdinForChild4() {
8087
8239
  if (process.stdin.isTTY) {
@@ -8122,7 +8274,7 @@ var RailwayProvider = class {
8122
8274
  );
8123
8275
  resetStdinForChild4();
8124
8276
  await new Promise((resolve2, reject) => {
8125
- const proc = (0, import_child_process9.spawn)("railway", ["login"], { stdio: "inherit" });
8277
+ const proc = (0, import_child_process11.spawn)("railway", ["login"], { stdio: "inherit" });
8126
8278
  proc.on("exit", (code) => {
8127
8279
  if (code === 0) resolve2();
8128
8280
  else reject(new Error("railway login failed."));
@@ -8265,7 +8417,7 @@ var RailwayProvider = class {
8265
8417
  }
8266
8418
  resetStdinForChild4();
8267
8419
  return new Promise((resolve2, reject) => {
8268
- const proc = (0, import_child_process9.spawn)(
8420
+ const proc = (0, import_child_process11.spawn)(
8269
8421
  "railway",
8270
8422
  ["shell", "--project", projectId, "--service", serviceId, "--command", command2],
8271
8423
  { stdio: "inherit" }
@@ -8289,8 +8441,8 @@ var RailwayProvider = class {
8289
8441
  const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
8290
8442
  const remoteCmd = `mkdir -p ${shellQuote4(remoteDir)} && tar -xzf - -C ${shellQuote4(remoteDir)}`;
8291
8443
  await new Promise((resolve2, reject) => {
8292
- const tar = (0, import_child_process9.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
8293
- const sh = (0, import_child_process9.spawn)(
8444
+ const tar = (0, import_child_process11.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
8445
+ const sh = (0, import_child_process11.spawn)(
8294
8446
  "railway",
8295
8447
  ["shell", "--project", projectId, "--service", serviceId, "--command", remoteCmd],
8296
8448
  { stdio: [tar.stdout, "pipe", "pipe"] }
@@ -8316,14 +8468,14 @@ var RailwayProvider = class {
8316
8468
  if (!projectId || !serviceId) {
8317
8469
  throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
8318
8470
  }
8319
- const remoteDir = path15.posix.dirname(remotePath);
8471
+ const remoteDir = path16.posix.dirname(remotePath);
8320
8472
  const parts = [`mkdir -p ${shellQuote4(remoteDir)}`, `cat > ${shellQuote4(remotePath)}`];
8321
8473
  if (options.mode != null) {
8322
8474
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote4(remotePath)}`);
8323
8475
  }
8324
8476
  const cmd = parts.join(" && ");
8325
8477
  await new Promise((resolve2, reject) => {
8326
- const proc = (0, import_child_process9.spawn)(
8478
+ const proc = (0, import_child_process11.spawn)(
8327
8479
  "railway",
8328
8480
  ["shell", "--project", projectId, "--service", serviceId, "--command", cmd],
8329
8481
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -8355,7 +8507,7 @@ var PROVIDERS = [
8355
8507
  ];
8356
8508
 
8357
8509
  // src/commands/deploy.ts
8358
- var execFileP6 = (0, import_util6.promisify)(import_child_process10.execFile);
8510
+ var execFileP6 = (0, import_util6.promisify)(import_child_process12.execFile);
8359
8511
  async function deploy() {
8360
8512
  console.log();
8361
8513
  mt(import_picocolors9.default.bgMagenta(import_picocolors9.default.white(" codeam deploy ")));
@@ -8508,7 +8660,7 @@ async function deploy() {
8508
8660
  process.exit(1);
8509
8661
  }
8510
8662
  }
8511
- const localClaudeDir = path16.join(os8.homedir(), ".claude");
8663
+ const localClaudeDir = path17.join(os9.homedir(), ".claude");
8512
8664
  const localCredsKind = await detectLocalClaudeCredentials(localClaudeDir);
8513
8665
  let bridged = "none";
8514
8666
  if (localCredsKind !== "none") {
@@ -8552,7 +8704,7 @@ async function deploy() {
8552
8704
  process.exit(1);
8553
8705
  }
8554
8706
  claudeStep.stop("\u2713 Claude CLI installed");
8555
- const haveLocalClaude = fs9.existsSync(localClaudeDir) && fs9.statSync(localClaudeDir).isDirectory();
8707
+ const haveLocalClaude = fs10.existsSync(localClaudeDir) && fs10.statSync(localClaudeDir).isDirectory();
8556
8708
  if (haveLocalClaude) {
8557
8709
  const copyStep = fe();
8558
8710
  copyStep.start("Copying local Claude config to workspace\u2026");
@@ -8606,10 +8758,10 @@ async function deploy() {
8606
8758
  }
8607
8759
  }
8608
8760
  if (bridged !== "none") {
8609
- const localClaudeJson = path16.join(os8.homedir(), ".claude.json");
8610
- if (fs9.existsSync(localClaudeJson)) {
8761
+ const localClaudeJson = path17.join(os9.homedir(), ".claude.json");
8762
+ if (fs10.existsSync(localClaudeJson)) {
8611
8763
  try {
8612
- const contents = fs9.readFileSync(localClaudeJson);
8764
+ const contents = fs10.readFileSync(localClaudeJson);
8613
8765
  await provider.uploadFile(
8614
8766
  workspace.id,
8615
8767
  "/home/codespace/.claude.json",
@@ -8799,7 +8951,7 @@ async function runRemoteClaudeLogin(provider, workspaceId) {
8799
8951
  }
8800
8952
  }
8801
8953
  async function detectLocalClaudeCredentials(localClaudeDir) {
8802
- if (fs9.existsSync(path16.join(localClaudeDir, ".credentials.json"))) {
8954
+ if (fs10.existsSync(path17.join(localClaudeDir, ".credentials.json"))) {
8803
8955
  return "flat-file";
8804
8956
  }
8805
8957
  if (process.platform === "darwin") {
@@ -8832,8 +8984,8 @@ async function verifyClaudeAuth(provider, workspaceId) {
8832
8984
  }
8833
8985
  }
8834
8986
  async function bridgeClaudeCredentials(provider, workspaceId, localClaudeDir) {
8835
- const fileBased = path16.join(localClaudeDir, ".credentials.json");
8836
- if (fs9.existsSync(fileBased)) return "flat-file";
8987
+ const fileBased = path17.join(localClaudeDir, ".credentials.json");
8988
+ if (fs10.existsSync(fileBased)) return "flat-file";
8837
8989
  if (process.platform === "darwin") {
8838
8990
  try {
8839
8991
  const { stdout } = await execFileP6(
@@ -9041,7 +9193,7 @@ async function stopWorkspaceFromLocal(target) {
9041
9193
  // src/commands/version.ts
9042
9194
  var import_picocolors11 = __toESM(require("picocolors"));
9043
9195
  function version() {
9044
- const v = true ? "2.4.38" : "unknown";
9196
+ const v = true ? "2.5.0" : "unknown";
9045
9197
  console.log(`${import_picocolors11.default.bold("codeam-cli")} ${import_picocolors11.default.cyan(v)}`);
9046
9198
  }
9047
9199
 
@@ -9078,22 +9230,22 @@ function help() {
9078
9230
  }
9079
9231
 
9080
9232
  // src/lib/updateNotifier.ts
9081
- var fs10 = __toESM(require("fs"));
9082
- var os9 = __toESM(require("os"));
9083
- var path17 = __toESM(require("path"));
9084
- var https4 = __toESM(require("https"));
9233
+ var fs11 = __toESM(require("fs"));
9234
+ var os10 = __toESM(require("os"));
9235
+ var path18 = __toESM(require("path"));
9236
+ var https5 = __toESM(require("https"));
9085
9237
  var import_picocolors13 = __toESM(require("picocolors"));
9086
9238
  var PKG_NAME = "codeam-cli";
9087
9239
  var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
9088
9240
  var TTL_MS = 24 * 60 * 60 * 1e3;
9089
9241
  var REQUEST_TIMEOUT_MS = 1500;
9090
9242
  function cachePath() {
9091
- const dir = path17.join(os9.homedir(), ".codeam");
9092
- return path17.join(dir, "update-check.json");
9243
+ const dir = path18.join(os10.homedir(), ".codeam");
9244
+ return path18.join(dir, "update-check.json");
9093
9245
  }
9094
9246
  function readCache() {
9095
9247
  try {
9096
- const raw = fs10.readFileSync(cachePath(), "utf8");
9248
+ const raw = fs11.readFileSync(cachePath(), "utf8");
9097
9249
  const parsed = JSON.parse(raw);
9098
9250
  if (typeof parsed.fetchedAt !== "number" || typeof parsed.latest !== "string") return null;
9099
9251
  return parsed;
@@ -9104,8 +9256,8 @@ function readCache() {
9104
9256
  function writeCache(cache) {
9105
9257
  try {
9106
9258
  const file = cachePath();
9107
- fs10.mkdirSync(path17.dirname(file), { recursive: true });
9108
- fs10.writeFileSync(file, JSON.stringify(cache));
9259
+ fs11.mkdirSync(path18.dirname(file), { recursive: true });
9260
+ fs11.writeFileSync(file, JSON.stringify(cache));
9109
9261
  } catch {
9110
9262
  }
9111
9263
  }
@@ -9124,7 +9276,7 @@ function compareSemver(a, b) {
9124
9276
  }
9125
9277
  function fetchLatest() {
9126
9278
  return new Promise((resolve2) => {
9127
- const req = https4.get(
9279
+ const req = https5.get(
9128
9280
  REGISTRY_URL,
9129
9281
  { headers: { Accept: "application/json" }, timeout: REQUEST_TIMEOUT_MS },
9130
9282
  (res) => {
@@ -9176,7 +9328,7 @@ function checkForUpdates() {
9176
9328
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
9177
9329
  if (process.env.CI) return;
9178
9330
  if (!process.stdout.isTTY) return;
9179
- const current = true ? "2.4.38" : null;
9331
+ const current = true ? "2.5.0" : null;
9180
9332
  if (!current) return;
9181
9333
  const cache = readCache();
9182
9334
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;