codeam-cli 2.4.39 → 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 +1159 -1067
  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.39",
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);
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(() => {
5433
+ });
5434
+ }
5435
+ const selector = detectAnySelector(lines);
5226
5436
  this.stopPoll();
5227
- this.active = false;
5437
+ this.pty.deactivate();
5228
5438
  if (selector) {
5229
- this.postChunk({ type: "select_prompt", content: selector.question, options: selector.options, optionDescriptions: selector.optionDescriptions, currentIndex: selector.currentIndex, done: true }).catch(() => {
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
5447
+ },
5448
+ { critical: true }
5449
+ ).catch(() => {
5230
5450
  });
5231
5451
  } else {
5232
- const content = filterChrome(lines).join("\n").replace(/\n{3,}/g, "\n\n").trim();
5233
- this.postChunk({ type: "text", content, done: true }).catch(() => {
5452
+ const content = extractContent(lines);
5453
+ this.send(
5454
+ { type: "text", content, done: true },
5455
+ { critical: true }
5456
+ ).catch(() => {
5234
5457
  });
5235
5458
  this.onTurnComplete?.();
5236
5459
  }
5237
5460
  }
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;
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;
5255
5476
  }
5256
5477
  }
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
5478
  }
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}`);
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());
5286
5485
  }
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
5486
  }
5321
5487
  };
5322
- var _transport2 = {
5323
- sendOutputChunk: _sendOutputChunk
5324
- };
5325
- function _sendOutputChunk(url, headers, payload) {
5326
- return new Promise((resolve2, reject) => {
5327
- let settled = false;
5328
- const u2 = new URL(url);
5329
- const transport = u2.protocol === "https:" ? https2 : http2;
5330
- const req = transport.request(
5331
- {
5332
- hostname: u2.hostname,
5333
- port: u2.port || (u2.protocol === "https:" ? 443 : 80),
5334
- path: u2.pathname,
5335
- method: "POST",
5336
- headers: {
5337
- ...headers,
5338
- "Content-Length": Buffer.byteLength(payload)
5339
- },
5340
- timeout: 8e3
5341
- },
5342
- (res) => {
5343
- let resData = "";
5344
- res.on("data", (c2) => {
5345
- resData += c2.toString();
5346
- });
5347
- res.on("end", () => {
5348
- if (settled) return;
5349
- settled = true;
5350
- resolve2({ statusCode: res.statusCode ?? 0, body: resData });
5351
- });
5352
- }
5353
- );
5354
- req.on("error", (err) => {
5355
- if (settled) return;
5356
- settled = true;
5357
- reject(err);
5358
- });
5359
- req.on("timeout", () => {
5360
- req.destroy();
5361
- });
5362
- req.write(payload);
5363
- req.end();
5364
- });
5365
- }
5366
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,
@@ -5834,6 +5956,161 @@ var HistoryService = class {
5834
5956
  }
5835
5957
  };
5836
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
+
5837
6114
  // src/lib/payload.ts
5838
6115
  var import_zod2 = require("zod");
5839
6116
  var fileEntrySchema = import_zod2.z.object({
@@ -5869,8 +6146,8 @@ function parsePayload(schema, raw) {
5869
6146
  }
5870
6147
 
5871
6148
  // src/services/file-ops.service.ts
5872
- var fs6 = __toESM(require("fs/promises"));
5873
- var path9 = __toESM(require("path"));
6149
+ var fs7 = __toESM(require("fs/promises"));
6150
+ var path10 = __toESM(require("path"));
5874
6151
  var MAX_FILE_BYTES = 5 * 1024 * 1024;
5875
6152
  var MAX_WALK_DEPTH = 6;
5876
6153
  var MAX_VISITED_DIRS = 5e3;
@@ -5905,12 +6182,12 @@ var SUBDIR_IGNORE = /* @__PURE__ */ new Set([
5905
6182
  "__pycache__"
5906
6183
  ]);
5907
6184
  function isUnder(parent, candidate) {
5908
- const rel = path9.relative(parent, candidate);
5909
- return rel === "" || !rel.startsWith("..") && !path9.isAbsolute(rel);
6185
+ const rel = path10.relative(parent, candidate);
6186
+ return rel === "" || !rel.startsWith("..") && !path10.isAbsolute(rel);
5910
6187
  }
5911
6188
  async function isExistingFile(absPath) {
5912
6189
  try {
5913
- const stat3 = await fs6.stat(absPath);
6190
+ const stat3 = await fs7.stat(absPath);
5914
6191
  return stat3.isFile();
5915
6192
  } catch {
5916
6193
  return false;
@@ -5923,13 +6200,13 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
5923
6200
  ctx.visited++;
5924
6201
  let entries = [];
5925
6202
  try {
5926
- entries = await fs6.readdir(dir, { withFileTypes: true });
6203
+ entries = await fs7.readdir(dir, { withFileTypes: true });
5927
6204
  } catch {
5928
6205
  return;
5929
6206
  }
5930
6207
  for (const e of entries) {
5931
6208
  if (!e.isFile()) continue;
5932
- const full = path9.join(dir, e.name);
6209
+ const full = path10.join(dir, e.name);
5933
6210
  if (needleVariants.some((needle) => full.endsWith(needle))) {
5934
6211
  ctx.matches.push(full);
5935
6212
  if (ctx.matches.length >= ctx.cap) return;
@@ -5939,21 +6216,21 @@ async function walkForSuffix(dir, needleVariants, depth, ctx) {
5939
6216
  if (!e.isDirectory()) continue;
5940
6217
  if (SUBDIR_IGNORE.has(e.name)) continue;
5941
6218
  if (e.name.startsWith(".") && SUBDIR_IGNORE.has(e.name)) continue;
5942
- await walkForSuffix(path9.join(dir, e.name), needleVariants, depth + 1, ctx);
6219
+ await walkForSuffix(path10.join(dir, e.name), needleVariants, depth + 1, ctx);
5943
6220
  if (ctx.matches.length >= ctx.cap) return;
5944
6221
  }
5945
6222
  }
5946
6223
  async function findFile(rawPath) {
5947
6224
  const cwd = process.cwd();
5948
- if (path9.isAbsolute(rawPath)) {
5949
- const abs = path9.normalize(rawPath);
6225
+ if (path10.isAbsolute(rawPath)) {
6226
+ const abs = path10.normalize(rawPath);
5950
6227
  if (isUnder(cwd, abs) && await isExistingFile(abs)) return abs;
5951
6228
  }
5952
- const direct = path9.resolve(cwd, rawPath);
6229
+ const direct = path10.resolve(cwd, rawPath);
5953
6230
  if (isUnder(cwd, direct) && await isExistingFile(direct)) return direct;
5954
- const normalized = path9.normalize(rawPath).replace(/^[./\\]+/, "");
6231
+ const normalized = path10.normalize(rawPath).replace(/^[./\\]+/, "");
5955
6232
  const needles = [
5956
- `${path9.sep}${normalized}`,
6233
+ `${path10.sep}${normalized}`,
5957
6234
  `/${normalized}`
5958
6235
  ].filter((v, i, a) => a.indexOf(v) === i);
5959
6236
  const ctx = { visited: 0, matches: [], cap: 16 };
@@ -5967,7 +6244,7 @@ async function findWriteTarget(rawPath) {
5967
6244
  const found = await findFile(rawPath);
5968
6245
  if (found) return found;
5969
6246
  const cwd = process.cwd();
5970
- 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);
5971
6248
  if (!isUnder(cwd, fallback)) return null;
5972
6249
  return fallback;
5973
6250
  }
@@ -5984,11 +6261,11 @@ async function readProjectFile(rawPath) {
5984
6261
  if (!abs) {
5985
6262
  return { error: `File not found in the project tree: ${rawPath}` };
5986
6263
  }
5987
- const stat3 = await fs6.stat(abs);
6264
+ const stat3 = await fs7.stat(abs);
5988
6265
  if (stat3.size > MAX_FILE_BYTES) {
5989
6266
  return { error: `File too large (${(stat3.size / 1024 / 1024).toFixed(1)} MB > ${MAX_FILE_BYTES / 1024 / 1024} MB).` };
5990
6267
  }
5991
- const buf = await fs6.readFile(abs);
6268
+ const buf = await fs7.readFile(abs);
5992
6269
  if (looksBinary(buf)) {
5993
6270
  return { error: "Binary file \u2014 refusing to open in a code editor." };
5994
6271
  }
@@ -6007,8 +6284,8 @@ async function writeProjectFile(rawPath, content) {
6007
6284
  if (Buffer.byteLength(content, "utf-8") > MAX_FILE_BYTES) {
6008
6285
  return { error: "Content too large." };
6009
6286
  }
6010
- await fs6.mkdir(path9.dirname(abs), { recursive: true });
6011
- await fs6.writeFile(abs, content, "utf-8");
6287
+ await fs7.mkdir(path10.dirname(abs), { recursive: true });
6288
+ await fs7.writeFile(abs, content, "utf-8");
6012
6289
  return { ok: true };
6013
6290
  } catch (e) {
6014
6291
  const msg = e instanceof Error ? e.message : "Write failed";
@@ -6017,11 +6294,11 @@ async function writeProjectFile(rawPath, content) {
6017
6294
  }
6018
6295
 
6019
6296
  // src/services/project-ops.service.ts
6020
- var import_child_process4 = require("child_process");
6297
+ var import_child_process6 = require("child_process");
6021
6298
  var import_util = require("util");
6022
- var fs7 = __toESM(require("fs/promises"));
6023
- var path10 = __toESM(require("path"));
6024
- var execFileP = (0, import_util.promisify)(import_child_process4.execFile);
6299
+ var fs8 = __toESM(require("fs/promises"));
6300
+ var path11 = __toESM(require("path"));
6301
+ var execFileP = (0, import_util.promisify)(import_child_process6.execFile);
6025
6302
  var PROJECT_IGNORE = /* @__PURE__ */ new Set([
6026
6303
  "node_modules",
6027
6304
  ".git",
@@ -6068,7 +6345,7 @@ async function listProjectFiles(opts = {}) {
6068
6345
  }
6069
6346
  let entries = [];
6070
6347
  try {
6071
- entries = await fs7.readdir(dir, { withFileTypes: true });
6348
+ entries = await fs8.readdir(dir, { withFileTypes: true });
6072
6349
  } catch {
6073
6350
  return;
6074
6351
  }
@@ -6078,18 +6355,18 @@ async function listProjectFiles(opts = {}) {
6078
6355
  return;
6079
6356
  }
6080
6357
  if (PROJECT_IGNORE.has(e.name)) continue;
6081
- const full = path10.join(dir, e.name);
6358
+ const full = path11.join(dir, e.name);
6082
6359
  if (e.isDirectory()) {
6083
6360
  if (depth >= 12) continue;
6084
6361
  await walk(full, depth + 1);
6085
6362
  } else if (e.isFile()) {
6086
- const rel = path10.relative(root, full);
6363
+ const rel = path11.relative(root, full);
6087
6364
  if (q2 && !rel.toLowerCase().includes(q2) && !e.name.toLowerCase().includes(q2)) {
6088
6365
  continue;
6089
6366
  }
6090
6367
  let size = 0;
6091
6368
  try {
6092
- const st3 = await fs7.stat(full);
6369
+ const st3 = await fs8.stat(full);
6093
6370
  size = st3.size;
6094
6371
  } catch {
6095
6372
  }
@@ -6191,8 +6468,8 @@ async function gitStatus(cwd) {
6191
6468
  let hasMergeInProgress = false;
6192
6469
  try {
6193
6470
  const gitDir = (await git(["rev-parse", "--git-dir"], root)).stdout.trim();
6194
- const mergeHead = path10.isAbsolute(gitDir) ? path10.join(gitDir, "MERGE_HEAD") : path10.join(root, gitDir, "MERGE_HEAD");
6195
- await fs7.access(mergeHead);
6471
+ const mergeHead = path11.isAbsolute(gitDir) ? path11.join(gitDir, "MERGE_HEAD") : path11.join(root, gitDir, "MERGE_HEAD");
6472
+ await fs8.access(mergeHead);
6196
6473
  hasMergeInProgress = true;
6197
6474
  } catch {
6198
6475
  }
@@ -6265,15 +6542,261 @@ async function gitResolve(file, side, cwd) {
6265
6542
  return { ok: true };
6266
6543
  }
6267
6544
 
6268
- // src/commands/start.ts
6545
+ // src/commands/start/handlers.ts
6269
6546
  function saveFilesTemp(files) {
6270
6547
  return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
6271
6548
  const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
6272
- const tmpPath = path11.join(os7.tmpdir(), `codeam-${(0, import_crypto.randomUUID)()}-${safeName}`);
6273
- fs8.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
6549
+ const tmpPath = path12.join(os8.tmpdir(), `codeam-${(0, import_crypto.randomUUID)()}-${safeName}`);
6550
+ fs9.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
6274
6551
  return tmpPath;
6275
6552
  });
6276
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
+
6799
+ // src/commands/start.ts
6277
6800
  async function start() {
6278
6801
  showIntro();
6279
6802
  const session = getActiveSession();
@@ -6287,434 +6810,32 @@ async function start() {
6287
6810
  showInfo(`${session.userName} \xB7 ${import_picocolors2.default.cyan(session.plan)}`);
6288
6811
  showInfo("Launching Claude Code...\n");
6289
6812
  const cwd = process.cwd();
6290
- const ws = new WebSocketService(session.id, pluginId);
6291
6813
  const historySvc = new HistoryService(pluginId, cwd);
6292
- let quotaFetchInProgress = false;
6293
- function fetchQuotaUsage() {
6294
- if (quotaFetchInProgress) return;
6295
- quotaFetchInProgress = true;
6296
- const claudeCmd = findInPath("claude") ? "claude" : "claude-code";
6297
- if (!claudeCmd) {
6298
- quotaFetchInProgress = false;
6299
- return;
6300
- }
6301
- const helperScript = `import os,pty,sys,select,signal,struct,fcntl,termios,errno
6302
- m,s=pty.openpty()
6303
- try:
6304
- fcntl.ioctl(s,termios.TIOCSWINSZ,struct.pack('HHHH',30,120,0,0))
6305
- except Exception:pass
6306
- pid=os.fork()
6307
- if pid==0:
6308
- os.close(m);os.setsid()
6309
- try:fcntl.ioctl(s,termios.TIOCSCTTY,0)
6310
- except Exception:pass
6311
- for fd in[0,1,2]:os.dup2(s,fd)
6312
- if s>2:os.close(s)
6313
- os.execvp(sys.argv[1],sys.argv[1:])
6314
- sys.exit(127)
6315
- os.close(s)
6316
- done=[False]
6317
- def onchld(n,f):
6318
- try:os.waitpid(pid,os.WNOHANG)
6319
- except Exception:pass
6320
- done[0]=True
6321
- signal.signal(signal.SIGCHLD,onchld)
6322
- i=sys.stdin.fileno();o=sys.stdout.fileno()
6323
- while not done[0]:
6324
- try:r,_,_=select.select([i,m],[],[],0.1)
6325
- except OSError as e:
6326
- if e.errno==errno.EINTR:continue
6327
- break
6328
- if i in r:
6329
- try:
6330
- d=os.read(i,4096)
6331
- if d:os.write(m,d)
6332
- else:break
6333
- except OSError:break
6334
- if m in r:
6335
- try:
6336
- d=os.read(m,4096)
6337
- if d:os.write(o,d)
6338
- except OSError:done[0]=True
6339
- try:os.kill(pid,signal.SIGTERM)
6340
- except Exception:pass
6341
- try:
6342
- _,st=os.waitpid(pid,0)
6343
- sys.exit((st>>8)&0xFF)
6344
- except Exception:sys.exit(0)
6345
- `;
6346
- const helperPath = path11.join(os7.tmpdir(), "codeam-quota-helper.py");
6347
- fs8.writeFileSync(helperPath, helperScript, { mode: 420 });
6348
- const python = findInPath("python3") ?? findInPath("python");
6349
- if (!python) {
6350
- quotaFetchInProgress = false;
6351
- return;
6352
- }
6353
- const proc = (0, import_child_process5.spawn)(python, [helperPath, claudeCmd, "--tools", ""], {
6354
- stdio: ["pipe", "pipe", "ignore"],
6355
- cwd: process.cwd(),
6356
- env: { ...process.env, TERM: "dumb", COLUMNS: "120", LINES: "30" }
6357
- });
6358
- let output = "";
6359
- proc.stdout?.on("data", (chunk) => {
6360
- output += chunk.toString("utf8");
6361
- });
6362
- setTimeout(() => {
6363
- proc.stdin?.write("/usage\r");
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);
6364
6826
  setTimeout(() => {
6365
- const clean = output.replace(/\x1B\[[^@-~]*[@-~]/g, "").replace(/[\x00-\x1F\x7F]/g, " ").replace(/\s+/g, " ");
6366
- const weekMatch = clean.match(/(\d+)%\s*used/i) || clean.match(/(\d+)\s*%/);
6367
- if (weekMatch) {
6368
- historySvc.setQuotaPercent(parseInt(weekMatch[1], 10));
6369
- }
6370
- const resetMatch = clean.match(/resets\s+(.+?)(?:\s*\(|$)/im);
6371
- if (resetMatch) {
6372
- historySvc.setRateLimitReset(resetMatch[1].trim());
6373
- }
6374
- try {
6375
- proc.kill();
6376
- } catch {
6377
- }
6378
- try {
6379
- fs8.unlinkSync(helperPath);
6380
- } catch {
6381
- }
6382
- quotaFetchInProgress = false;
6383
- }, 5e3);
6384
- }, 8e3);
6385
- proc.on("exit", () => {
6386
- quotaFetchInProgress = false;
6387
- });
6388
- setTimeout(() => {
6389
- try {
6390
- proc.kill();
6391
- } catch {
6392
- }
6393
- }, 2e4);
6394
- }
6395
- const outputSvc = new OutputService(session.id, pluginId, (conversationId) => {
6396
- historySvc.setCurrentConversationId(conversationId);
6397
- }, (reset) => {
6398
- historySvc.setRateLimitReset(reset);
6399
- }, () => {
6400
- if (historySvc.isQuotaStale()) {
6401
- fetchQuotaUsage();
6402
- }
6403
- setTimeout(() => {
6404
- historySvc.uploadDelta().catch(() => {
6405
- });
6406
- }, 400);
6407
- }, () => {
6408
- const prevCount = historySvc.getCurrentMessageCount();
6409
- historySvc.waitForNewUserMessage(prevCount).then((userText) => outputSvc.startTerminalTurn(userText ?? void 0)).catch(() => outputSvc.startTerminalTurn(void 0));
6410
- }, session.pluginAuthToken);
6411
- function sendPrompt(prompt) {
6412
- outputSvc.newTurn();
6413
- claude.sendCommand(prompt);
6414
- }
6415
- const relay = new CommandRelayService(pluginId, async (cmd) => {
6416
- const parsed = parsePayload(startCommandSchema, cmd.payload);
6417
- if (!parsed) {
6418
- showInfo(`Ignoring malformed ${cmd.type} payload.`);
6419
- return;
6420
- }
6421
- switch (cmd.type) {
6422
- case "start_task": {
6423
- const { prompt, files } = parsed;
6424
- const effectivePrompt = prompt ?? "";
6425
- if (files && files.length > 0) {
6426
- const paths = saveFilesTemp(files);
6427
- const atRefs = paths.map((p2) => `@${p2}`).join(" ");
6428
- outputSvc.newTurn();
6429
- claude.sendCommand(`${atRefs} ${effectivePrompt}`.trim());
6430
- setTimeout(() => {
6431
- for (const p2 of paths) {
6432
- try {
6433
- fs8.unlinkSync(p2);
6434
- } catch {
6435
- }
6436
- }
6437
- }, 12e4);
6438
- } else if (effectivePrompt) {
6439
- sendPrompt(effectivePrompt);
6440
- }
6441
- break;
6442
- }
6443
- case "provide_input": {
6444
- const { input } = parsed;
6445
- if (input) sendPrompt(input);
6446
- break;
6447
- }
6448
- case "select_option": {
6449
- const index = parsed.index ?? 0;
6450
- const from = parsed.from ?? 0;
6451
- outputSvc.newTurn();
6452
- claude.selectOption(index, from);
6453
- break;
6454
- }
6455
- case "escape_key":
6456
- outputSvc.newTurn();
6457
- claude.sendEscape();
6458
- break;
6459
- case "stop_task":
6460
- claude.interrupt();
6461
- break;
6462
- case "set_keep_alive": {
6463
- const enabled = !!cmd.payload.enabled;
6464
- const inCodespaceEnv = process.env.CODESPACES === "true";
6465
- setKeepAlive(enabled);
6466
- try {
6467
- await relay.sendResult(
6468
- cmd.id,
6469
- "success",
6470
- { enabled, applied: enabled && inCodespaceEnv, runtime: inCodespaceEnv ? "github-codespaces" : "local" }
6471
- );
6472
- } catch {
6473
- }
6474
- break;
6475
- }
6476
- case "session_terminated": {
6477
- showInfo("Session was deleted from the app \u2014 exiting.");
6478
- try {
6479
- claude.kill();
6480
- } catch {
6481
- }
6482
- try {
6483
- const proc = (0, import_child_process5.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
6484
- detached: true,
6485
- stdio: "ignore"
6486
- });
6487
- proc.unref();
6488
- } catch {
6489
- }
6490
- outputSvc.dispose();
6491
- relay.stop();
6492
- ws.disconnect();
6493
- process.exit(0);
6494
- }
6495
- case "shutdown_session": {
6496
- try {
6497
- await relay.sendResult(cmd.id, "success", { ok: true });
6498
- } catch {
6499
- }
6500
- try {
6501
- claude.kill();
6502
- } catch {
6503
- }
6504
- const codespaceName2 = process.env.CODESPACE_NAME;
6505
- if (codespaceName2 && process.env.CODESPACES === "true") {
6506
- try {
6507
- const stopProc = (0, import_child_process5.spawn)(
6508
- "bash",
6509
- ["-lc", `sleep 1; gh codespace stop -c ${JSON.stringify(codespaceName2)} >/dev/null 2>&1 || true`],
6510
- { detached: true, stdio: "ignore" }
6511
- );
6512
- stopProc.unref();
6513
- } catch {
6514
- }
6515
- }
6516
- try {
6517
- const proc = (0, import_child_process5.spawn)("bash", ["-lc", "pm2 delete codeam-pair >/dev/null 2>&1 || true"], {
6518
- detached: true,
6519
- stdio: "ignore"
6520
- });
6521
- proc.unref();
6522
- } catch {
6523
- }
6524
- outputSvc.dispose();
6525
- relay.stop();
6526
- ws.disconnect();
6527
- process.exit(0);
6528
- }
6529
- case "get_context": {
6530
- const usage = historySvc.getCurrentUsage();
6531
- const monthlyCost = historySvc.getMonthlyEstimatedCost();
6532
- const rateLimitReset = historySvc.getRateLimitReset();
6533
- const quotaPercent = historySvc.getQuotaPercent();
6534
- const base = usage ? { ...usage, monthlyCost } : { used: 0, total: 2e5, percent: 0, model: null, outputTokens: 0, cacheReadTokens: 0, monthlyCost, error: "No usage data found" };
6535
- const result = { ...base, ...rateLimitReset ? { rateLimitReset } : {}, ...quotaPercent !== null ? { quotaPercent } : {} };
6536
- await relay.sendResult(cmd.id, "completed", result);
6537
- break;
6538
- }
6539
- case "resume_session": {
6540
- const { id, auto } = parsed;
6541
- if (!id) break;
6542
- historySvc.setCurrentConversationId(id);
6543
- await historySvc.loadConversation(id);
6544
- await outputSvc.newTurnResume(id);
6545
- claude.restart(id, auto ?? false);
6546
- break;
6547
- }
6548
- case "get_conversation": {
6549
- const currentId = historySvc.getCurrentConversationId();
6550
- if (currentId) {
6551
- try {
6552
- await historySvc.loadConversation(currentId);
6553
- await relay.sendResult(cmd.id, "completed", { conversationId: currentId });
6554
- } catch {
6555
- await relay.sendResult(cmd.id, "failed", {});
6556
- }
6557
- } else {
6558
- await relay.sendResult(cmd.id, "completed", { conversationId: null });
6559
- }
6560
- break;
6561
- }
6562
- case "list_models": {
6563
- const models = [
6564
- { id: "claude-opus-4-7", label: "Claude Opus 4.7", description: "Most capable", family: "claude", vendor: "anthropic", isDefault: false },
6565
- { id: "claude-opus-4-6", label: "Claude Opus 4.6", description: "Top tier", family: "claude", vendor: "anthropic", isDefault: false },
6566
- { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", description: "Balanced", family: "claude", vendor: "anthropic", isDefault: true },
6567
- { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5", description: "Fastest", family: "claude", vendor: "anthropic", isDefault: false }
6568
- ];
6569
- await relay.sendResult(cmd.id, "completed", { models });
6570
- break;
6571
- }
6572
- case "read_file": {
6573
- const { path: filePath } = parsed;
6574
- if (!filePath) {
6575
- await relay.sendResult(cmd.id, "failed", { error: "Missing path" });
6576
- break;
6577
- }
6578
- const result = await readProjectFile(filePath);
6579
- await relay.sendResult(cmd.id, "completed", result);
6580
- break;
6581
- }
6582
- case "write_file": {
6583
- const { path: filePath, content } = parsed;
6584
- if (!filePath || typeof content !== "string") {
6585
- await relay.sendResult(cmd.id, "failed", { error: "Missing path or content" });
6586
- break;
6587
- }
6588
- const result = await writeProjectFile(filePath, content);
6589
- await relay.sendResult(cmd.id, "completed", result);
6590
- break;
6591
- }
6592
- case "list_files": {
6593
- const result = await listProjectFiles({ query: parsed.query });
6594
- await relay.sendResult(cmd.id, "completed", result);
6595
- break;
6596
- }
6597
- case "git_status": {
6598
- const result = await gitStatus();
6599
- await relay.sendResult(cmd.id, "completed", result);
6600
- break;
6601
- }
6602
- case "git_diff": {
6603
- const { path: filePath } = parsed;
6604
- const result = await gitDiff(filePath ?? null);
6605
- await relay.sendResult(cmd.id, "completed", result);
6606
- break;
6607
- }
6608
- case "git_diff_staged": {
6609
- const { path: filePath } = parsed;
6610
- const result = await gitDiffStaged(filePath ?? null);
6611
- await relay.sendResult(cmd.id, "completed", result);
6612
- break;
6613
- }
6614
- case "git_log": {
6615
- const result = await gitLog(parsed.limit ?? 30);
6616
- await relay.sendResult(cmd.id, "completed", result);
6617
- break;
6618
- }
6619
- case "git_commit": {
6620
- if (!parsed.message) {
6621
- await relay.sendResult(cmd.id, "failed", { error: "Missing message" });
6622
- break;
6623
- }
6624
- const result = await gitCommit(parsed.message, parsed.paths);
6625
- await relay.sendResult(cmd.id, "completed", result);
6626
- break;
6627
- }
6628
- case "git_push": {
6629
- const result = await gitPush();
6630
- await relay.sendResult(cmd.id, "completed", result);
6631
- break;
6632
- }
6633
- case "git_pull": {
6634
- const result = await gitPull();
6635
- await relay.sendResult(cmd.id, "completed", result);
6636
- break;
6637
- }
6638
- case "git_resolve": {
6639
- const { path: filePath, side } = parsed;
6640
- if (!filePath || !side) {
6641
- await relay.sendResult(cmd.id, "failed", { error: "Missing path or side" });
6642
- break;
6643
- }
6644
- const result = await gitResolve(filePath, side);
6645
- await relay.sendResult(cmd.id, "completed", result);
6646
- break;
6647
- }
6648
- }
6649
- });
6650
- ws.addHandler({
6651
- onConnected() {
6827
+ historySvc.uploadDelta().catch(() => {
6828
+ });
6829
+ }, 400);
6652
6830
  },
6653
- onDisconnected() {
6831
+ () => {
6832
+ const prevCount = historySvc.getCurrentMessageCount();
6833
+ historySvc.waitForNewUserMessage(prevCount).then((userText) => outputSvc.startTerminalTurn(userText ?? void 0)).catch(() => outputSvc.startTerminalTurn(void 0));
6654
6834
  },
6655
- onMessage(type, payload) {
6656
- if (type !== "agent_command") return;
6657
- const cmdType = typeof payload.type === "string" ? payload.type : null;
6658
- if (!cmdType) return;
6659
- const parsed = parsePayload(startCommandSchema, payload.payload ?? {});
6660
- if (!parsed) {
6661
- showInfo(`Ignoring malformed ${cmdType} payload (ws).`);
6662
- return;
6663
- }
6664
- if (cmdType === "start_task") {
6665
- const { prompt, files } = parsed;
6666
- const effectivePrompt = prompt ?? "";
6667
- if (files && files.length > 0) {
6668
- const paths = saveFilesTemp(files);
6669
- const atRefs = paths.map((p2) => `@${p2}`).join(" ");
6670
- outputSvc.newTurn();
6671
- claude.sendCommand(`${atRefs} ${effectivePrompt}`.trim());
6672
- setTimeout(() => {
6673
- for (const p2 of paths) {
6674
- try {
6675
- fs8.unlinkSync(p2);
6676
- } catch {
6677
- }
6678
- }
6679
- }, 12e4);
6680
- } else if (effectivePrompt) {
6681
- sendPrompt(effectivePrompt);
6682
- }
6683
- } else if (cmdType === "provide_input") {
6684
- const { input } = parsed;
6685
- if (input) sendPrompt(input);
6686
- } else if (cmdType === "select_option") {
6687
- const index = parsed.index ?? 0;
6688
- const from = parsed.from ?? 0;
6689
- outputSvc.newTurn();
6690
- claude.selectOption(index, from);
6691
- } else if (cmdType === "escape_key") {
6692
- outputSvc.newTurn();
6693
- claude.sendEscape();
6694
- } else if (cmdType === "stop_task") {
6695
- claude.interrupt();
6696
- } else if (cmdType === "get_conversation") {
6697
- const currentId = historySvc.getCurrentConversationId();
6698
- if (currentId) {
6699
- historySvc.loadConversation(currentId).catch(() => {
6700
- });
6701
- }
6702
- } else if (cmdType === "resume_session") {
6703
- const { id, auto } = parsed;
6704
- if (id) {
6705
- const autoFlag = auto ?? false;
6706
- historySvc.loadConversation(id).then(() => outputSvc.newTurnResume(id)).then(() => {
6707
- claude.restart(id, autoFlag);
6708
- }).catch(() => {
6709
- });
6710
- }
6711
- }
6712
- }
6713
- });
6714
- ws.connect();
6715
- relay.start();
6835
+ session.pluginAuthToken
6836
+ );
6716
6837
  const claude = new ClaudeService({
6717
- cwd: process.cwd(),
6838
+ cwd,
6718
6839
  onData(raw) {
6719
6840
  outputSvc.push(raw);
6720
6841
  },
@@ -6722,18 +6843,29 @@ except Exception:sys.exit(0)
6722
6843
  process.removeListener("SIGINT", sigintHandler);
6723
6844
  outputSvc.dispose();
6724
6845
  relay.stop();
6725
- ws.disconnect();
6726
6846
  process.exit(code);
6727
6847
  }
6728
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;
6729
6861
  function sigintHandler() {
6730
6862
  claude.kill();
6731
6863
  outputSvc.dispose();
6732
6864
  relay.stop();
6733
- ws.disconnect();
6734
6865
  process.exit(0);
6735
6866
  }
6736
6867
  process.once("SIGINT", sigintHandler);
6868
+ relay.start();
6737
6869
  await claude.spawn();
6738
6870
  setTimeout(() => {
6739
6871
  historySvc.detectCurrentConversation();
@@ -6745,47 +6877,7 @@ except Exception:sys.exit(0)
6745
6877
  });
6746
6878
  }
6747
6879
  }, 2e3);
6748
- setTimeout(() => {
6749
- fetchQuotaUsage();
6750
- }, 5e3);
6751
- const inCodespace = process.env.CODESPACES === "true";
6752
- const codespaceName = process.env.CODESPACE_NAME;
6753
- let keepAliveTimer = null;
6754
- async function setIdleTimeout(minutes) {
6755
- if (!inCodespace || !codespaceName) return;
6756
- await new Promise((resolve2) => {
6757
- const proc = (0, import_child_process5.spawn)(
6758
- "gh",
6759
- [
6760
- "api",
6761
- "-X",
6762
- "PATCH",
6763
- `/user/codespaces/${codespaceName}`,
6764
- "-F",
6765
- `idle_timeout_minutes=${minutes}`
6766
- ],
6767
- { stdio: "ignore", detached: true }
6768
- );
6769
- proc.unref();
6770
- proc.on("exit", () => resolve2());
6771
- proc.on("error", () => resolve2());
6772
- });
6773
- }
6774
- function setKeepAlive(enabled) {
6775
- if (keepAliveTimer) {
6776
- clearInterval(keepAliveTimer);
6777
- keepAliveTimer = null;
6778
- }
6779
- if (!inCodespace || !codespaceName) return;
6780
- if (!enabled) {
6781
- void setIdleTimeout(30);
6782
- return;
6783
- }
6784
- void setIdleTimeout(240);
6785
- keepAliveTimer = setInterval(() => {
6786
- void setIdleTimeout(240);
6787
- }, 30 * 60 * 1e3);
6788
- }
6880
+ setTimeout(() => fetchQuotaUsage(historySvc), 5e3);
6789
6881
  }
6790
6882
 
6791
6883
  // src/commands/pair.ts
@@ -6966,19 +7058,19 @@ async function logout() {
6966
7058
  }
6967
7059
 
6968
7060
  // src/commands/deploy.ts
6969
- var import_child_process10 = require("child_process");
6970
- var fs9 = __toESM(require("fs"));
6971
- var os8 = __toESM(require("os"));
6972
- var path16 = __toESM(require("path"));
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"));
6973
7065
  var import_util6 = require("util");
6974
7066
  var import_picocolors9 = __toESM(require("picocolors"));
6975
7067
 
6976
7068
  // src/services/providers/github-codespaces.ts
6977
- var import_child_process6 = require("child_process");
7069
+ var import_child_process8 = require("child_process");
6978
7070
  var import_util2 = require("util");
6979
7071
  var import_picocolors7 = __toESM(require("picocolors"));
6980
- var path12 = __toESM(require("path"));
6981
- 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);
6982
7074
  var MAX_BUFFER = 8 * 1024 * 1024;
6983
7075
  function resetStdinForChild() {
6984
7076
  if (process.stdin.isTTY) {
@@ -7022,7 +7114,7 @@ var GitHubCodespacesProvider = class {
7022
7114
  if (!isAuthed) {
7023
7115
  resetStdinForChild();
7024
7116
  await new Promise((resolve2, reject) => {
7025
- 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"], {
7026
7118
  stdio: "inherit"
7027
7119
  });
7028
7120
  proc.on("exit", (code) => {
@@ -7056,7 +7148,7 @@ var GitHubCodespacesProvider = class {
7056
7148
  wt(noteLines.join("\n"), "One more permission needed");
7057
7149
  resetStdinForChild();
7058
7150
  const refreshCode = await new Promise((resolve2, reject) => {
7059
- const proc = (0, import_child_process6.spawn)(
7151
+ const proc = (0, import_child_process8.spawn)(
7060
7152
  "gh",
7061
7153
  ["auth", "refresh", "-h", "github.com", "-s", "codespace"],
7062
7154
  { stdio: "inherit" }
@@ -7206,7 +7298,7 @@ var GitHubCodespacesProvider = class {
7206
7298
  O2.step(`Installing gh via ${installCmd.describe}\u2026`);
7207
7299
  resetStdinForChild();
7208
7300
  const ok = await new Promise((resolve2) => {
7209
- 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" });
7210
7302
  proc.on("exit", (code) => resolve2(code === 0));
7211
7303
  proc.on("error", () => resolve2(false));
7212
7304
  });
@@ -7233,7 +7325,7 @@ var GitHubCodespacesProvider = class {
7233
7325
  );
7234
7326
  resetStdinForChild();
7235
7327
  await new Promise((resolve2, reject) => {
7236
- const proc = (0, import_child_process6.spawn)(
7328
+ const proc = (0, import_child_process8.spawn)(
7237
7329
  "gh",
7238
7330
  ["auth", "refresh", "-h", "github.com", "-s", "repo,read:org"],
7239
7331
  { stdio: "inherit" }
@@ -7411,7 +7503,7 @@ var GitHubCodespacesProvider = class {
7411
7503
  async streamCommand(workspaceId, command2) {
7412
7504
  resetStdinForChild();
7413
7505
  return new Promise((resolve2, reject) => {
7414
- const proc = (0, import_child_process6.spawn)(
7506
+ const proc = (0, import_child_process8.spawn)(
7415
7507
  "gh",
7416
7508
  ["codespace", "ssh", "-c", workspaceId, "--", "-tt", command2],
7417
7509
  { stdio: "inherit" }
@@ -7438,11 +7530,11 @@ var GitHubCodespacesProvider = class {
7438
7530
  `mkdir -p ${shellQuote(remoteDir)} && tar -xzf - -C ${shellQuote(remoteDir)}`
7439
7531
  ];
7440
7532
  await new Promise((resolve2, reject) => {
7441
- const tar = (0, import_child_process6.spawn)("tar", tarArgs, {
7533
+ const tar = (0, import_child_process8.spawn)("tar", tarArgs, {
7442
7534
  stdio: ["ignore", "pipe", "pipe"],
7443
7535
  env: tarEnv
7444
7536
  });
7445
- const ssh = (0, import_child_process6.spawn)("gh", sshArgs, {
7537
+ const ssh = (0, import_child_process8.spawn)("gh", sshArgs, {
7446
7538
  stdio: [tar.stdout, "pipe", "pipe"]
7447
7539
  });
7448
7540
  let tarErr = "";
@@ -7466,7 +7558,7 @@ var GitHubCodespacesProvider = class {
7466
7558
  });
7467
7559
  }
7468
7560
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
7469
- const remoteDir = path12.posix.dirname(remotePath);
7561
+ const remoteDir = path13.posix.dirname(remotePath);
7470
7562
  const parts = [
7471
7563
  `mkdir -p ${shellQuote(remoteDir)}`,
7472
7564
  `cat > ${shellQuote(remotePath)}`
@@ -7476,7 +7568,7 @@ var GitHubCodespacesProvider = class {
7476
7568
  }
7477
7569
  const cmd = parts.join(" && ");
7478
7570
  await new Promise((resolve2, reject) => {
7479
- const proc = (0, import_child_process6.spawn)(
7571
+ const proc = (0, import_child_process8.spawn)(
7480
7572
  "gh",
7481
7573
  ["codespace", "ssh", "-c", workspaceId, "--", cmd],
7482
7574
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -7534,11 +7626,11 @@ function shellQuote(s) {
7534
7626
  }
7535
7627
 
7536
7628
  // src/services/providers/gitpod.ts
7537
- var import_child_process7 = require("child_process");
7629
+ var import_child_process9 = require("child_process");
7538
7630
  var import_util3 = require("util");
7539
- var path13 = __toESM(require("path"));
7631
+ var path14 = __toESM(require("path"));
7540
7632
  var import_picocolors8 = __toESM(require("picocolors"));
7541
- var execFileP3 = (0, import_util3.promisify)(import_child_process7.execFile);
7633
+ var execFileP3 = (0, import_util3.promisify)(import_child_process9.execFile);
7542
7634
  var MAX_BUFFER2 = 8 * 1024 * 1024;
7543
7635
  function resetStdinForChild2() {
7544
7636
  if (process.stdin.isTTY) {
@@ -7578,7 +7670,7 @@ var GitpodProvider = class {
7578
7670
  );
7579
7671
  resetStdinForChild2();
7580
7672
  await new Promise((resolve2, reject) => {
7581
- const proc = (0, import_child_process7.spawn)("gitpod", ["login"], { stdio: "inherit" });
7673
+ const proc = (0, import_child_process9.spawn)("gitpod", ["login"], { stdio: "inherit" });
7582
7674
  proc.on("exit", (code) => {
7583
7675
  if (code === 0) resolve2();
7584
7676
  else reject(new Error("gitpod login failed."));
@@ -7730,7 +7822,7 @@ var GitpodProvider = class {
7730
7822
  async streamCommand(workspaceId, command2) {
7731
7823
  resetStdinForChild2();
7732
7824
  return new Promise((resolve2, reject) => {
7733
- const proc = (0, import_child_process7.spawn)(
7825
+ const proc = (0, import_child_process9.spawn)(
7734
7826
  "gitpod",
7735
7827
  ["workspace", "ssh", workspaceId, "--", "-tt", command2],
7736
7828
  { stdio: "inherit" }
@@ -7750,11 +7842,11 @@ var GitpodProvider = class {
7750
7842
  const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
7751
7843
  const remoteCmd = `mkdir -p ${shellQuote2(remoteDir)} && tar -xzf - -C ${shellQuote2(remoteDir)}`;
7752
7844
  await new Promise((resolve2, reject) => {
7753
- const tar = (0, import_child_process7.spawn)("tar", tarArgs, {
7845
+ const tar = (0, import_child_process9.spawn)("tar", tarArgs, {
7754
7846
  stdio: ["ignore", "pipe", "pipe"],
7755
7847
  env: tarEnv
7756
7848
  });
7757
- const ssh = (0, import_child_process7.spawn)(
7849
+ const ssh = (0, import_child_process9.spawn)(
7758
7850
  "gitpod",
7759
7851
  ["workspace", "ssh", workspaceId, "--", remoteCmd],
7760
7852
  { stdio: [tar.stdout, "pipe", "pipe"] }
@@ -7776,7 +7868,7 @@ var GitpodProvider = class {
7776
7868
  });
7777
7869
  }
7778
7870
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
7779
- const remoteDir = path13.posix.dirname(remotePath);
7871
+ const remoteDir = path14.posix.dirname(remotePath);
7780
7872
  const parts = [
7781
7873
  `mkdir -p ${shellQuote2(remoteDir)}`,
7782
7874
  `cat > ${shellQuote2(remotePath)}`
@@ -7786,7 +7878,7 @@ var GitpodProvider = class {
7786
7878
  }
7787
7879
  const cmd = parts.join(" && ");
7788
7880
  await new Promise((resolve2, reject) => {
7789
- const proc = (0, import_child_process7.spawn)(
7881
+ const proc = (0, import_child_process9.spawn)(
7790
7882
  "gitpod",
7791
7883
  ["workspace", "ssh", workspaceId, "--", cmd],
7792
7884
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -7810,10 +7902,10 @@ function shellQuote2(s) {
7810
7902
  }
7811
7903
 
7812
7904
  // src/services/providers/gitlab-workspaces.ts
7813
- var import_child_process8 = require("child_process");
7905
+ var import_child_process10 = require("child_process");
7814
7906
  var import_util4 = require("util");
7815
- var path14 = __toESM(require("path"));
7816
- 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);
7817
7909
  var MAX_BUFFER3 = 8 * 1024 * 1024;
7818
7910
  var GITLAB_API_BASE = process.env.CODEAM_GITLAB_API_URL ?? "https://gitlab.com/api/v4";
7819
7911
  function resetStdinForChild3() {
@@ -7855,7 +7947,7 @@ var GitLabWorkspacesProvider = class {
7855
7947
  );
7856
7948
  resetStdinForChild3();
7857
7949
  await new Promise((resolve2, reject) => {
7858
- const proc = (0, import_child_process8.spawn)(
7950
+ const proc = (0, import_child_process10.spawn)(
7859
7951
  "glab",
7860
7952
  ["auth", "login", "--scopes", "api,read_user,read_repository"],
7861
7953
  { stdio: "inherit" }
@@ -8027,7 +8119,7 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
8027
8119
  const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
8028
8120
  resetStdinForChild3();
8029
8121
  return new Promise((resolve2, reject) => {
8030
- const proc = (0, import_child_process8.spawn)(
8122
+ const proc = (0, import_child_process10.spawn)(
8031
8123
  "ssh",
8032
8124
  ["-tt", "-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, command2],
8033
8125
  { stdio: "inherit" }
@@ -8048,8 +8140,8 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
8048
8140
  const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
8049
8141
  const remoteCmd = `mkdir -p ${shellQuote3(remoteDir)} && tar -xzf - -C ${shellQuote3(remoteDir)}`;
8050
8142
  await new Promise((resolve2, reject) => {
8051
- const tar = (0, import_child_process8.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
8052
- const ssh = (0, import_child_process8.spawn)(
8143
+ const tar = (0, import_child_process10.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
8144
+ const ssh = (0, import_child_process10.spawn)(
8053
8145
  "ssh",
8054
8146
  ["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, remoteCmd],
8055
8147
  { stdio: [tar.stdout, "pipe", "pipe"] }
@@ -8072,14 +8164,14 @@ Docs: https://docs.gitlab.com/ee/user/workspace/configuration.html`
8072
8164
  }
8073
8165
  async uploadFile(workspaceId, remotePath, contents, options = {}) {
8074
8166
  const sshHost = process.env.CODEAM_GITLAB_SSH_HOST ?? "workspaces.gitlab.com";
8075
- const remoteDir = path14.posix.dirname(remotePath);
8167
+ const remoteDir = path15.posix.dirname(remotePath);
8076
8168
  const parts = [`mkdir -p ${shellQuote3(remoteDir)}`, `cat > ${shellQuote3(remotePath)}`];
8077
8169
  if (options.mode != null) {
8078
8170
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote3(remotePath)}`);
8079
8171
  }
8080
8172
  const cmd = parts.join(" && ");
8081
8173
  await new Promise((resolve2, reject) => {
8082
- const proc = (0, import_child_process8.spawn)(
8174
+ const proc = (0, import_child_process10.spawn)(
8083
8175
  "ssh",
8084
8176
  ["-o", "StrictHostKeyChecking=accept-new", `${workspaceId}@${sshHost}`, cmd],
8085
8177
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -8138,10 +8230,10 @@ function shellQuote3(s) {
8138
8230
  }
8139
8231
 
8140
8232
  // src/services/providers/railway.ts
8141
- var import_child_process9 = require("child_process");
8233
+ var import_child_process11 = require("child_process");
8142
8234
  var import_util5 = require("util");
8143
- var path15 = __toESM(require("path"));
8144
- 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);
8145
8237
  var MAX_BUFFER4 = 8 * 1024 * 1024;
8146
8238
  function resetStdinForChild4() {
8147
8239
  if (process.stdin.isTTY) {
@@ -8182,7 +8274,7 @@ var RailwayProvider = class {
8182
8274
  );
8183
8275
  resetStdinForChild4();
8184
8276
  await new Promise((resolve2, reject) => {
8185
- const proc = (0, import_child_process9.spawn)("railway", ["login"], { stdio: "inherit" });
8277
+ const proc = (0, import_child_process11.spawn)("railway", ["login"], { stdio: "inherit" });
8186
8278
  proc.on("exit", (code) => {
8187
8279
  if (code === 0) resolve2();
8188
8280
  else reject(new Error("railway login failed."));
@@ -8325,7 +8417,7 @@ var RailwayProvider = class {
8325
8417
  }
8326
8418
  resetStdinForChild4();
8327
8419
  return new Promise((resolve2, reject) => {
8328
- const proc = (0, import_child_process9.spawn)(
8420
+ const proc = (0, import_child_process11.spawn)(
8329
8421
  "railway",
8330
8422
  ["shell", "--project", projectId, "--service", serviceId, "--command", command2],
8331
8423
  { stdio: "inherit" }
@@ -8349,8 +8441,8 @@ var RailwayProvider = class {
8349
8441
  const tarEnv = { ...process.env, COPYFILE_DISABLE: "1" };
8350
8442
  const remoteCmd = `mkdir -p ${shellQuote4(remoteDir)} && tar -xzf - -C ${shellQuote4(remoteDir)}`;
8351
8443
  await new Promise((resolve2, reject) => {
8352
- const tar = (0, import_child_process9.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
8353
- const sh = (0, import_child_process9.spawn)(
8444
+ const tar = (0, import_child_process11.spawn)("tar", tarArgs, { stdio: ["ignore", "pipe", "pipe"], env: tarEnv });
8445
+ const sh = (0, import_child_process11.spawn)(
8354
8446
  "railway",
8355
8447
  ["shell", "--project", projectId, "--service", serviceId, "--command", remoteCmd],
8356
8448
  { stdio: [tar.stdout, "pipe", "pipe"] }
@@ -8376,14 +8468,14 @@ var RailwayProvider = class {
8376
8468
  if (!projectId || !serviceId) {
8377
8469
  throw new Error("Invalid Railway workspace id (expected projectId/serviceId).");
8378
8470
  }
8379
- const remoteDir = path15.posix.dirname(remotePath);
8471
+ const remoteDir = path16.posix.dirname(remotePath);
8380
8472
  const parts = [`mkdir -p ${shellQuote4(remoteDir)}`, `cat > ${shellQuote4(remotePath)}`];
8381
8473
  if (options.mode != null) {
8382
8474
  parts.push(`chmod ${options.mode.toString(8)} ${shellQuote4(remotePath)}`);
8383
8475
  }
8384
8476
  const cmd = parts.join(" && ");
8385
8477
  await new Promise((resolve2, reject) => {
8386
- const proc = (0, import_child_process9.spawn)(
8478
+ const proc = (0, import_child_process11.spawn)(
8387
8479
  "railway",
8388
8480
  ["shell", "--project", projectId, "--service", serviceId, "--command", cmd],
8389
8481
  { stdio: ["pipe", "pipe", "pipe"] }
@@ -8415,7 +8507,7 @@ var PROVIDERS = [
8415
8507
  ];
8416
8508
 
8417
8509
  // src/commands/deploy.ts
8418
- var execFileP6 = (0, import_util6.promisify)(import_child_process10.execFile);
8510
+ var execFileP6 = (0, import_util6.promisify)(import_child_process12.execFile);
8419
8511
  async function deploy() {
8420
8512
  console.log();
8421
8513
  mt(import_picocolors9.default.bgMagenta(import_picocolors9.default.white(" codeam deploy ")));
@@ -8568,7 +8660,7 @@ async function deploy() {
8568
8660
  process.exit(1);
8569
8661
  }
8570
8662
  }
8571
- const localClaudeDir = path16.join(os8.homedir(), ".claude");
8663
+ const localClaudeDir = path17.join(os9.homedir(), ".claude");
8572
8664
  const localCredsKind = await detectLocalClaudeCredentials(localClaudeDir);
8573
8665
  let bridged = "none";
8574
8666
  if (localCredsKind !== "none") {
@@ -8612,7 +8704,7 @@ async function deploy() {
8612
8704
  process.exit(1);
8613
8705
  }
8614
8706
  claudeStep.stop("\u2713 Claude CLI installed");
8615
- const haveLocalClaude = fs9.existsSync(localClaudeDir) && fs9.statSync(localClaudeDir).isDirectory();
8707
+ const haveLocalClaude = fs10.existsSync(localClaudeDir) && fs10.statSync(localClaudeDir).isDirectory();
8616
8708
  if (haveLocalClaude) {
8617
8709
  const copyStep = fe();
8618
8710
  copyStep.start("Copying local Claude config to workspace\u2026");
@@ -8666,10 +8758,10 @@ async function deploy() {
8666
8758
  }
8667
8759
  }
8668
8760
  if (bridged !== "none") {
8669
- const localClaudeJson = path16.join(os8.homedir(), ".claude.json");
8670
- if (fs9.existsSync(localClaudeJson)) {
8761
+ const localClaudeJson = path17.join(os9.homedir(), ".claude.json");
8762
+ if (fs10.existsSync(localClaudeJson)) {
8671
8763
  try {
8672
- const contents = fs9.readFileSync(localClaudeJson);
8764
+ const contents = fs10.readFileSync(localClaudeJson);
8673
8765
  await provider.uploadFile(
8674
8766
  workspace.id,
8675
8767
  "/home/codespace/.claude.json",
@@ -8859,7 +8951,7 @@ async function runRemoteClaudeLogin(provider, workspaceId) {
8859
8951
  }
8860
8952
  }
8861
8953
  async function detectLocalClaudeCredentials(localClaudeDir) {
8862
- if (fs9.existsSync(path16.join(localClaudeDir, ".credentials.json"))) {
8954
+ if (fs10.existsSync(path17.join(localClaudeDir, ".credentials.json"))) {
8863
8955
  return "flat-file";
8864
8956
  }
8865
8957
  if (process.platform === "darwin") {
@@ -8892,8 +8984,8 @@ async function verifyClaudeAuth(provider, workspaceId) {
8892
8984
  }
8893
8985
  }
8894
8986
  async function bridgeClaudeCredentials(provider, workspaceId, localClaudeDir) {
8895
- const fileBased = path16.join(localClaudeDir, ".credentials.json");
8896
- if (fs9.existsSync(fileBased)) return "flat-file";
8987
+ const fileBased = path17.join(localClaudeDir, ".credentials.json");
8988
+ if (fs10.existsSync(fileBased)) return "flat-file";
8897
8989
  if (process.platform === "darwin") {
8898
8990
  try {
8899
8991
  const { stdout } = await execFileP6(
@@ -9101,7 +9193,7 @@ async function stopWorkspaceFromLocal(target) {
9101
9193
  // src/commands/version.ts
9102
9194
  var import_picocolors11 = __toESM(require("picocolors"));
9103
9195
  function version() {
9104
- const v = true ? "2.4.39" : "unknown";
9196
+ const v = true ? "2.5.0" : "unknown";
9105
9197
  console.log(`${import_picocolors11.default.bold("codeam-cli")} ${import_picocolors11.default.cyan(v)}`);
9106
9198
  }
9107
9199
 
@@ -9138,22 +9230,22 @@ function help() {
9138
9230
  }
9139
9231
 
9140
9232
  // src/lib/updateNotifier.ts
9141
- var fs10 = __toESM(require("fs"));
9142
- var os9 = __toESM(require("os"));
9143
- var path17 = __toESM(require("path"));
9144
- var https4 = __toESM(require("https"));
9233
+ var fs11 = __toESM(require("fs"));
9234
+ var os10 = __toESM(require("os"));
9235
+ var path18 = __toESM(require("path"));
9236
+ var https5 = __toESM(require("https"));
9145
9237
  var import_picocolors13 = __toESM(require("picocolors"));
9146
9238
  var PKG_NAME = "codeam-cli";
9147
9239
  var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
9148
9240
  var TTL_MS = 24 * 60 * 60 * 1e3;
9149
9241
  var REQUEST_TIMEOUT_MS = 1500;
9150
9242
  function cachePath() {
9151
- const dir = path17.join(os9.homedir(), ".codeam");
9152
- return path17.join(dir, "update-check.json");
9243
+ const dir = path18.join(os10.homedir(), ".codeam");
9244
+ return path18.join(dir, "update-check.json");
9153
9245
  }
9154
9246
  function readCache() {
9155
9247
  try {
9156
- const raw = fs10.readFileSync(cachePath(), "utf8");
9248
+ const raw = fs11.readFileSync(cachePath(), "utf8");
9157
9249
  const parsed = JSON.parse(raw);
9158
9250
  if (typeof parsed.fetchedAt !== "number" || typeof parsed.latest !== "string") return null;
9159
9251
  return parsed;
@@ -9164,8 +9256,8 @@ function readCache() {
9164
9256
  function writeCache(cache) {
9165
9257
  try {
9166
9258
  const file = cachePath();
9167
- fs10.mkdirSync(path17.dirname(file), { recursive: true });
9168
- fs10.writeFileSync(file, JSON.stringify(cache));
9259
+ fs11.mkdirSync(path18.dirname(file), { recursive: true });
9260
+ fs11.writeFileSync(file, JSON.stringify(cache));
9169
9261
  } catch {
9170
9262
  }
9171
9263
  }
@@ -9184,7 +9276,7 @@ function compareSemver(a, b) {
9184
9276
  }
9185
9277
  function fetchLatest() {
9186
9278
  return new Promise((resolve2) => {
9187
- const req = https4.get(
9279
+ const req = https5.get(
9188
9280
  REGISTRY_URL,
9189
9281
  { headers: { Accept: "application/json" }, timeout: REQUEST_TIMEOUT_MS },
9190
9282
  (res) => {
@@ -9236,7 +9328,7 @@ function checkForUpdates() {
9236
9328
  if (process.env.CODEAM_DISABLE_UPDATE_CHECK === "1") return;
9237
9329
  if (process.env.CI) return;
9238
9330
  if (!process.stdout.isTTY) return;
9239
- const current = true ? "2.4.39" : null;
9331
+ const current = true ? "2.5.0" : null;
9240
9332
  if (!current) return;
9241
9333
  const cache = readCache();
9242
9334
  const fresh = cache && Date.now() - cache.fetchedAt < TTL_MS;