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.
- package/dist/index.js +252 -26
- 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.
|
|
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:
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if (cmd.includes("
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if (cmd.includes("
|
|
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
|
-
|
|
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
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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);
|