ragent-cli 1.2.1 → 1.3.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 +382 -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.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: {
|
|
@@ -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(({ 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;
|
|
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,247 @@ 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 streamsBase = (0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "ragent", "streams");
|
|
1279
|
+
try {
|
|
1280
|
+
(0, import_node_fs.mkdirSync)(streamsBase, { recursive: true });
|
|
1281
|
+
} catch {
|
|
1282
|
+
}
|
|
1283
|
+
let tmpDir;
|
|
1284
|
+
try {
|
|
1285
|
+
tmpDir = (0, import_node_fs.mkdtempSync)((0, import_node_path.join)(streamsBase, "s-"));
|
|
1286
|
+
} catch {
|
|
1287
|
+
tmpDir = (0, import_node_fs.mkdtempSync)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "ragent-stream-"));
|
|
1288
|
+
}
|
|
1289
|
+
const fifoPath = (0, import_node_path.join)(tmpDir, "pane.fifo");
|
|
1290
|
+
(0, import_node_child_process3.execFileSync)("mkfifo", ["-m", "600", fifoPath]);
|
|
1291
|
+
const stream = {
|
|
1292
|
+
sessionId,
|
|
1293
|
+
streamType: "tmux-pipe",
|
|
1294
|
+
paneTarget,
|
|
1295
|
+
fifoPath,
|
|
1296
|
+
tmpDir,
|
|
1297
|
+
catProc: null,
|
|
1298
|
+
stopped: false,
|
|
1299
|
+
initializing: true,
|
|
1300
|
+
initBuffer: [],
|
|
1301
|
+
ptyProc: null
|
|
1302
|
+
};
|
|
1303
|
+
this.active.set(sessionId, stream);
|
|
1304
|
+
try {
|
|
1305
|
+
(0, import_node_child_process3.execFileSync)(
|
|
1306
|
+
"tmux",
|
|
1307
|
+
["pipe-pane", "-O", "-t", paneTarget, `cat > ${fifoPath}`],
|
|
1308
|
+
{ env: cleanEnv, timeout: 5e3 }
|
|
1309
|
+
);
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
this.cleanupStream(stream);
|
|
1312
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1313
|
+
console.warn(`[rAgent] Failed pipe-pane for ${sessionId}: ${message}`);
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
const catProc = (0, import_node_child_process3.spawn)("cat", [fifoPath], { env: cleanEnv, stdio: ["ignore", "pipe", "ignore"] });
|
|
1317
|
+
stream.catProc = catProc;
|
|
1318
|
+
catProc.stdout.on("data", (chunk) => {
|
|
1319
|
+
if (stream.stopped) return;
|
|
1320
|
+
const data = chunk.toString("utf-8");
|
|
1321
|
+
if (stream.initializing) {
|
|
1322
|
+
stream.initBuffer.push(data);
|
|
1323
|
+
} else {
|
|
1324
|
+
this.sendFn(sessionId, data);
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
catProc.on("exit", () => {
|
|
1328
|
+
if (!stream.stopped) {
|
|
1329
|
+
this.cleanupStream(stream);
|
|
1330
|
+
this.active.delete(sessionId);
|
|
1331
|
+
this.pendingStops.delete(sessionId);
|
|
1332
|
+
console.log(`[rAgent] Session stream ended: ${sessionId}`);
|
|
1333
|
+
this.onStreamStopped?.(sessionId);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
this.sendFn(sessionId, "\x1B[2J\x1B[H");
|
|
1337
|
+
try {
|
|
1338
|
+
const initial = (0, import_node_child_process3.execFileSync)(
|
|
1339
|
+
"tmux",
|
|
1340
|
+
["capture-pane", "-t", paneTarget, "-p", "-e"],
|
|
1341
|
+
{ env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
|
|
1342
|
+
);
|
|
1343
|
+
if (initial) {
|
|
1344
|
+
this.sendFn(sessionId, initial);
|
|
1345
|
+
}
|
|
1346
|
+
} catch {
|
|
1347
|
+
}
|
|
1348
|
+
try {
|
|
1349
|
+
const cursorInfo = (0, import_node_child_process3.execFileSync)(
|
|
1350
|
+
"tmux",
|
|
1351
|
+
["display-message", "-t", paneTarget, "-p", "#{cursor_x} #{cursor_y}"],
|
|
1352
|
+
{ env: cleanEnv, timeout: 5e3, encoding: "utf-8" }
|
|
1353
|
+
).trim();
|
|
1354
|
+
const parts = cursorInfo.split(" ");
|
|
1355
|
+
if (parts.length === 2) {
|
|
1356
|
+
const x = parseInt(parts[0], 10);
|
|
1357
|
+
const y = parseInt(parts[1], 10);
|
|
1358
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
1359
|
+
this.sendFn(sessionId, `\x1B[${y + 1};${x + 1}H`);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
} catch {
|
|
1363
|
+
}
|
|
1364
|
+
stream.initializing = false;
|
|
1365
|
+
if (stream.initBuffer.length > 0) {
|
|
1366
|
+
for (const buffered of stream.initBuffer) {
|
|
1367
|
+
this.sendFn(sessionId, buffered);
|
|
1368
|
+
}
|
|
1369
|
+
stream.initBuffer = [];
|
|
1370
|
+
}
|
|
1371
|
+
console.log(`[rAgent] Started streaming: ${sessionId} (pane: ${paneTarget})`);
|
|
1372
|
+
return true;
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1375
|
+
console.warn(`[rAgent] Failed to start stream for ${sessionId}: ${message}`);
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
// ---------------------------------------------------------------------------
|
|
1380
|
+
// screen streaming (screen -x in node-pty)
|
|
1381
|
+
// ---------------------------------------------------------------------------
|
|
1382
|
+
startScreenStream(sessionId) {
|
|
1383
|
+
const sessionName = parseScreenSession(sessionId);
|
|
1384
|
+
if (!sessionName) return false;
|
|
1385
|
+
try {
|
|
1386
|
+
const proc = pty2.spawn("screen", ["-x", sessionName], {
|
|
1387
|
+
name: "xterm-256color",
|
|
1388
|
+
cols: 80,
|
|
1389
|
+
rows: 30,
|
|
1390
|
+
cwd: process.cwd(),
|
|
1391
|
+
env: process.env
|
|
1392
|
+
});
|
|
1393
|
+
const stream = {
|
|
1394
|
+
sessionId,
|
|
1395
|
+
streamType: "pty-attach",
|
|
1396
|
+
stopped: false,
|
|
1397
|
+
paneTarget: "",
|
|
1398
|
+
fifoPath: "",
|
|
1399
|
+
tmpDir: "",
|
|
1400
|
+
catProc: null,
|
|
1401
|
+
initializing: false,
|
|
1402
|
+
initBuffer: [],
|
|
1403
|
+
ptyProc: proc
|
|
1404
|
+
};
|
|
1405
|
+
this.active.set(sessionId, stream);
|
|
1406
|
+
proc.onData((data) => {
|
|
1407
|
+
if (!stream.stopped) {
|
|
1408
|
+
this.sendFn(sessionId, data);
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
proc.onExit(() => {
|
|
1412
|
+
if (!stream.stopped) {
|
|
1413
|
+
stream.stopped = true;
|
|
1414
|
+
this.active.delete(sessionId);
|
|
1415
|
+
this.pendingStops.delete(sessionId);
|
|
1416
|
+
console.log(`[rAgent] Session stream ended: ${sessionId}`);
|
|
1417
|
+
this.onStreamStopped?.(sessionId);
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
console.log(`[rAgent] Started streaming: ${sessionId} (screen: ${sessionName})`);
|
|
1421
|
+
return true;
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1424
|
+
console.warn(`[rAgent] Failed to start screen stream for ${sessionId}: ${message}`);
|
|
1425
|
+
return false;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
// ---------------------------------------------------------------------------
|
|
1429
|
+
// zellij streaming (zellij attach in node-pty)
|
|
1430
|
+
// ---------------------------------------------------------------------------
|
|
1431
|
+
startZellijStream(sessionId) {
|
|
1432
|
+
const sessionName = parseZellijSession(sessionId);
|
|
1433
|
+
if (!sessionName) return false;
|
|
1434
|
+
try {
|
|
1435
|
+
const proc = pty2.spawn("zellij", ["attach", sessionName], {
|
|
1436
|
+
name: "xterm-256color",
|
|
1437
|
+
cols: 80,
|
|
1438
|
+
rows: 30,
|
|
1439
|
+
cwd: process.cwd(),
|
|
1440
|
+
env: process.env
|
|
1441
|
+
});
|
|
1442
|
+
const stream = {
|
|
1443
|
+
sessionId,
|
|
1444
|
+
streamType: "pty-attach",
|
|
1445
|
+
stopped: false,
|
|
1446
|
+
paneTarget: "",
|
|
1447
|
+
fifoPath: "",
|
|
1448
|
+
tmpDir: "",
|
|
1449
|
+
catProc: null,
|
|
1450
|
+
initializing: false,
|
|
1451
|
+
initBuffer: [],
|
|
1452
|
+
ptyProc: proc
|
|
1453
|
+
};
|
|
1454
|
+
this.active.set(sessionId, stream);
|
|
1455
|
+
proc.onData((data) => {
|
|
1456
|
+
if (!stream.stopped) {
|
|
1457
|
+
this.sendFn(sessionId, data);
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
proc.onExit(() => {
|
|
1461
|
+
if (!stream.stopped) {
|
|
1462
|
+
stream.stopped = true;
|
|
1463
|
+
this.active.delete(sessionId);
|
|
1464
|
+
this.pendingStops.delete(sessionId);
|
|
1465
|
+
console.log(`[rAgent] Session stream ended: ${sessionId}`);
|
|
1466
|
+
this.onStreamStopped?.(sessionId);
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
console.log(`[rAgent] Started streaming: ${sessionId} (zellij: ${sessionName})`);
|
|
1470
|
+
return true;
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1473
|
+
console.warn(`[rAgent] Failed to start zellij stream for ${sessionId}: ${message}`);
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
// ---------------------------------------------------------------------------
|
|
1478
|
+
// Cleanup
|
|
1479
|
+
// ---------------------------------------------------------------------------
|
|
1480
|
+
cleanupStream(stream) {
|
|
1481
|
+
stream.stopped = true;
|
|
1482
|
+
if (stream.streamType === "tmux-pipe") {
|
|
1483
|
+
try {
|
|
1484
|
+
(0, import_node_child_process3.execFileSync)("tmux", ["pipe-pane", "-t", stream.paneTarget], { timeout: 5e3 });
|
|
1485
|
+
} catch {
|
|
1486
|
+
}
|
|
1487
|
+
if (stream.catProc && !stream.catProc.killed) {
|
|
1488
|
+
try {
|
|
1489
|
+
stream.catProc.kill("SIGTERM");
|
|
1490
|
+
} catch {
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
try {
|
|
1494
|
+
if (stream.tmpDir && (0, import_node_fs.existsSync)(stream.tmpDir)) {
|
|
1495
|
+
(0, import_node_fs.rmSync)(stream.tmpDir, { recursive: true, force: true });
|
|
1496
|
+
}
|
|
1497
|
+
} catch {
|
|
1498
|
+
}
|
|
1499
|
+
} else if (stream.streamType === "pty-attach") {
|
|
1500
|
+
if (stream.ptyProc) {
|
|
1501
|
+
try {
|
|
1502
|
+
stream.ptyProc.kill();
|
|
1503
|
+
} catch {
|
|
1504
|
+
}
|
|
1505
|
+
stream.ptyProc = null;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1231
1509
|
};
|
|
1232
1510
|
|
|
1233
1511
|
// src/provisioner.ts
|
|
@@ -1511,6 +1789,8 @@ async function runAgent(rawOptions) {
|
|
|
1511
1789
|
let activeGroups = { privateGroup: "", registryGroup: "" };
|
|
1512
1790
|
let wsHeartbeatTimer = null;
|
|
1513
1791
|
let httpHeartbeatTimer = null;
|
|
1792
|
+
let wsPingTimer = null;
|
|
1793
|
+
let wsPongTimeout = null;
|
|
1514
1794
|
let suppressNextShellRespawn = false;
|
|
1515
1795
|
let lastSentFingerprint = "";
|
|
1516
1796
|
let lastHttpHeartbeatAt = 0;
|
|
@@ -1566,6 +1846,14 @@ async function runAgent(rawOptions) {
|
|
|
1566
1846
|
spawnOrRespawnShell();
|
|
1567
1847
|
const cleanupSocket = (opts = {}) => {
|
|
1568
1848
|
if (opts.stopStreams) sessionStreamer.stopAll();
|
|
1849
|
+
if (wsPingTimer) {
|
|
1850
|
+
clearInterval(wsPingTimer);
|
|
1851
|
+
wsPingTimer = null;
|
|
1852
|
+
}
|
|
1853
|
+
if (wsPongTimeout) {
|
|
1854
|
+
clearTimeout(wsPongTimeout);
|
|
1855
|
+
wsPongTimeout = null;
|
|
1856
|
+
}
|
|
1569
1857
|
if (wsHeartbeatTimer) {
|
|
1570
1858
|
clearInterval(wsHeartbeatTimer);
|
|
1571
1859
|
wsHeartbeatTimer = null;
|
|
@@ -1584,17 +1872,28 @@ async function runAgent(rawOptions) {
|
|
|
1584
1872
|
}
|
|
1585
1873
|
activeGroups = { privateGroup: "", registryGroup: "" };
|
|
1586
1874
|
};
|
|
1587
|
-
|
|
1875
|
+
let prevCpuSnapshot = null;
|
|
1876
|
+
function takeCpuSnapshot() {
|
|
1588
1877
|
const cpus2 = os5.cpus();
|
|
1589
|
-
let
|
|
1590
|
-
let
|
|
1878
|
+
let idle = 0;
|
|
1879
|
+
let total = 0;
|
|
1591
1880
|
for (const cpu of cpus2) {
|
|
1592
1881
|
for (const type of Object.keys(cpu.times)) {
|
|
1593
|
-
|
|
1882
|
+
total += cpu.times[type];
|
|
1594
1883
|
}
|
|
1595
|
-
|
|
1884
|
+
idle += cpu.times.idle;
|
|
1596
1885
|
}
|
|
1597
|
-
|
|
1886
|
+
return { idle, total };
|
|
1887
|
+
}
|
|
1888
|
+
const collectVitals = () => {
|
|
1889
|
+
const currentSnapshot = takeCpuSnapshot();
|
|
1890
|
+
let cpuUsage = 0;
|
|
1891
|
+
if (prevCpuSnapshot) {
|
|
1892
|
+
const idleDelta = currentSnapshot.idle - prevCpuSnapshot.idle;
|
|
1893
|
+
const totalDelta = currentSnapshot.total - prevCpuSnapshot.total;
|
|
1894
|
+
cpuUsage = totalDelta > 0 ? Math.round((totalDelta - idleDelta) / totalDelta * 100) : 0;
|
|
1895
|
+
}
|
|
1896
|
+
prevCpuSnapshot = currentSnapshot;
|
|
1598
1897
|
const totalMem = os5.totalmem();
|
|
1599
1898
|
const freeMem = os5.freemem();
|
|
1600
1899
|
const memUsedPct = totalMem > 0 ? Math.round((totalMem - freeMem) / totalMem * 100) : 0;
|
|
@@ -1725,8 +2024,8 @@ async function runAgent(rawOptions) {
|
|
|
1725
2024
|
}
|
|
1726
2025
|
tmuxArgs.push(fullCmd);
|
|
1727
2026
|
try {
|
|
1728
|
-
const { execFileSync:
|
|
1729
|
-
|
|
2027
|
+
const { execFileSync: execFileSync3 } = await import("child_process");
|
|
2028
|
+
execFileSync3("tmux", tmuxArgs, { stdio: "ignore" });
|
|
1730
2029
|
console.log(`[rAgent] Started agent session "${sessionName}": ${cmd}`);
|
|
1731
2030
|
} catch (error) {
|
|
1732
2031
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1743,12 +2042,12 @@ async function runAgent(rawOptions) {
|
|
|
1743
2042
|
sendToGroup(ws2, activeGroups.privateGroup, {
|
|
1744
2043
|
type: "stream-error",
|
|
1745
2044
|
sessionId,
|
|
1746
|
-
error: "
|
|
2045
|
+
error: "This agent is running outside a terminal multiplexer. Stop and relaunch via Start Agent to enable live streaming."
|
|
1747
2046
|
});
|
|
1748
2047
|
}
|
|
1749
2048
|
return;
|
|
1750
2049
|
}
|
|
1751
|
-
if (sessionId.startsWith("tmux:")) {
|
|
2050
|
+
if (sessionId.startsWith("tmux:") || sessionId.startsWith("screen:") || sessionId.startsWith("zellij:")) {
|
|
1752
2051
|
const started = sessionStreamer.startStream(sessionId);
|
|
1753
2052
|
const ws2 = activeSocket;
|
|
1754
2053
|
if (ws2 && ws2.readyState === import_ws2.default.OPEN && activeGroups.privateGroup) {
|
|
@@ -1761,7 +2060,7 @@ async function runAgent(rawOptions) {
|
|
|
1761
2060
|
sendToGroup(ws2, activeGroups.privateGroup, {
|
|
1762
2061
|
type: "stream-error",
|
|
1763
2062
|
sessionId,
|
|
1764
|
-
error: "Failed to attach to
|
|
2063
|
+
error: "Failed to attach to session. It may no longer exist."
|
|
1765
2064
|
});
|
|
1766
2065
|
}
|
|
1767
2066
|
}
|
|
@@ -1772,7 +2071,7 @@ async function runAgent(rawOptions) {
|
|
|
1772
2071
|
sendToGroup(ws, activeGroups.privateGroup, {
|
|
1773
2072
|
type: "stream-error",
|
|
1774
2073
|
sessionId,
|
|
1775
|
-
error:
|
|
2074
|
+
error: "Live streaming is not yet supported for this session type."
|
|
1776
2075
|
});
|
|
1777
2076
|
}
|
|
1778
2077
|
return;
|
|
@@ -1859,6 +2158,23 @@ async function runAgent(rawOptions) {
|
|
|
1859
2158
|
return;
|
|
1860
2159
|
await syncInventory();
|
|
1861
2160
|
}, HTTP_HEARTBEAT_MS);
|
|
2161
|
+
wsPingTimer = setInterval(() => {
|
|
2162
|
+
if (!activeSocket || activeSocket.readyState !== import_ws2.default.OPEN) return;
|
|
2163
|
+
activeSocket.ping();
|
|
2164
|
+
wsPongTimeout = setTimeout(() => {
|
|
2165
|
+
console.warn("[rAgent] No pong received within 10s \u2014 closing stale connection.");
|
|
2166
|
+
try {
|
|
2167
|
+
activeSocket?.terminate();
|
|
2168
|
+
} catch {
|
|
2169
|
+
}
|
|
2170
|
+
}, 1e4);
|
|
2171
|
+
}, 2e4);
|
|
2172
|
+
});
|
|
2173
|
+
ws.on("pong", () => {
|
|
2174
|
+
if (wsPongTimeout) {
|
|
2175
|
+
clearTimeout(wsPongTimeout);
|
|
2176
|
+
wsPongTimeout = null;
|
|
2177
|
+
}
|
|
1862
2178
|
});
|
|
1863
2179
|
ws.on("message", async (data) => {
|
|
1864
2180
|
let msg;
|
|
@@ -1875,6 +2191,8 @@ async function runAgent(rawOptions) {
|
|
|
1875
2191
|
if (ptyProcess) ptyProcess.write(payload.data);
|
|
1876
2192
|
} else if (sid.startsWith("tmux:")) {
|
|
1877
2193
|
await sendInputToTmux(sid, payload.data);
|
|
2194
|
+
} else if (sid.startsWith("screen:") || sid.startsWith("zellij:")) {
|
|
2195
|
+
sessionStreamer.writeInput(sid, payload.data);
|
|
1878
2196
|
}
|
|
1879
2197
|
} else if (payload.type === "resize" && Number.isInteger(payload.cols) && Number.isInteger(payload.rows)) {
|
|
1880
2198
|
const sid = typeof payload.sessionId === "string" ? payload.sessionId.trim() : "";
|
|
@@ -1960,10 +2278,11 @@ async function runAgent(rawOptions) {
|
|
|
1960
2278
|
await wait(300);
|
|
1961
2279
|
continue;
|
|
1962
2280
|
}
|
|
2281
|
+
const jitteredDelay = reconnectDelay * (0.5 + Math.random());
|
|
1963
2282
|
console.log(
|
|
1964
|
-
`[rAgent] Disconnected. Reconnecting in ${Math.round(
|
|
2283
|
+
`[rAgent] Disconnected. Reconnecting in ${Math.round(jitteredDelay / 1e3)}s...`
|
|
1965
2284
|
);
|
|
1966
|
-
await wait(
|
|
2285
|
+
await wait(jitteredDelay);
|
|
1967
2286
|
reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY_MS);
|
|
1968
2287
|
}
|
|
1969
2288
|
} finally {
|
|
@@ -2373,6 +2692,10 @@ function registerUninstallCommand(parent) {
|
|
|
2373
2692
|
}
|
|
2374
2693
|
|
|
2375
2694
|
// src/index.ts
|
|
2695
|
+
process.on("unhandledRejection", (reason) => {
|
|
2696
|
+
const message = reason instanceof Error ? reason.stack || reason.message : String(reason);
|
|
2697
|
+
console.error(`[rAgent] Unhandled promise rejection: ${message}`);
|
|
2698
|
+
});
|
|
2376
2699
|
import_commander.program.name("ragent").description("Connect machines to rAgent Live").version(CURRENT_VERSION);
|
|
2377
2700
|
registerConnectCommand(import_commander.program);
|
|
2378
2701
|
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.1",
|
|
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
|
}
|