ragent-cli 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +238 -18
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "ragent-cli",
34
- version: "1.1.0",
34
+ version: "1.2.1",
35
35
  description: "CLI agent for rAgent Live \u2014 browser-first terminal control plane for AI coding agents",
36
36
  main: "dist/index.js",
37
37
  bin: {
@@ -435,6 +435,14 @@ async function collectZellijSessions() {
435
435
  return [];
436
436
  }
437
437
  }
438
+ async function getProcessWorkingDir(pid) {
439
+ try {
440
+ const cwd = await execAsync(`readlink -f /proc/${pid}/cwd`);
441
+ return cwd.trim() || null;
442
+ } catch {
443
+ return null;
444
+ }
445
+ }
438
446
  async function collectBareAgentProcesses(excludePids) {
439
447
  try {
440
448
  const raw = await execAsync("ps axo pid,ppid,comm,args --no-headers");
@@ -452,14 +460,18 @@ async function collectBareAgentProcesses(excludePids) {
452
460
  if (excludePids?.has(pid)) continue;
453
461
  if (seen.has(pid)) continue;
454
462
  seen.add(pid);
463
+ const workingDir = await getProcessWorkingDir(pid);
464
+ const dirName = workingDir ? workingDir.split("/").pop() : null;
465
+ const displayName = dirName && dirName !== "/" && dirName !== "" ? `${agentType} (${dirName})` : `${agentType} (pid ${pid})`;
455
466
  const id = `process:${pid}`;
456
467
  sessions.push({
457
468
  id,
458
469
  type: "process",
459
- name: `${agentType} (pid ${pid})`,
470
+ name: displayName,
460
471
  status: "active",
461
472
  command: args,
462
473
  agentType,
474
+ workingDir: workingDir || void 0,
463
475
  lastActivityAt: (/* @__PURE__ */ new Date()).toISOString(),
464
476
  pids: [pid]
465
477
  });
@@ -735,6 +747,7 @@ var OutputBuffer = class {
735
747
  };
736
748
 
737
749
  // src/pty.ts
750
+ var import_node_child_process2 = require("child_process");
738
751
  var pty = __toESM(require("node-pty"));
739
752
  function isInteractiveShell(command) {
740
753
  const trimmed = String(command).trim();
@@ -762,6 +775,27 @@ async function stopTmuxPaneBySessionId(sessionId) {
762
775
  await execAsync(`tmux kill-pane -t ${shellQuote(paneTarget)}`, { timeout: 5e3 });
763
776
  return true;
764
777
  }
778
+ async function sendInputToTmux(sessionId, data) {
779
+ if (!sessionId.startsWith("tmux:")) return;
780
+ const target = sessionId.slice("tmux:".length).trim();
781
+ if (!target) return;
782
+ const sessionName = target.split(":")[0].split(".")[0];
783
+ if (!/^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/.test(sessionName)) {
784
+ console.warn(`[rAgent] Invalid tmux session name: ${sessionName}`);
785
+ return;
786
+ }
787
+ try {
788
+ await new Promise((resolve, reject) => {
789
+ (0, import_node_child_process2.execFile)("tmux", ["send-keys", "-t", target, "-l", "--", data], { timeout: 5e3 }, (err) => {
790
+ if (err) reject(err);
791
+ else resolve();
792
+ });
793
+ });
794
+ } catch (error) {
795
+ const message = error instanceof Error ? error.message : String(error);
796
+ console.warn(`[rAgent] Failed to send input to ${sessionId}: ${message}`);
797
+ }
798
+ }
765
799
  async function stopAllDetachedTmuxSessions() {
766
800
  try {
767
801
  const raw = await execAsync(
@@ -1082,6 +1116,120 @@ function sendToGroup(ws, group, data) {
1082
1116
  );
1083
1117
  }
1084
1118
 
1119
+ // src/session-streamer.ts
1120
+ var pty2 = __toESM(require("node-pty"));
1121
+ var STOP_DEBOUNCE_MS = 2e3;
1122
+ var SessionStreamer = class {
1123
+ active = /* @__PURE__ */ new Map();
1124
+ pendingStops = /* @__PURE__ */ new Map();
1125
+ sendFn;
1126
+ onStreamStopped;
1127
+ constructor(sendFn, onStreamStopped) {
1128
+ this.sendFn = sendFn;
1129
+ this.onStreamStopped = onStreamStopped;
1130
+ }
1131
+ /**
1132
+ * Start streaming a tmux session. Returns true if streaming started.
1133
+ */
1134
+ startStream(sessionId) {
1135
+ const pendingStop = this.pendingStops.get(sessionId);
1136
+ if (pendingStop) {
1137
+ clearTimeout(pendingStop);
1138
+ this.pendingStops.delete(sessionId);
1139
+ if (this.active.has(sessionId)) {
1140
+ console.log(`[rAgent] Cancelled pending stop for: ${sessionId} (re-mounted)`);
1141
+ return true;
1142
+ }
1143
+ }
1144
+ if (this.active.has(sessionId)) {
1145
+ return true;
1146
+ }
1147
+ const parts = sessionId.split(":");
1148
+ if (parts[0] !== "tmux" || parts.length < 2) {
1149
+ return false;
1150
+ }
1151
+ const tmuxSession = parts[1];
1152
+ try {
1153
+ const cleanEnv = { ...process.env };
1154
+ delete cleanEnv.TMUX;
1155
+ delete cleanEnv.TMUX_PANE;
1156
+ const proc = pty2.spawn("tmux", ["attach-session", "-t", tmuxSession, "-r"], {
1157
+ name: "xterm-256color",
1158
+ cols: 120,
1159
+ rows: 40,
1160
+ cwd: process.cwd(),
1161
+ env: cleanEnv
1162
+ });
1163
+ const stream = { pty: proc, sessionId, tmuxSession };
1164
+ this.active.set(sessionId, stream);
1165
+ proc.onData((data) => {
1166
+ this.sendFn(sessionId, data);
1167
+ });
1168
+ proc.onExit(({ exitCode, signal }) => {
1169
+ this.active.delete(sessionId);
1170
+ this.pendingStops.delete(sessionId);
1171
+ console.log(`[rAgent] Session stream ended: ${sessionId} (exit=${exitCode}, signal=${signal})`);
1172
+ this.onStreamStopped?.(sessionId);
1173
+ });
1174
+ console.log(`[rAgent] Started streaming: ${sessionId} (tmux session: ${tmuxSession})`);
1175
+ return true;
1176
+ } catch (error) {
1177
+ const message = error instanceof Error ? error.message : String(error);
1178
+ console.warn(`[rAgent] Failed to start stream for ${sessionId}: ${message}`);
1179
+ return false;
1180
+ }
1181
+ }
1182
+ /**
1183
+ * Stop streaming a session (debounced to absorb React remount cycles).
1184
+ */
1185
+ stopStream(sessionId) {
1186
+ const stream = this.active.get(sessionId);
1187
+ if (!stream) return;
1188
+ if (this.pendingStops.has(sessionId)) return;
1189
+ const timer = setTimeout(() => {
1190
+ this.pendingStops.delete(sessionId);
1191
+ const s = this.active.get(sessionId);
1192
+ if (!s) return;
1193
+ try {
1194
+ s.pty.kill();
1195
+ } catch {
1196
+ }
1197
+ this.active.delete(sessionId);
1198
+ console.log(`[rAgent] Stopped streaming: ${sessionId}`);
1199
+ }, STOP_DEBOUNCE_MS);
1200
+ this.pendingStops.set(sessionId, timer);
1201
+ }
1202
+ /**
1203
+ * Check if a session is currently being streamed.
1204
+ */
1205
+ isStreaming(sessionId) {
1206
+ return this.active.has(sessionId);
1207
+ }
1208
+ /**
1209
+ * Stop all active streams immediately (no debounce).
1210
+ */
1211
+ stopAll() {
1212
+ for (const timer of this.pendingStops.values()) {
1213
+ clearTimeout(timer);
1214
+ }
1215
+ this.pendingStops.clear();
1216
+ for (const [id, stream] of this.active) {
1217
+ try {
1218
+ stream.pty.kill();
1219
+ } catch {
1220
+ }
1221
+ console.log(`[rAgent] Stopped streaming: ${id}`);
1222
+ }
1223
+ this.active.clear();
1224
+ }
1225
+ /**
1226
+ * Get the number of active streams.
1227
+ */
1228
+ get activeCount() {
1229
+ return this.active.size;
1230
+ }
1231
+ };
1232
+
1085
1233
  // src/provisioner.ts
1086
1234
  var import_child_process2 = require("child_process");
1087
1235
  var DANGEROUS_PATTERN = /rm\s+-r[fe]*\s+\/|mkfs|dd\s+if=|:()\s*\{|>\s*\/dev\/sd/i;
@@ -1368,14 +1516,27 @@ async function runAgent(rawOptions) {
1368
1516
  let lastHttpHeartbeatAt = 0;
1369
1517
  const outputBuffer = new OutputBuffer();
1370
1518
  let ptyProcess = null;
1519
+ const ptySessionId = `pty:${options.hostId}`;
1371
1520
  const sendOutput = (chunk) => {
1372
1521
  const ws = activeSocket;
1373
1522
  if (!ws || ws.readyState !== import_ws2.default.OPEN || !activeGroups.privateGroup) {
1374
1523
  outputBuffer.push(chunk);
1375
1524
  return;
1376
1525
  }
1377
- sendToGroup(ws, activeGroups.privateGroup, { type: "output", data: chunk });
1526
+ sendToGroup(ws, activeGroups.privateGroup, { type: "output", data: chunk, sessionId: ptySessionId });
1378
1527
  };
1528
+ const sessionStreamer = new SessionStreamer(
1529
+ (sessionId, data) => {
1530
+ const ws = activeSocket;
1531
+ if (!ws || ws.readyState !== import_ws2.default.OPEN || !activeGroups.privateGroup) return;
1532
+ sendToGroup(ws, activeGroups.privateGroup, { type: "output", data, sessionId });
1533
+ },
1534
+ (sessionId) => {
1535
+ const ws = activeSocket;
1536
+ if (!ws || ws.readyState !== import_ws2.default.OPEN || !activeGroups.privateGroup) return;
1537
+ sendToGroup(ws, activeGroups.privateGroup, { type: "stream-stopped", sessionId });
1538
+ }
1539
+ );
1379
1540
  const killCurrentShell = () => {
1380
1541
  if (!ptyProcess) return;
1381
1542
  suppressNextShellRespawn = true;
@@ -1403,7 +1564,8 @@ async function runAgent(rawOptions) {
1403
1564
  spawnOrRespawnShell();
1404
1565
  };
1405
1566
  spawnOrRespawnShell();
1406
- const cleanupSocket = () => {
1567
+ const cleanupSocket = (opts = {}) => {
1568
+ if (opts.stopStreams) sessionStreamer.stopAll();
1407
1569
  if (wsHeartbeatTimer) {
1408
1570
  clearInterval(wsHeartbeatTimer);
1409
1571
  wsHeartbeatTimer = null;
@@ -1495,12 +1657,14 @@ async function runAgent(rawOptions) {
1495
1657
  case "restart-agent":
1496
1658
  case "disconnect":
1497
1659
  reconnectRequested = true;
1660
+ sessionStreamer.stopAll();
1498
1661
  if (activeSocket && activeSocket.readyState === import_ws2.default.OPEN) {
1499
1662
  activeSocket.close();
1500
1663
  }
1501
1664
  return;
1502
1665
  case "stop-agent":
1503
1666
  shouldRun = false;
1667
+ sessionStreamer.stopAll();
1504
1668
  requestStopSelfService();
1505
1669
  if (activeSocket && activeSocket.readyState === import_ws2.default.OPEN) {
1506
1670
  activeSocket.close();
@@ -1571,12 +1735,59 @@ async function runAgent(rawOptions) {
1571
1735
  await syncInventory(true);
1572
1736
  return;
1573
1737
  }
1738
+ case "stream-session": {
1739
+ if (!sessionId) return;
1740
+ if (sessionId.startsWith("process:")) {
1741
+ const ws2 = activeSocket;
1742
+ if (ws2 && ws2.readyState === import_ws2.default.OPEN && activeGroups.privateGroup) {
1743
+ sendToGroup(ws2, activeGroups.privateGroup, {
1744
+ type: "stream-error",
1745
+ sessionId,
1746
+ error: "Live output is not available for standalone processes. Start agents inside tmux for live monitoring."
1747
+ });
1748
+ }
1749
+ return;
1750
+ }
1751
+ if (sessionId.startsWith("tmux:")) {
1752
+ const started = sessionStreamer.startStream(sessionId);
1753
+ const ws2 = activeSocket;
1754
+ if (ws2 && ws2.readyState === import_ws2.default.OPEN && activeGroups.privateGroup) {
1755
+ if (started) {
1756
+ sendToGroup(ws2, activeGroups.privateGroup, {
1757
+ type: "stream-started",
1758
+ sessionId
1759
+ });
1760
+ } else {
1761
+ sendToGroup(ws2, activeGroups.privateGroup, {
1762
+ type: "stream-error",
1763
+ sessionId,
1764
+ error: "Failed to attach to tmux session. It may no longer exist."
1765
+ });
1766
+ }
1767
+ }
1768
+ return;
1769
+ }
1770
+ const ws = activeSocket;
1771
+ if (ws && ws.readyState === import_ws2.default.OPEN && activeGroups.privateGroup) {
1772
+ sendToGroup(ws, activeGroups.privateGroup, {
1773
+ type: "stream-error",
1774
+ sessionId,
1775
+ error: `Streaming not yet supported for session type: ${sessionId.split(":")[0]}`
1776
+ });
1777
+ }
1778
+ return;
1779
+ }
1780
+ case "stop-stream": {
1781
+ if (!sessionId) return;
1782
+ sessionStreamer.stopStream(sessionId);
1783
+ return;
1784
+ }
1574
1785
  default:
1575
1786
  }
1576
1787
  };
1577
1788
  const onSignal = () => {
1578
1789
  shouldRun = false;
1579
- cleanupSocket();
1790
+ cleanupSocket({ stopStreams: true });
1580
1791
  killCurrentShell();
1581
1792
  releasePidLock(lockPath);
1582
1793
  };
@@ -1632,7 +1843,8 @@ async function runAgent(rawOptions) {
1632
1843
  for (const chunk of buffered) {
1633
1844
  sendToGroup(ws, activeGroups.privateGroup, {
1634
1845
  type: "output",
1635
- data: chunk
1846
+ data: chunk,
1847
+ sessionId: ptySessionId
1636
1848
  });
1637
1849
  }
1638
1850
  }
@@ -1658,18 +1870,26 @@ async function runAgent(rawOptions) {
1658
1870
  if (msg.type === "message" && msg.group === activeGroups.privateGroup) {
1659
1871
  const payload = msg.data || {};
1660
1872
  if (payload.type === "input" && typeof payload.data === "string") {
1661
- if (ptyProcess) ptyProcess.write(payload.data);
1873
+ const sid = typeof payload.sessionId === "string" ? payload.sessionId.trim() : "";
1874
+ if (!sid || sid.startsWith("pty:")) {
1875
+ if (ptyProcess) ptyProcess.write(payload.data);
1876
+ } else if (sid.startsWith("tmux:")) {
1877
+ await sendInputToTmux(sid, payload.data);
1878
+ }
1662
1879
  } else if (payload.type === "resize" && Number.isInteger(payload.cols) && Number.isInteger(payload.rows)) {
1663
- try {
1664
- if (ptyProcess)
1665
- ptyProcess.resize(
1666
- payload.cols,
1667
- payload.rows
1668
- );
1669
- } catch (error) {
1670
- const message = error instanceof Error ? error.message : String(error);
1671
- if (!message.includes("EBADF")) {
1672
- console.warn(`[rAgent] Resize failed: ${message}`);
1880
+ const sid = typeof payload.sessionId === "string" ? payload.sessionId.trim() : "";
1881
+ if (!sid || sid.startsWith("pty:")) {
1882
+ try {
1883
+ if (ptyProcess)
1884
+ ptyProcess.resize(
1885
+ payload.cols,
1886
+ payload.rows
1887
+ );
1888
+ } catch (error) {
1889
+ const message = error instanceof Error ? error.message : String(error);
1890
+ if (!message.includes("EBADF")) {
1891
+ console.warn(`[rAgent] Resize failed: ${message}`);
1892
+ }
1673
1893
  }
1674
1894
  }
1675
1895
  } else if (payload.type === "control" && typeof payload.action === "string") {
@@ -1747,7 +1967,7 @@ async function runAgent(rawOptions) {
1747
1967
  reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY_MS);
1748
1968
  }
1749
1969
  } finally {
1750
- cleanupSocket();
1970
+ cleanupSocket({ stopStreams: true });
1751
1971
  killCurrentShell();
1752
1972
  releasePidLock(lockPath);
1753
1973
  process.removeListener("SIGTERM", onSignal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ragent-cli",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "CLI agent for rAgent Live — browser-first terminal control plane for AI coding agents",
5
5
  "main": "dist/index.js",
6
6
  "bin": {