ragent-cli 1.1.0 → 1.2.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 +252 -26
  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.0",
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
  });
@@ -494,14 +506,20 @@ async function collectSessionInventory(hostId, command) {
494
506
  function detectAgentType(command) {
495
507
  if (!command) return void 0;
496
508
  const cmd = command.toLowerCase();
497
- if (cmd.includes("claude")) return "Claude Code";
498
- if (cmd.includes("codex")) return "Codex CLI";
499
- if (cmd.includes("aider")) return "aider";
500
- if (cmd.includes("cursor")) return "Cursor";
501
- if (cmd.includes("windsurf")) return "Windsurf";
502
- if (cmd.includes("gemini")) return "Gemini CLI";
503
- if (cmd.includes("amazon-q") || cmd.includes("amazon_q")) return "Amazon Q";
504
- if (cmd.includes("copilot")) return "Copilot CLI";
509
+ const parts = cmd.split(/\s+/);
510
+ const binary = parts[0]?.split("/").pop() ?? "";
511
+ const scriptArg = binary === "node" && parts[1] ? parts[1].split("/").pop() ?? "" : "";
512
+ if (binary === "claude" || scriptArg === "cli.js" && cmd.includes("claude-code")) {
513
+ if (cmd.includes("--chrome-native-host")) return void 0;
514
+ return "Claude Code";
515
+ }
516
+ if (binary === "codex" || cmd.includes("codex-cli")) return "Codex CLI";
517
+ if (binary === "aider") return "aider";
518
+ if (binary === "cursor") return "Cursor";
519
+ if (binary === "windsurf") return "Windsurf";
520
+ if (binary === "gemini") return "Gemini CLI";
521
+ if (binary === "amazon-q" || binary === "amazon_q") return "Amazon Q";
522
+ if (binary === "copilot") return "Copilot CLI";
505
523
  return void 0;
506
524
  }
507
525
  function sessionInventoryFingerprint(sessions) {
@@ -729,6 +747,7 @@ var OutputBuffer = class {
729
747
  };
730
748
 
731
749
  // src/pty.ts
750
+ var import_node_child_process2 = require("child_process");
732
751
  var pty = __toESM(require("node-pty"));
733
752
  function isInteractiveShell(command) {
734
753
  const trimmed = String(command).trim();
@@ -756,6 +775,27 @@ async function stopTmuxPaneBySessionId(sessionId) {
756
775
  await execAsync(`tmux kill-pane -t ${shellQuote(paneTarget)}`, { timeout: 5e3 });
757
776
  return true;
758
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
+ }
759
799
  async function stopAllDetachedTmuxSessions() {
760
800
  try {
761
801
  const raw = await execAsync(
@@ -1076,6 +1116,120 @@ function sendToGroup(ws, group, data) {
1076
1116
  );
1077
1117
  }
1078
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(() => {
1169
+ this.active.delete(sessionId);
1170
+ this.pendingStops.delete(sessionId);
1171
+ console.log(`[rAgent] Session stream ended: ${sessionId}`);
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
+
1079
1233
  // src/provisioner.ts
1080
1234
  var import_child_process2 = require("child_process");
1081
1235
  var DANGEROUS_PATTERN = /rm\s+-r[fe]*\s+\/|mkfs|dd\s+if=|:()\s*\{|>\s*\/dev\/sd/i;
@@ -1362,14 +1516,27 @@ async function runAgent(rawOptions) {
1362
1516
  let lastHttpHeartbeatAt = 0;
1363
1517
  const outputBuffer = new OutputBuffer();
1364
1518
  let ptyProcess = null;
1519
+ const ptySessionId = `pty:${options.hostId}`;
1365
1520
  const sendOutput = (chunk) => {
1366
1521
  const ws = activeSocket;
1367
1522
  if (!ws || ws.readyState !== import_ws2.default.OPEN || !activeGroups.privateGroup) {
1368
1523
  outputBuffer.push(chunk);
1369
1524
  return;
1370
1525
  }
1371
- sendToGroup(ws, activeGroups.privateGroup, { type: "output", data: chunk });
1526
+ sendToGroup(ws, activeGroups.privateGroup, { type: "output", data: chunk, sessionId: ptySessionId });
1372
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
+ );
1373
1540
  const killCurrentShell = () => {
1374
1541
  if (!ptyProcess) return;
1375
1542
  suppressNextShellRespawn = true;
@@ -1397,7 +1564,8 @@ async function runAgent(rawOptions) {
1397
1564
  spawnOrRespawnShell();
1398
1565
  };
1399
1566
  spawnOrRespawnShell();
1400
- const cleanupSocket = () => {
1567
+ const cleanupSocket = (opts = {}) => {
1568
+ if (opts.stopStreams) sessionStreamer.stopAll();
1401
1569
  if (wsHeartbeatTimer) {
1402
1570
  clearInterval(wsHeartbeatTimer);
1403
1571
  wsHeartbeatTimer = null;
@@ -1489,12 +1657,14 @@ async function runAgent(rawOptions) {
1489
1657
  case "restart-agent":
1490
1658
  case "disconnect":
1491
1659
  reconnectRequested = true;
1660
+ sessionStreamer.stopAll();
1492
1661
  if (activeSocket && activeSocket.readyState === import_ws2.default.OPEN) {
1493
1662
  activeSocket.close();
1494
1663
  }
1495
1664
  return;
1496
1665
  case "stop-agent":
1497
1666
  shouldRun = false;
1667
+ sessionStreamer.stopAll();
1498
1668
  requestStopSelfService();
1499
1669
  if (activeSocket && activeSocket.readyState === import_ws2.default.OPEN) {
1500
1670
  activeSocket.close();
@@ -1565,12 +1735,59 @@ async function runAgent(rawOptions) {
1565
1735
  await syncInventory(true);
1566
1736
  return;
1567
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
+ }
1568
1785
  default:
1569
1786
  }
1570
1787
  };
1571
1788
  const onSignal = () => {
1572
1789
  shouldRun = false;
1573
- cleanupSocket();
1790
+ cleanupSocket({ stopStreams: true });
1574
1791
  killCurrentShell();
1575
1792
  releasePidLock(lockPath);
1576
1793
  };
@@ -1626,7 +1843,8 @@ async function runAgent(rawOptions) {
1626
1843
  for (const chunk of buffered) {
1627
1844
  sendToGroup(ws, activeGroups.privateGroup, {
1628
1845
  type: "output",
1629
- data: chunk
1846
+ data: chunk,
1847
+ sessionId: ptySessionId
1630
1848
  });
1631
1849
  }
1632
1850
  }
@@ -1652,18 +1870,26 @@ async function runAgent(rawOptions) {
1652
1870
  if (msg.type === "message" && msg.group === activeGroups.privateGroup) {
1653
1871
  const payload = msg.data || {};
1654
1872
  if (payload.type === "input" && typeof payload.data === "string") {
1655
- 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
+ }
1656
1879
  } else if (payload.type === "resize" && Number.isInteger(payload.cols) && Number.isInteger(payload.rows)) {
1657
- try {
1658
- if (ptyProcess)
1659
- ptyProcess.resize(
1660
- payload.cols,
1661
- payload.rows
1662
- );
1663
- } catch (error) {
1664
- const message = error instanceof Error ? error.message : String(error);
1665
- if (!message.includes("EBADF")) {
1666
- 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
+ }
1667
1893
  }
1668
1894
  }
1669
1895
  } else if (payload.type === "control" && typeof payload.action === "string") {
@@ -1741,7 +1967,7 @@ async function runAgent(rawOptions) {
1741
1967
  reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY_MS);
1742
1968
  }
1743
1969
  } finally {
1744
- cleanupSocket();
1970
+ cleanupSocket({ stopStreams: true });
1745
1971
  killCurrentShell();
1746
1972
  releasePidLock(lockPath);
1747
1973
  process.removeListener("SIGTERM", onSignal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ragent-cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
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": {