ragent-cli 1.4.1 → 1.4.3
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 +239 -29
- 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.4.
|
|
34
|
+
version: "1.4.3",
|
|
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: {
|
|
@@ -830,24 +830,71 @@ var OutputBuffer = class {
|
|
|
830
830
|
// src/websocket.ts
|
|
831
831
|
var import_ws = __toESM(require("ws"));
|
|
832
832
|
var BACKPRESSURE_HIGH_WATER = 256 * 1024;
|
|
833
|
+
var BACKPRESSURE_LOW_WATER = 64 * 1024;
|
|
834
|
+
var MAX_PENDING_QUEUE = 500;
|
|
835
|
+
var DRAIN_INTERVAL_MS = 50;
|
|
833
836
|
function sanitizeForJson(str) {
|
|
834
837
|
return str.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
|
|
835
838
|
}
|
|
839
|
+
var pendingQueue = [];
|
|
840
|
+
var drainTimer = null;
|
|
841
|
+
var drainWs = null;
|
|
842
|
+
var droppedFrames = 0;
|
|
843
|
+
function drainQueue() {
|
|
844
|
+
if (!drainWs || drainWs.readyState !== import_ws.default.OPEN) {
|
|
845
|
+
pendingQueue.length = 0;
|
|
846
|
+
stopDrainTimer();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
while (pendingQueue.length > 0 && drainWs.bufferedAmount < BACKPRESSURE_LOW_WATER) {
|
|
850
|
+
const frame = pendingQueue.shift();
|
|
851
|
+
drainWs.send(frame);
|
|
852
|
+
}
|
|
853
|
+
if (pendingQueue.length === 0) {
|
|
854
|
+
stopDrainTimer();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function startDrainTimer(ws) {
|
|
858
|
+
drainWs = ws;
|
|
859
|
+
if (!drainTimer) {
|
|
860
|
+
drainTimer = setInterval(drainQueue, DRAIN_INTERVAL_MS);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function stopDrainTimer() {
|
|
864
|
+
if (drainTimer) {
|
|
865
|
+
clearInterval(drainTimer);
|
|
866
|
+
drainTimer = null;
|
|
867
|
+
}
|
|
868
|
+
drainWs = null;
|
|
869
|
+
}
|
|
836
870
|
function sendToGroup(ws, group, data) {
|
|
837
871
|
if (!group || ws.readyState !== import_ws.default.OPEN) return;
|
|
872
|
+
const sanitized = sanitizePayload(data);
|
|
873
|
+
const frame = JSON.stringify({
|
|
874
|
+
type: "sendToGroup",
|
|
875
|
+
group,
|
|
876
|
+
dataType: "json",
|
|
877
|
+
data: sanitized,
|
|
878
|
+
noEcho: true
|
|
879
|
+
});
|
|
838
880
|
if (ws.bufferedAmount > BACKPRESSURE_HIGH_WATER) {
|
|
881
|
+
if (pendingQueue.length >= MAX_PENDING_QUEUE) {
|
|
882
|
+
pendingQueue.shift();
|
|
883
|
+
droppedFrames++;
|
|
884
|
+
if (droppedFrames % 100 === 1) {
|
|
885
|
+
console.warn(`[rAgent] Backpressure: dropped ${droppedFrames} frames (queue full at ${MAX_PENDING_QUEUE})`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
pendingQueue.push(frame);
|
|
889
|
+
startDrainTimer(ws);
|
|
839
890
|
return;
|
|
840
891
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
data: sanitized,
|
|
848
|
-
noEcho: true
|
|
849
|
-
})
|
|
850
|
-
);
|
|
892
|
+
if (pendingQueue.length > 0) {
|
|
893
|
+
pendingQueue.push(frame);
|
|
894
|
+
drainQueue();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
ws.send(frame);
|
|
851
898
|
}
|
|
852
899
|
function sanitizePayload(obj) {
|
|
853
900
|
const result = {};
|
|
@@ -868,6 +915,7 @@ var import_node_child_process2 = require("child_process");
|
|
|
868
915
|
var import_node_fs = require("fs");
|
|
869
916
|
var import_node_path = require("path");
|
|
870
917
|
var import_node_os = require("os");
|
|
918
|
+
var import_node_string_decoder = require("string_decoder");
|
|
871
919
|
var pty = __toESM(require("node-pty"));
|
|
872
920
|
var STOP_DEBOUNCE_MS = 2e3;
|
|
873
921
|
function parsePaneTarget(sessionId) {
|
|
@@ -889,6 +937,37 @@ function parseZellijSession(sessionId) {
|
|
|
889
937
|
if (!rest) return null;
|
|
890
938
|
return rest.split(":")[0] || null;
|
|
891
939
|
}
|
|
940
|
+
function parseProcessPid(sessionId) {
|
|
941
|
+
if (!sessionId.startsWith("process:")) return null;
|
|
942
|
+
const rest = sessionId.slice("process:".length);
|
|
943
|
+
if (!rest) return null;
|
|
944
|
+
const pidStr = rest.split(":")[0];
|
|
945
|
+
const pid = parseInt(pidStr, 10);
|
|
946
|
+
return isNaN(pid) ? null : pid;
|
|
947
|
+
}
|
|
948
|
+
function unescapeStrace(s) {
|
|
949
|
+
return s.replace(/\\x([0-9a-fA-F]{2})|\\n|\\r|\\t|\\\\|\\"|\\0/g, (match) => {
|
|
950
|
+
if (match.startsWith("\\x")) {
|
|
951
|
+
return String.fromCharCode(parseInt(match.slice(2), 16));
|
|
952
|
+
}
|
|
953
|
+
switch (match) {
|
|
954
|
+
case "\\n":
|
|
955
|
+
return "\n";
|
|
956
|
+
case "\\r":
|
|
957
|
+
return "\r";
|
|
958
|
+
case "\\t":
|
|
959
|
+
return " ";
|
|
960
|
+
case "\\\\":
|
|
961
|
+
return "\\";
|
|
962
|
+
case '\\"':
|
|
963
|
+
return '"';
|
|
964
|
+
case "\\0":
|
|
965
|
+
return "\0";
|
|
966
|
+
default:
|
|
967
|
+
return match;
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
}
|
|
892
971
|
var SessionStreamer = class {
|
|
893
972
|
active = /* @__PURE__ */ new Map();
|
|
894
973
|
pendingStops = /* @__PURE__ */ new Map();
|
|
@@ -946,6 +1025,9 @@ var SessionStreamer = class {
|
|
|
946
1025
|
if (sessionId.startsWith("zellij:")) {
|
|
947
1026
|
return this.startZellijStream(sessionId);
|
|
948
1027
|
}
|
|
1028
|
+
if (sessionId.startsWith("process:")) {
|
|
1029
|
+
return this.startProcessStream(sessionId);
|
|
1030
|
+
}
|
|
949
1031
|
return false;
|
|
950
1032
|
}
|
|
951
1033
|
/**
|
|
@@ -1054,7 +1136,9 @@ var SessionStreamer = class {
|
|
|
1054
1136
|
initializing: true,
|
|
1055
1137
|
initBuffer: [],
|
|
1056
1138
|
cleanEnv,
|
|
1057
|
-
ptyProc: null
|
|
1139
|
+
ptyProc: null,
|
|
1140
|
+
straceProc: null,
|
|
1141
|
+
utf8Decoder: new import_node_string_decoder.StringDecoder("utf8")
|
|
1058
1142
|
};
|
|
1059
1143
|
this.active.set(sessionId, stream);
|
|
1060
1144
|
try {
|
|
@@ -1073,7 +1157,8 @@ var SessionStreamer = class {
|
|
|
1073
1157
|
stream.catProc = catProc;
|
|
1074
1158
|
catProc.stdout.on("data", (chunk) => {
|
|
1075
1159
|
if (stream.stopped) return;
|
|
1076
|
-
const data =
|
|
1160
|
+
const data = stream.utf8Decoder.write(chunk);
|
|
1161
|
+
if (!data) return;
|
|
1077
1162
|
if (stream.initializing) {
|
|
1078
1163
|
stream.initBuffer.push(data);
|
|
1079
1164
|
} else {
|
|
@@ -1082,6 +1167,8 @@ var SessionStreamer = class {
|
|
|
1082
1167
|
});
|
|
1083
1168
|
catProc.on("exit", () => {
|
|
1084
1169
|
if (!stream.stopped) {
|
|
1170
|
+
const remaining = stream.utf8Decoder.end();
|
|
1171
|
+
if (remaining) this.sendFn(sessionId, remaining);
|
|
1085
1172
|
this.cleanupStream(stream);
|
|
1086
1173
|
this.active.delete(sessionId);
|
|
1087
1174
|
this.pendingStops.delete(sessionId);
|
|
@@ -1162,7 +1249,9 @@ var SessionStreamer = class {
|
|
|
1162
1249
|
catProc: null,
|
|
1163
1250
|
initializing: false,
|
|
1164
1251
|
initBuffer: [],
|
|
1165
|
-
ptyProc: proc
|
|
1252
|
+
ptyProc: proc,
|
|
1253
|
+
straceProc: null,
|
|
1254
|
+
utf8Decoder: new import_node_string_decoder.StringDecoder("utf8")
|
|
1166
1255
|
};
|
|
1167
1256
|
this.active.set(sessionId, stream);
|
|
1168
1257
|
proc.onData((data) => {
|
|
@@ -1211,7 +1300,9 @@ var SessionStreamer = class {
|
|
|
1211
1300
|
catProc: null,
|
|
1212
1301
|
initializing: false,
|
|
1213
1302
|
initBuffer: [],
|
|
1214
|
-
ptyProc: proc
|
|
1303
|
+
ptyProc: proc,
|
|
1304
|
+
straceProc: null,
|
|
1305
|
+
utf8Decoder: new import_node_string_decoder.StringDecoder("utf8")
|
|
1215
1306
|
};
|
|
1216
1307
|
this.active.set(sessionId, stream);
|
|
1217
1308
|
proc.onData((data) => {
|
|
@@ -1237,6 +1328,89 @@ var SessionStreamer = class {
|
|
|
1237
1328
|
}
|
|
1238
1329
|
}
|
|
1239
1330
|
// ---------------------------------------------------------------------------
|
|
1331
|
+
// process streaming (strace -p PID write interception)
|
|
1332
|
+
// ---------------------------------------------------------------------------
|
|
1333
|
+
startProcessStream(sessionId) {
|
|
1334
|
+
const pid = parseProcessPid(sessionId);
|
|
1335
|
+
if (!pid) return false;
|
|
1336
|
+
if (!(0, import_node_fs.existsSync)(`/proc/${pid}`)) {
|
|
1337
|
+
console.warn(`[rAgent] Process ${pid} does not exist (no /proc/${pid}).`);
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
try {
|
|
1341
|
+
const straceProc = (0, import_node_child_process2.spawn)(
|
|
1342
|
+
"strace",
|
|
1343
|
+
["-p", String(pid), "-e", "trace=write", "-e", "signal=none", "-s", "1000000", "-x"],
|
|
1344
|
+
{ stdio: ["ignore", "ignore", "pipe"] }
|
|
1345
|
+
);
|
|
1346
|
+
const stream = {
|
|
1347
|
+
sessionId,
|
|
1348
|
+
streamType: "process-trace",
|
|
1349
|
+
stopped: false,
|
|
1350
|
+
paneTarget: "",
|
|
1351
|
+
fifoPath: "",
|
|
1352
|
+
tmpDir: "",
|
|
1353
|
+
catProc: null,
|
|
1354
|
+
initializing: false,
|
|
1355
|
+
initBuffer: [],
|
|
1356
|
+
ptyProc: null,
|
|
1357
|
+
straceProc,
|
|
1358
|
+
utf8Decoder: new import_node_string_decoder.StringDecoder("utf8")
|
|
1359
|
+
};
|
|
1360
|
+
this.active.set(sessionId, stream);
|
|
1361
|
+
const writeRegex = /^write\(([12]),\s*"((?:[^"\\]|\\.)*)"/;
|
|
1362
|
+
let lineBuf = "";
|
|
1363
|
+
straceProc.stderr.on("data", (chunk) => {
|
|
1364
|
+
if (stream.stopped) return;
|
|
1365
|
+
lineBuf += chunk.toString("utf-8");
|
|
1366
|
+
const lines = lineBuf.split("\n");
|
|
1367
|
+
lineBuf = lines.pop() ?? "";
|
|
1368
|
+
for (const line of lines) {
|
|
1369
|
+
const match = writeRegex.exec(line.trim());
|
|
1370
|
+
if (!match) continue;
|
|
1371
|
+
const escaped = match[2];
|
|
1372
|
+
const unescaped = unescapeStrace(escaped);
|
|
1373
|
+
if (unescaped) {
|
|
1374
|
+
const decoded = stream.utf8Decoder.write(Buffer.from(unescaped, "binary"));
|
|
1375
|
+
if (decoded) {
|
|
1376
|
+
this.sendFn(sessionId, decoded);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
straceProc.on("error", (err) => {
|
|
1382
|
+
if (stream.stopped) return;
|
|
1383
|
+
const message = err.message;
|
|
1384
|
+
if (message.includes("ENOENT")) {
|
|
1385
|
+
this.sendFn(sessionId, "\r\n[rAgent] strace is not installed. Install it to enable process streaming:\r\n sudo apt install strace (Debian/Ubuntu)\r\n sudo dnf install strace (Fedora/RHEL)\r\n");
|
|
1386
|
+
}
|
|
1387
|
+
stream.stopped = true;
|
|
1388
|
+
this.active.delete(sessionId);
|
|
1389
|
+
this.pendingStops.delete(sessionId);
|
|
1390
|
+
this.onStreamStopped?.(sessionId);
|
|
1391
|
+
});
|
|
1392
|
+
straceProc.on("exit", (code) => {
|
|
1393
|
+
if (stream.stopped) return;
|
|
1394
|
+
const remaining = stream.utf8Decoder.end();
|
|
1395
|
+
if (remaining) this.sendFn(sessionId, remaining);
|
|
1396
|
+
if (code === 1) {
|
|
1397
|
+
this.sendFn(sessionId, "\r\n[rAgent] Permission denied: cannot attach to process.\r\nTo enable process tracing, run:\r\n sudo sysctl kernel.yama.ptrace_scope=0\r\n");
|
|
1398
|
+
}
|
|
1399
|
+
stream.stopped = true;
|
|
1400
|
+
this.active.delete(sessionId);
|
|
1401
|
+
this.pendingStops.delete(sessionId);
|
|
1402
|
+
console.log(`[rAgent] Session stream ended: ${sessionId}`);
|
|
1403
|
+
this.onStreamStopped?.(sessionId);
|
|
1404
|
+
});
|
|
1405
|
+
console.log(`[rAgent] Started streaming: ${sessionId} (strace PID: ${pid})`);
|
|
1406
|
+
return true;
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1409
|
+
console.warn(`[rAgent] Failed to start process stream for ${sessionId}: ${message}`);
|
|
1410
|
+
return false;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
// ---------------------------------------------------------------------------
|
|
1240
1414
|
// Cleanup
|
|
1241
1415
|
// ---------------------------------------------------------------------------
|
|
1242
1416
|
cleanupStream(stream) {
|
|
@@ -1269,6 +1443,14 @@ var SessionStreamer = class {
|
|
|
1269
1443
|
}
|
|
1270
1444
|
stream.ptyProc = null;
|
|
1271
1445
|
}
|
|
1446
|
+
} else if (stream.streamType === "process-trace") {
|
|
1447
|
+
if (stream.straceProc && !stream.straceProc.killed) {
|
|
1448
|
+
try {
|
|
1449
|
+
stream.straceProc.kill("SIGTERM");
|
|
1450
|
+
} catch {
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
stream.straceProc = null;
|
|
1272
1454
|
}
|
|
1273
1455
|
}
|
|
1274
1456
|
};
|
|
@@ -1660,6 +1842,7 @@ var ConnectionManager = class {
|
|
|
1660
1842
|
|
|
1661
1843
|
// src/control-dispatcher.ts
|
|
1662
1844
|
var crypto2 = __toESM(require("crypto"));
|
|
1845
|
+
var import_child_process4 = require("child_process");
|
|
1663
1846
|
var import_ws4 = __toESM(require("ws"));
|
|
1664
1847
|
|
|
1665
1848
|
// src/service.ts
|
|
@@ -2197,7 +2380,8 @@ var ControlDispatcher = class {
|
|
|
2197
2380
|
"restart-shell",
|
|
2198
2381
|
"stop-session",
|
|
2199
2382
|
"stop-detached",
|
|
2200
|
-
"disconnect"
|
|
2383
|
+
"disconnect",
|
|
2384
|
+
"kill-process"
|
|
2201
2385
|
]);
|
|
2202
2386
|
if (dangerousActions.has(action) && this.connection.sessionKey) {
|
|
2203
2387
|
if (!this.verifyMessageHmac(payload)) {
|
|
@@ -2244,6 +2428,25 @@ var ControlDispatcher = class {
|
|
|
2244
2428
|
await this.syncInventory();
|
|
2245
2429
|
}
|
|
2246
2430
|
return;
|
|
2431
|
+
case "kill-process":
|
|
2432
|
+
if (!sessionId) return;
|
|
2433
|
+
{
|
|
2434
|
+
const pid = parseProcessPid(sessionId);
|
|
2435
|
+
if (pid === null) {
|
|
2436
|
+
console.warn(`[rAgent] kill-process: could not parse PID from ${sessionId}`);
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
this.streamer.stopStream(sessionId);
|
|
2440
|
+
try {
|
|
2441
|
+
process.kill(pid, "SIGTERM");
|
|
2442
|
+
console.log(`[rAgent] Sent SIGTERM to process ${pid} (${sessionId}).`);
|
|
2443
|
+
} catch (error) {
|
|
2444
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2445
|
+
console.warn(`[rAgent] Failed to kill process ${pid}: ${message}`);
|
|
2446
|
+
}
|
|
2447
|
+
await this.syncInventory();
|
|
2448
|
+
}
|
|
2449
|
+
return;
|
|
2247
2450
|
case "stop-detached": {
|
|
2248
2451
|
const killed = await stopAllDetachedTmuxSessions();
|
|
2249
2452
|
console.log(`[rAgent] Killed ${killed} detached tmux session(s).`);
|
|
@@ -2277,10 +2480,28 @@ var ControlDispatcher = class {
|
|
|
2277
2480
|
handleResize(cols, rows, sessionId) {
|
|
2278
2481
|
if (!sessionId || sessionId.startsWith("pty:")) {
|
|
2279
2482
|
this.shell.resize(cols, rows);
|
|
2483
|
+
} else if (sessionId.startsWith("tmux:")) {
|
|
2484
|
+
this.resizeTmuxPane(sessionId, cols, rows);
|
|
2280
2485
|
} else if (sessionId.startsWith("screen:") || sessionId.startsWith("zellij:")) {
|
|
2281
2486
|
this.streamer.resize(sessionId, cols, rows);
|
|
2282
2487
|
}
|
|
2283
2488
|
}
|
|
2489
|
+
/** Resize a tmux pane to match the viewer's terminal dimensions. */
|
|
2490
|
+
resizeTmuxPane(sessionId, cols, rows) {
|
|
2491
|
+
const paneTarget = sessionId.slice("tmux:".length);
|
|
2492
|
+
if (!paneTarget) return;
|
|
2493
|
+
const cleanEnv = { ...process.env };
|
|
2494
|
+
delete cleanEnv.TMUX;
|
|
2495
|
+
delete cleanEnv.TMUX_PANE;
|
|
2496
|
+
try {
|
|
2497
|
+
(0, import_child_process4.execFileSync)("tmux", ["resize-pane", "-t", paneTarget, "-x", String(cols), "-y", String(rows)], {
|
|
2498
|
+
env: cleanEnv,
|
|
2499
|
+
timeout: 5e3,
|
|
2500
|
+
stdio: "ignore"
|
|
2501
|
+
});
|
|
2502
|
+
} catch {
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2284
2505
|
/** Handle provision request from dashboard. */
|
|
2285
2506
|
async handleProvision(payload) {
|
|
2286
2507
|
const provReq = payload;
|
|
@@ -2344,8 +2565,7 @@ var ControlDispatcher = class {
|
|
|
2344
2565
|
}
|
|
2345
2566
|
tmuxArgs.push(fullCmd);
|
|
2346
2567
|
try {
|
|
2347
|
-
|
|
2348
|
-
execFileSync3("tmux", tmuxArgs, { stdio: "ignore" });
|
|
2568
|
+
(0, import_child_process4.execFileSync)("tmux", tmuxArgs, { stdio: "ignore" });
|
|
2349
2569
|
console.log(`[rAgent] Started agent session "${sessionName}": ${cmd}`);
|
|
2350
2570
|
} catch (error) {
|
|
2351
2571
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2357,17 +2577,7 @@ var ControlDispatcher = class {
|
|
|
2357
2577
|
if (!sessionId) return;
|
|
2358
2578
|
const ws = this.connection.activeSocket;
|
|
2359
2579
|
const group = this.connection.activeGroups.privateGroup;
|
|
2360
|
-
if (sessionId.startsWith("process:")) {
|
|
2361
|
-
if (ws && ws.readyState === import_ws4.default.OPEN && group) {
|
|
2362
|
-
sendToGroup(ws, group, {
|
|
2363
|
-
type: "stream-error",
|
|
2364
|
-
sessionId,
|
|
2365
|
-
error: "This agent is running outside a terminal multiplexer. Stop and relaunch via Start Agent to enable live streaming."
|
|
2366
|
-
});
|
|
2367
|
-
}
|
|
2368
|
-
return;
|
|
2369
|
-
}
|
|
2370
|
-
if (sessionId.startsWith("tmux:") || sessionId.startsWith("screen:") || sessionId.startsWith("zellij:")) {
|
|
2580
|
+
if (sessionId.startsWith("tmux:") || sessionId.startsWith("screen:") || sessionId.startsWith("zellij:") || sessionId.startsWith("process:")) {
|
|
2371
2581
|
const started = this.streamer.startStream(sessionId);
|
|
2372
2582
|
if (ws && ws.readyState === import_ws4.default.OPEN && group) {
|
|
2373
2583
|
if (started) {
|