ragent-cli 1.4.2 → 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.
Files changed (2) hide show
  1. package/dist/index.js +219 -27
  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.4.2",
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
- const sanitized = sanitizePayload(data);
842
- ws.send(
843
- JSON.stringify({
844
- type: "sendToGroup",
845
- group,
846
- dataType: "json",
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 = chunk.toString("utf-8");
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
  };
@@ -2198,7 +2380,8 @@ var ControlDispatcher = class {
2198
2380
  "restart-shell",
2199
2381
  "stop-session",
2200
2382
  "stop-detached",
2201
- "disconnect"
2383
+ "disconnect",
2384
+ "kill-process"
2202
2385
  ]);
2203
2386
  if (dangerousActions.has(action) && this.connection.sessionKey) {
2204
2387
  if (!this.verifyMessageHmac(payload)) {
@@ -2245,6 +2428,25 @@ var ControlDispatcher = class {
2245
2428
  await this.syncInventory();
2246
2429
  }
2247
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;
2248
2450
  case "stop-detached": {
2249
2451
  const killed = await stopAllDetachedTmuxSessions();
2250
2452
  console.log(`[rAgent] Killed ${killed} detached tmux session(s).`);
@@ -2375,17 +2577,7 @@ var ControlDispatcher = class {
2375
2577
  if (!sessionId) return;
2376
2578
  const ws = this.connection.activeSocket;
2377
2579
  const group = this.connection.activeGroups.privateGroup;
2378
- if (sessionId.startsWith("process:")) {
2379
- if (ws && ws.readyState === import_ws4.default.OPEN && group) {
2380
- sendToGroup(ws, group, {
2381
- type: "stream-error",
2382
- sessionId,
2383
- error: "This agent is running outside a terminal multiplexer. Stop and relaunch via Start Agent to enable live streaming."
2384
- });
2385
- }
2386
- return;
2387
- }
2388
- if (sessionId.startsWith("tmux:") || sessionId.startsWith("screen:") || sessionId.startsWith("zellij:")) {
2580
+ if (sessionId.startsWith("tmux:") || sessionId.startsWith("screen:") || sessionId.startsWith("zellij:") || sessionId.startsWith("process:")) {
2389
2581
  const started = this.streamer.startStream(sessionId);
2390
2582
  if (ws && ws.readyState === import_ws4.default.OPEN && group) {
2391
2583
  if (started) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ragent-cli",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
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": {