ragent-cli 1.2.0 → 1.3.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 +372 -59
- package/package.json +13 -4
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.3.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: {
|
|
@@ -41,7 +41,11 @@ var require_package = __commonJS({
|
|
|
41
41
|
build: "tsup",
|
|
42
42
|
dev: "tsup --watch",
|
|
43
43
|
test: "vitest run",
|
|
44
|
-
typecheck: "tsc --noEmit"
|
|
44
|
+
typecheck: "tsc --noEmit",
|
|
45
|
+
lint: "eslint src/",
|
|
46
|
+
changeset: "changeset",
|
|
47
|
+
"version-packages": "changeset version",
|
|
48
|
+
release: "npm run build && npm run test && changeset publish"
|
|
45
49
|
},
|
|
46
50
|
keywords: [
|
|
47
51
|
"terminal",
|
|
@@ -81,12 +85,17 @@ var require_package = __commonJS({
|
|
|
81
85
|
ws: "^8.19.0"
|
|
82
86
|
},
|
|
83
87
|
devDependencies: {
|
|
88
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
89
|
+
"@changesets/cli": "^2.29.8",
|
|
90
|
+
"@eslint/js": "^10.0.1",
|
|
84
91
|
"@types/figlet": "^1.7.0",
|
|
85
|
-
"@types/node": "^
|
|
92
|
+
"@types/node": "^25.3.0",
|
|
86
93
|
"@types/ws": "^8.5.13",
|
|
94
|
+
eslint: "^10.0.2",
|
|
87
95
|
tsup: "^8.4.0",
|
|
88
96
|
typescript: "^5.7.0",
|
|
89
|
-
|
|
97
|
+
"typescript-eslint": "^8.56.1",
|
|
98
|
+
vitest: "^4.0.18"
|
|
90
99
|
}
|
|
91
100
|
};
|
|
92
101
|
}
|
|
@@ -509,11 +518,11 @@ function detectAgentType(command) {
|
|
|
509
518
|
const parts = cmd.split(/\s+/);
|
|
510
519
|
const binary = parts[0]?.split("/").pop() ?? "";
|
|
511
520
|
const scriptArg = binary === "node" && parts[1] ? parts[1].split("/").pop() ?? "" : "";
|
|
512
|
-
if (binary === "claude" || scriptArg === "cli.js" && cmd.includes("claude-code")) {
|
|
521
|
+
if (binary === "claude" || binary === "claude-code" || scriptArg === "cli.js" && cmd.includes("claude-code")) {
|
|
513
522
|
if (cmd.includes("--chrome-native-host")) return void 0;
|
|
514
523
|
return "Claude Code";
|
|
515
524
|
}
|
|
516
|
-
if (binary === "codex" || cmd.includes("codex-cli")) return "Codex CLI";
|
|
525
|
+
if (binary === "codex" || scriptArg === "codex" || cmd.includes("codex-cli")) return "Codex CLI";
|
|
517
526
|
if (binary === "aider") return "aider";
|
|
518
527
|
if (binary === "cursor") return "Cursor";
|
|
519
528
|
if (binary === "windsurf") return "Windsurf";
|
|
@@ -1103,22 +1112,66 @@ function requestStopSelfService() {
|
|
|
1103
1112
|
|
|
1104
1113
|
// src/websocket.ts
|
|
1105
1114
|
var import_ws = __toESM(require("ws"));
|
|
1115
|
+
var BACKPRESSURE_HIGH_WATER = 256 * 1024;
|
|
1116
|
+
function sanitizeForJson(str) {
|
|
1117
|
+
return str.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
|
|
1118
|
+
}
|
|
1106
1119
|
function sendToGroup(ws, group, data) {
|
|
1107
1120
|
if (!group || ws.readyState !== import_ws.default.OPEN) return;
|
|
1121
|
+
if (ws.bufferedAmount > BACKPRESSURE_HIGH_WATER) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const sanitized = sanitizePayload(data);
|
|
1108
1125
|
ws.send(
|
|
1109
1126
|
JSON.stringify({
|
|
1110
1127
|
type: "sendToGroup",
|
|
1111
1128
|
group,
|
|
1112
1129
|
dataType: "json",
|
|
1113
|
-
data,
|
|
1130
|
+
data: sanitized,
|
|
1114
1131
|
noEcho: true
|
|
1115
1132
|
})
|
|
1116
1133
|
);
|
|
1117
1134
|
}
|
|
1135
|
+
function sanitizePayload(obj) {
|
|
1136
|
+
const result = {};
|
|
1137
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1138
|
+
if (typeof value === "string") {
|
|
1139
|
+
result[key] = sanitizeForJson(value);
|
|
1140
|
+
} else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1141
|
+
result[key] = sanitizePayload(value);
|
|
1142
|
+
} else {
|
|
1143
|
+
result[key] = value;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return result;
|
|
1147
|
+
}
|
|
1118
1148
|
|
|
1119
1149
|
// src/session-streamer.ts
|
|
1150
|
+
var import_node_child_process3 = require("child_process");
|
|
1151
|
+
var import_node_fs = require("fs");
|
|
1152
|
+
var import_node_path = require("path");
|
|
1153
|
+
var import_node_os = require("os");
|
|
1120
1154
|
var pty2 = __toESM(require("node-pty"));
|
|
1121
1155
|
var STOP_DEBOUNCE_MS = 2e3;
|
|
1156
|
+
function parsePaneTarget(sessionId) {
|
|
1157
|
+
if (!sessionId.startsWith("tmux:")) return null;
|
|
1158
|
+
const rest = sessionId.slice("tmux:".length);
|
|
1159
|
+
if (!rest) return null;
|
|
1160
|
+
return rest;
|
|
1161
|
+
}
|
|
1162
|
+
function parseScreenSession(sessionId) {
|
|
1163
|
+
if (!sessionId.startsWith("screen:")) return null;
|
|
1164
|
+
const rest = sessionId.slice("screen:".length);
|
|
1165
|
+
if (!rest) return null;
|
|
1166
|
+
const name = rest.split(":")[0];
|
|
1167
|
+
return name || null;
|
|
1168
|
+
}
|
|
1169
|
+
function parseZellijSession(sessionId) {
|
|
1170
|
+
if (!sessionId.startsWith("zellij:")) return null;
|
|
1171
|
+
const rest = sessionId.slice("zellij:".length);
|
|
1172
|
+
if (!rest) return null;
|
|
1173
|
+
return rest.split(":")[0] || null;
|
|
1174
|
+
}
|
|
1122
1175
|
var SessionStreamer = class {
|
|
1123
1176
|
active = /* @__PURE__ */ new Map();
|
|
1124
1177
|
pendingStops = /* @__PURE__ */ new Map();
|
|
@@ -1129,7 +1182,7 @@ var SessionStreamer = class {
|
|
|
1129
1182
|
this.onStreamStopped = onStreamStopped;
|
|
1130
1183
|
}
|
|
1131
1184
|
/**
|
|
1132
|
-
* Start streaming a
|
|
1185
|
+
* Start streaming a session. Dispatches based on session type prefix.
|
|
1133
1186
|
*/
|
|
1134
1187
|
startStream(sessionId) {
|
|
1135
1188
|
const pendingStop = this.pendingStops.get(sessionId);
|
|
@@ -1144,39 +1197,29 @@ var SessionStreamer = class {
|
|
|
1144
1197
|
if (this.active.has(sessionId)) {
|
|
1145
1198
|
return true;
|
|
1146
1199
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
return false;
|
|
1200
|
+
if (sessionId.startsWith("tmux:")) {
|
|
1201
|
+
return this.startTmuxStream(sessionId);
|
|
1150
1202
|
}
|
|
1151
|
-
|
|
1203
|
+
if (sessionId.startsWith("screen:")) {
|
|
1204
|
+
return this.startScreenStream(sessionId);
|
|
1205
|
+
}
|
|
1206
|
+
if (sessionId.startsWith("zellij:")) {
|
|
1207
|
+
return this.startZellijStream(sessionId);
|
|
1208
|
+
}
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Write input to a PTY-attached stream (screen/zellij).
|
|
1213
|
+
* tmux input is handled separately via sendInputToTmux.
|
|
1214
|
+
*/
|
|
1215
|
+
writeInput(sessionId, data) {
|
|
1216
|
+
const stream = this.active.get(sessionId);
|
|
1217
|
+
if (!stream || stream.stopped || stream.streamType !== "pty-attach" || !stream.ptyProc) return;
|
|
1152
1218
|
try {
|
|
1153
|
-
|
|
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;
|
|
1219
|
+
stream.ptyProc.write(data);
|
|
1176
1220
|
} catch (error) {
|
|
1177
1221
|
const message = error instanceof Error ? error.message : String(error);
|
|
1178
|
-
console.warn(`[rAgent] Failed to
|
|
1179
|
-
return false;
|
|
1222
|
+
console.warn(`[rAgent] Failed to write input to ${sessionId}: ${message}`);
|
|
1180
1223
|
}
|
|
1181
1224
|
}
|
|
1182
1225
|
/**
|
|
@@ -1190,10 +1233,7 @@ var SessionStreamer = class {
|
|
|
1190
1233
|
this.pendingStops.delete(sessionId);
|
|
1191
1234
|
const s = this.active.get(sessionId);
|
|
1192
1235
|
if (!s) return;
|
|
1193
|
-
|
|
1194
|
-
s.pty.kill();
|
|
1195
|
-
} catch {
|
|
1196
|
-
}
|
|
1236
|
+
this.cleanupStream(s);
|
|
1197
1237
|
this.active.delete(sessionId);
|
|
1198
1238
|
console.log(`[rAgent] Stopped streaming: ${sessionId}`);
|
|
1199
1239
|
}, STOP_DEBOUNCE_MS);
|
|
@@ -1214,10 +1254,7 @@ var SessionStreamer = class {
|
|
|
1214
1254
|
}
|
|
1215
1255
|
this.pendingStops.clear();
|
|
1216
1256
|
for (const [id, stream] of this.active) {
|
|
1217
|
-
|
|
1218
|
-
stream.pty.kill();
|
|
1219
|
-
} catch {
|
|
1220
|
-
}
|
|
1257
|
+
this.cleanupStream(stream);
|
|
1221
1258
|
console.log(`[rAgent] Stopped streaming: ${id}`);
|
|
1222
1259
|
}
|
|
1223
1260
|
this.active.clear();
|
|
@@ -1228,6 +1265,237 @@ var SessionStreamer = class {
|
|
|
1228
1265
|
get activeCount() {
|
|
1229
1266
|
return this.active.size;
|
|
1230
1267
|
}
|
|
1268
|
+
// ---------------------------------------------------------------------------
|
|
1269
|
+
// tmux streaming (pipe-pane with cursor sync)
|
|
1270
|
+
// ---------------------------------------------------------------------------
|
|
1271
|
+
startTmuxStream(sessionId) {
|
|
1272
|
+
const paneTarget = parsePaneTarget(sessionId);
|
|
1273
|
+
if (!paneTarget) return false;
|
|
1274
|
+
try {
|
|
1275
|
+
const cleanEnv = { ...process.env };
|
|
1276
|
+
delete cleanEnv.TMUX;
|
|
1277
|
+
delete cleanEnv.TMUX_PANE;
|
|
1278
|
+
const tmpDir = (0, import_node_fs.mkdtempSync)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "ragent-stream-"));
|
|
1279
|
+
const fifoPath = (0, import_node_path.join)(tmpDir, "pane.fifo");
|
|
1280
|
+
(0, import_node_child_process3.execFileSync)("mkfifo", ["-m", "600", fifoPath]);
|
|
1281
|
+
const stream = {
|
|
1282
|
+
sessionId,
|
|
1283
|
+
streamType: "tmux-pipe",
|
|
1284
|
+
paneTarget,
|
|
1285
|
+
fifoPath,
|
|
1286
|
+
tmpDir,
|
|
1287
|
+
catProc: null,
|
|
1288
|
+
stopped: false,
|
|
1289
|
+
initializing: true,
|
|
1290
|
+
initBuffer: [],
|
|
1291
|
+
ptyProc: null
|
|
1292
|
+
};
|
|
1293
|
+
this.active.set(sessionId, stream);
|
|
1294
|
+
try {
|
|
1295
|
+
(0, import_node_child_process3.execFileSync)(
|
|
1296
|
+
"tmux",
|
|
1297
|
+
["pipe-pane", "-O", "-t", paneTarget, `cat > ${fifoPath}`],
|
|
1298
|
+
{ env: cleanEnv, timeout: 5e3 }
|
|
1299
|
+
);
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
this.cleanupStream(stream);
|
|
1302
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1303
|
+
console.warn(`[rAgent] Failed pipe-pane for ${sessionId}: ${message}`);
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
const catProc = (0, import_node_child_process3.spawn)("cat", [fifoPath], { env: cleanEnv, stdio: ["ignore", "pipe", "ignore"] });
|
|
1307
|
+
stream.catProc = catProc;
|
|
1308
|
+
catProc.stdout.on("data", (chunk) => {
|
|
1309
|
+
if (stream.stopped) return;
|
|
1310
|
+
const data = chunk.toString("utf-8");
|
|
1311
|
+
if (stream.initializing) {
|
|
1312
|
+
stream.initBuffer.push(data);
|
|
1313
|
+
} else {
|
|
1314
|
+
this.sendFn(sessionId, data);
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
catProc.on("exit", () => {
|
|
1318
|
+
if (!stream.stopped) {
|
|
1319
|
+
this.cleanupStream(stream);
|
|
1320
|
+
this.active.delete(sessionId);
|
|
1321
|
+
this.pendingStops.delete(sessionId);
|
|
1322
|
+
console.log(`[rAgent] Session stream ended: ${sessionId}`);
|
|
1323
|
+
this.onStreamStopped?.(sessionId);
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
this.sendFn(sessionId, "\x1B[2J\x1B[H");
|
|
1327
|
+
try {
|
|
1328
|
+
const initial = (0, import_node_child_process3.execFileSync)(
|
|
1329
|
+
"tmux",
|
|
1330
|
+
["capture-pane", "-t", paneTarget, "-p", "-e"],
|
|
1331
|
+
{ env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
|
|
1332
|
+
);
|
|
1333
|
+
if (initial) {
|
|
1334
|
+
this.sendFn(sessionId, initial);
|
|
1335
|
+
}
|
|
1336
|
+
} catch {
|
|
1337
|
+
}
|
|
1338
|
+
try {
|
|
1339
|
+
const cursorInfo = (0, import_node_child_process3.execFileSync)(
|
|
1340
|
+
"tmux",
|
|
1341
|
+
["display-message", "-t", paneTarget, "-p", "#{cursor_x} #{cursor_y}"],
|
|
1342
|
+
{ env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
|
|
1343
|
+
).trim();
|
|
1344
|
+
const parts = cursorInfo.split(" ");
|
|
1345
|
+
if (parts.length === 2) {
|
|
1346
|
+
const x = parseInt(parts[0], 10);
|
|
1347
|
+
const y = parseInt(parts[1], 10);
|
|
1348
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
1349
|
+
this.sendFn(sessionId, `\x1B[${y + 1};${x + 1}H`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
} catch {
|
|
1353
|
+
}
|
|
1354
|
+
stream.initializing = false;
|
|
1355
|
+
if (stream.initBuffer.length > 0) {
|
|
1356
|
+
for (const buffered of stream.initBuffer) {
|
|
1357
|
+
this.sendFn(sessionId, buffered);
|
|
1358
|
+
}
|
|
1359
|
+
stream.initBuffer = [];
|
|
1360
|
+
}
|
|
1361
|
+
console.log(`[rAgent] Started streaming: ${sessionId} (pane: ${paneTarget})`);
|
|
1362
|
+
return true;
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1365
|
+
console.warn(`[rAgent] Failed to start stream for ${sessionId}: ${message}`);
|
|
1366
|
+
return false;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
// ---------------------------------------------------------------------------
|
|
1370
|
+
// screen streaming (screen -x in node-pty)
|
|
1371
|
+
// ---------------------------------------------------------------------------
|
|
1372
|
+
startScreenStream(sessionId) {
|
|
1373
|
+
const sessionName = parseScreenSession(sessionId);
|
|
1374
|
+
if (!sessionName) return false;
|
|
1375
|
+
try {
|
|
1376
|
+
const proc = pty2.spawn("screen", ["-x", sessionName], {
|
|
1377
|
+
name: "xterm-256color",
|
|
1378
|
+
cols: 80,
|
|
1379
|
+
rows: 30,
|
|
1380
|
+
cwd: process.cwd(),
|
|
1381
|
+
env: process.env
|
|
1382
|
+
});
|
|
1383
|
+
const stream = {
|
|
1384
|
+
sessionId,
|
|
1385
|
+
streamType: "pty-attach",
|
|
1386
|
+
stopped: false,
|
|
1387
|
+
paneTarget: "",
|
|
1388
|
+
fifoPath: "",
|
|
1389
|
+
tmpDir: "",
|
|
1390
|
+
catProc: null,
|
|
1391
|
+
initializing: false,
|
|
1392
|
+
initBuffer: [],
|
|
1393
|
+
ptyProc: proc
|
|
1394
|
+
};
|
|
1395
|
+
this.active.set(sessionId, stream);
|
|
1396
|
+
proc.onData((data) => {
|
|
1397
|
+
if (!stream.stopped) {
|
|
1398
|
+
this.sendFn(sessionId, data);
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
proc.onExit(() => {
|
|
1402
|
+
if (!stream.stopped) {
|
|
1403
|
+
stream.stopped = true;
|
|
1404
|
+
this.active.delete(sessionId);
|
|
1405
|
+
this.pendingStops.delete(sessionId);
|
|
1406
|
+
console.log(`[rAgent] Session stream ended: ${sessionId}`);
|
|
1407
|
+
this.onStreamStopped?.(sessionId);
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
console.log(`[rAgent] Started streaming: ${sessionId} (screen: ${sessionName})`);
|
|
1411
|
+
return true;
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1414
|
+
console.warn(`[rAgent] Failed to start screen stream for ${sessionId}: ${message}`);
|
|
1415
|
+
return false;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
// ---------------------------------------------------------------------------
|
|
1419
|
+
// zellij streaming (zellij attach in node-pty)
|
|
1420
|
+
// ---------------------------------------------------------------------------
|
|
1421
|
+
startZellijStream(sessionId) {
|
|
1422
|
+
const sessionName = parseZellijSession(sessionId);
|
|
1423
|
+
if (!sessionName) return false;
|
|
1424
|
+
try {
|
|
1425
|
+
const proc = pty2.spawn("zellij", ["attach", sessionName], {
|
|
1426
|
+
name: "xterm-256color",
|
|
1427
|
+
cols: 80,
|
|
1428
|
+
rows: 30,
|
|
1429
|
+
cwd: process.cwd(),
|
|
1430
|
+
env: process.env
|
|
1431
|
+
});
|
|
1432
|
+
const stream = {
|
|
1433
|
+
sessionId,
|
|
1434
|
+
streamType: "pty-attach",
|
|
1435
|
+
stopped: false,
|
|
1436
|
+
paneTarget: "",
|
|
1437
|
+
fifoPath: "",
|
|
1438
|
+
tmpDir: "",
|
|
1439
|
+
catProc: null,
|
|
1440
|
+
initializing: false,
|
|
1441
|
+
initBuffer: [],
|
|
1442
|
+
ptyProc: proc
|
|
1443
|
+
};
|
|
1444
|
+
this.active.set(sessionId, stream);
|
|
1445
|
+
proc.onData((data) => {
|
|
1446
|
+
if (!stream.stopped) {
|
|
1447
|
+
this.sendFn(sessionId, data);
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
proc.onExit(() => {
|
|
1451
|
+
if (!stream.stopped) {
|
|
1452
|
+
stream.stopped = true;
|
|
1453
|
+
this.active.delete(sessionId);
|
|
1454
|
+
this.pendingStops.delete(sessionId);
|
|
1455
|
+
console.log(`[rAgent] Session stream ended: ${sessionId}`);
|
|
1456
|
+
this.onStreamStopped?.(sessionId);
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
console.log(`[rAgent] Started streaming: ${sessionId} (zellij: ${sessionName})`);
|
|
1460
|
+
return true;
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1463
|
+
console.warn(`[rAgent] Failed to start zellij stream for ${sessionId}: ${message}`);
|
|
1464
|
+
return false;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
// ---------------------------------------------------------------------------
|
|
1468
|
+
// Cleanup
|
|
1469
|
+
// ---------------------------------------------------------------------------
|
|
1470
|
+
cleanupStream(stream) {
|
|
1471
|
+
stream.stopped = true;
|
|
1472
|
+
if (stream.streamType === "tmux-pipe") {
|
|
1473
|
+
try {
|
|
1474
|
+
(0, import_node_child_process3.execFileSync)("tmux", ["pipe-pane", "-t", stream.paneTarget], { timeout: 5e3 });
|
|
1475
|
+
} catch {
|
|
1476
|
+
}
|
|
1477
|
+
if (stream.catProc && !stream.catProc.killed) {
|
|
1478
|
+
try {
|
|
1479
|
+
stream.catProc.kill("SIGTERM");
|
|
1480
|
+
} catch {
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
try {
|
|
1484
|
+
if (stream.tmpDir && (0, import_node_fs.existsSync)(stream.tmpDir)) {
|
|
1485
|
+
(0, import_node_fs.rmSync)(stream.tmpDir, { recursive: true, force: true });
|
|
1486
|
+
}
|
|
1487
|
+
} catch {
|
|
1488
|
+
}
|
|
1489
|
+
} else if (stream.streamType === "pty-attach") {
|
|
1490
|
+
if (stream.ptyProc) {
|
|
1491
|
+
try {
|
|
1492
|
+
stream.ptyProc.kill();
|
|
1493
|
+
} catch {
|
|
1494
|
+
}
|
|
1495
|
+
stream.ptyProc = null;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1231
1499
|
};
|
|
1232
1500
|
|
|
1233
1501
|
// src/provisioner.ts
|
|
@@ -1511,6 +1779,8 @@ async function runAgent(rawOptions) {
|
|
|
1511
1779
|
let activeGroups = { privateGroup: "", registryGroup: "" };
|
|
1512
1780
|
let wsHeartbeatTimer = null;
|
|
1513
1781
|
let httpHeartbeatTimer = null;
|
|
1782
|
+
let wsPingTimer = null;
|
|
1783
|
+
let wsPongTimeout = null;
|
|
1514
1784
|
let suppressNextShellRespawn = false;
|
|
1515
1785
|
let lastSentFingerprint = "";
|
|
1516
1786
|
let lastHttpHeartbeatAt = 0;
|
|
@@ -1566,6 +1836,14 @@ async function runAgent(rawOptions) {
|
|
|
1566
1836
|
spawnOrRespawnShell();
|
|
1567
1837
|
const cleanupSocket = (opts = {}) => {
|
|
1568
1838
|
if (opts.stopStreams) sessionStreamer.stopAll();
|
|
1839
|
+
if (wsPingTimer) {
|
|
1840
|
+
clearInterval(wsPingTimer);
|
|
1841
|
+
wsPingTimer = null;
|
|
1842
|
+
}
|
|
1843
|
+
if (wsPongTimeout) {
|
|
1844
|
+
clearTimeout(wsPongTimeout);
|
|
1845
|
+
wsPongTimeout = null;
|
|
1846
|
+
}
|
|
1569
1847
|
if (wsHeartbeatTimer) {
|
|
1570
1848
|
clearInterval(wsHeartbeatTimer);
|
|
1571
1849
|
wsHeartbeatTimer = null;
|
|
@@ -1584,17 +1862,28 @@ async function runAgent(rawOptions) {
|
|
|
1584
1862
|
}
|
|
1585
1863
|
activeGroups = { privateGroup: "", registryGroup: "" };
|
|
1586
1864
|
};
|
|
1587
|
-
|
|
1865
|
+
let prevCpuSnapshot = null;
|
|
1866
|
+
function takeCpuSnapshot() {
|
|
1588
1867
|
const cpus2 = os5.cpus();
|
|
1589
|
-
let
|
|
1590
|
-
let
|
|
1868
|
+
let idle = 0;
|
|
1869
|
+
let total = 0;
|
|
1591
1870
|
for (const cpu of cpus2) {
|
|
1592
1871
|
for (const type of Object.keys(cpu.times)) {
|
|
1593
|
-
|
|
1872
|
+
total += cpu.times[type];
|
|
1594
1873
|
}
|
|
1595
|
-
|
|
1874
|
+
idle += cpu.times.idle;
|
|
1596
1875
|
}
|
|
1597
|
-
|
|
1876
|
+
return { idle, total };
|
|
1877
|
+
}
|
|
1878
|
+
const collectVitals = () => {
|
|
1879
|
+
const currentSnapshot = takeCpuSnapshot();
|
|
1880
|
+
let cpuUsage = 0;
|
|
1881
|
+
if (prevCpuSnapshot) {
|
|
1882
|
+
const idleDelta = currentSnapshot.idle - prevCpuSnapshot.idle;
|
|
1883
|
+
const totalDelta = currentSnapshot.total - prevCpuSnapshot.total;
|
|
1884
|
+
cpuUsage = totalDelta > 0 ? Math.round((totalDelta - idleDelta) / totalDelta * 100) : 0;
|
|
1885
|
+
}
|
|
1886
|
+
prevCpuSnapshot = currentSnapshot;
|
|
1598
1887
|
const totalMem = os5.totalmem();
|
|
1599
1888
|
const freeMem = os5.freemem();
|
|
1600
1889
|
const memUsedPct = totalMem > 0 ? Math.round((totalMem - freeMem) / totalMem * 100) : 0;
|
|
@@ -1725,8 +2014,8 @@ async function runAgent(rawOptions) {
|
|
|
1725
2014
|
}
|
|
1726
2015
|
tmuxArgs.push(fullCmd);
|
|
1727
2016
|
try {
|
|
1728
|
-
const { execFileSync:
|
|
1729
|
-
|
|
2017
|
+
const { execFileSync: execFileSync3 } = await import("child_process");
|
|
2018
|
+
execFileSync3("tmux", tmuxArgs, { stdio: "ignore" });
|
|
1730
2019
|
console.log(`[rAgent] Started agent session "${sessionName}": ${cmd}`);
|
|
1731
2020
|
} catch (error) {
|
|
1732
2021
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1743,12 +2032,12 @@ async function runAgent(rawOptions) {
|
|
|
1743
2032
|
sendToGroup(ws2, activeGroups.privateGroup, {
|
|
1744
2033
|
type: "stream-error",
|
|
1745
2034
|
sessionId,
|
|
1746
|
-
error: "
|
|
2035
|
+
error: "This agent is running outside a terminal multiplexer. Stop and relaunch via Start Agent to enable live streaming."
|
|
1747
2036
|
});
|
|
1748
2037
|
}
|
|
1749
2038
|
return;
|
|
1750
2039
|
}
|
|
1751
|
-
if (sessionId.startsWith("tmux:")) {
|
|
2040
|
+
if (sessionId.startsWith("tmux:") || sessionId.startsWith("screen:") || sessionId.startsWith("zellij:")) {
|
|
1752
2041
|
const started = sessionStreamer.startStream(sessionId);
|
|
1753
2042
|
const ws2 = activeSocket;
|
|
1754
2043
|
if (ws2 && ws2.readyState === import_ws2.default.OPEN && activeGroups.privateGroup) {
|
|
@@ -1761,7 +2050,7 @@ async function runAgent(rawOptions) {
|
|
|
1761
2050
|
sendToGroup(ws2, activeGroups.privateGroup, {
|
|
1762
2051
|
type: "stream-error",
|
|
1763
2052
|
sessionId,
|
|
1764
|
-
error: "Failed to attach to
|
|
2053
|
+
error: "Failed to attach to session. It may no longer exist."
|
|
1765
2054
|
});
|
|
1766
2055
|
}
|
|
1767
2056
|
}
|
|
@@ -1772,7 +2061,7 @@ async function runAgent(rawOptions) {
|
|
|
1772
2061
|
sendToGroup(ws, activeGroups.privateGroup, {
|
|
1773
2062
|
type: "stream-error",
|
|
1774
2063
|
sessionId,
|
|
1775
|
-
error:
|
|
2064
|
+
error: "Live streaming is not yet supported for this session type."
|
|
1776
2065
|
});
|
|
1777
2066
|
}
|
|
1778
2067
|
return;
|
|
@@ -1859,6 +2148,23 @@ async function runAgent(rawOptions) {
|
|
|
1859
2148
|
return;
|
|
1860
2149
|
await syncInventory();
|
|
1861
2150
|
}, HTTP_HEARTBEAT_MS);
|
|
2151
|
+
wsPingTimer = setInterval(() => {
|
|
2152
|
+
if (!activeSocket || activeSocket.readyState !== import_ws2.default.OPEN) return;
|
|
2153
|
+
activeSocket.ping();
|
|
2154
|
+
wsPongTimeout = setTimeout(() => {
|
|
2155
|
+
console.warn("[rAgent] No pong received within 10s \u2014 closing stale connection.");
|
|
2156
|
+
try {
|
|
2157
|
+
activeSocket?.terminate();
|
|
2158
|
+
} catch {
|
|
2159
|
+
}
|
|
2160
|
+
}, 1e4);
|
|
2161
|
+
}, 2e4);
|
|
2162
|
+
});
|
|
2163
|
+
ws.on("pong", () => {
|
|
2164
|
+
if (wsPongTimeout) {
|
|
2165
|
+
clearTimeout(wsPongTimeout);
|
|
2166
|
+
wsPongTimeout = null;
|
|
2167
|
+
}
|
|
1862
2168
|
});
|
|
1863
2169
|
ws.on("message", async (data) => {
|
|
1864
2170
|
let msg;
|
|
@@ -1875,6 +2181,8 @@ async function runAgent(rawOptions) {
|
|
|
1875
2181
|
if (ptyProcess) ptyProcess.write(payload.data);
|
|
1876
2182
|
} else if (sid.startsWith("tmux:")) {
|
|
1877
2183
|
await sendInputToTmux(sid, payload.data);
|
|
2184
|
+
} else if (sid.startsWith("screen:") || sid.startsWith("zellij:")) {
|
|
2185
|
+
sessionStreamer.writeInput(sid, payload.data);
|
|
1878
2186
|
}
|
|
1879
2187
|
} else if (payload.type === "resize" && Number.isInteger(payload.cols) && Number.isInteger(payload.rows)) {
|
|
1880
2188
|
const sid = typeof payload.sessionId === "string" ? payload.sessionId.trim() : "";
|
|
@@ -1960,10 +2268,11 @@ async function runAgent(rawOptions) {
|
|
|
1960
2268
|
await wait(300);
|
|
1961
2269
|
continue;
|
|
1962
2270
|
}
|
|
2271
|
+
const jitteredDelay = reconnectDelay * (0.5 + Math.random());
|
|
1963
2272
|
console.log(
|
|
1964
|
-
`[rAgent] Disconnected. Reconnecting in ${Math.round(
|
|
2273
|
+
`[rAgent] Disconnected. Reconnecting in ${Math.round(jitteredDelay / 1e3)}s...`
|
|
1965
2274
|
);
|
|
1966
|
-
await wait(
|
|
2275
|
+
await wait(jitteredDelay);
|
|
1967
2276
|
reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY_MS);
|
|
1968
2277
|
}
|
|
1969
2278
|
} finally {
|
|
@@ -2373,6 +2682,10 @@ function registerUninstallCommand(parent) {
|
|
|
2373
2682
|
}
|
|
2374
2683
|
|
|
2375
2684
|
// src/index.ts
|
|
2685
|
+
process.on("unhandledRejection", (reason) => {
|
|
2686
|
+
const message = reason instanceof Error ? reason.stack || reason.message : String(reason);
|
|
2687
|
+
console.error(`[rAgent] Unhandled promise rejection: ${message}`);
|
|
2688
|
+
});
|
|
2376
2689
|
import_commander.program.name("ragent").description("Connect machines to rAgent Live").version(CURRENT_VERSION);
|
|
2377
2690
|
registerConnectCommand(import_commander.program);
|
|
2378
2691
|
registerRunCommand(import_commander.program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ragent-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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": {
|
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
"build": "tsup",
|
|
11
11
|
"dev": "tsup --watch",
|
|
12
12
|
"test": "vitest run",
|
|
13
|
-
"typecheck": "tsc --noEmit"
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"lint": "eslint src/",
|
|
15
|
+
"changeset": "changeset",
|
|
16
|
+
"version-packages": "changeset version",
|
|
17
|
+
"release": "npm run build && npm run test && changeset publish"
|
|
14
18
|
},
|
|
15
19
|
"keywords": [
|
|
16
20
|
"terminal",
|
|
@@ -50,11 +54,16 @@
|
|
|
50
54
|
"ws": "^8.19.0"
|
|
51
55
|
},
|
|
52
56
|
"devDependencies": {
|
|
57
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
58
|
+
"@changesets/cli": "^2.29.8",
|
|
59
|
+
"@eslint/js": "^10.0.1",
|
|
53
60
|
"@types/figlet": "^1.7.0",
|
|
54
|
-
"@types/node": "^
|
|
61
|
+
"@types/node": "^25.3.0",
|
|
55
62
|
"@types/ws": "^8.5.13",
|
|
63
|
+
"eslint": "^10.0.2",
|
|
56
64
|
"tsup": "^8.4.0",
|
|
57
65
|
"typescript": "^5.7.0",
|
|
58
|
-
"
|
|
66
|
+
"typescript-eslint": "^8.56.1",
|
|
67
|
+
"vitest": "^4.0.18"
|
|
59
68
|
}
|
|
60
69
|
}
|