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.
- package/dist/index.js +238 -18
- 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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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);
|