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.
Files changed (2) hide show
  1. package/dist/index.js +382 -59
  2. 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.2.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": "^20.17.0",
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
- vitest: "^3.0.0"
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 tmux session. Returns true if streaming started.
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
- const parts = sessionId.split(":");
1148
- if (parts[0] !== "tmux" || parts.length < 2) {
1149
- return false;
1200
+ if (sessionId.startsWith("tmux:")) {
1201
+ return this.startTmuxStream(sessionId);
1150
1202
  }
1151
- const tmuxSession = parts[1];
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
- 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;
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 start stream for ${sessionId}: ${message}`);
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
- try {
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
- try {
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
- const collectVitals = () => {
1875
+ let prevCpuSnapshot = null;
1876
+ function takeCpuSnapshot() {
1588
1877
  const cpus2 = os5.cpus();
1589
- let totalIdle = 0;
1590
- let totalTick = 0;
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
- totalTick += cpu.times[type];
1882
+ total += cpu.times[type];
1594
1883
  }
1595
- totalIdle += cpu.times.idle;
1884
+ idle += cpu.times.idle;
1596
1885
  }
1597
- const cpuUsage = totalTick > 0 ? Math.round((totalTick - totalIdle) / totalTick * 100) : 0;
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: execFileSync2 } = await import("child_process");
1729
- execFileSync2("tmux", tmuxArgs, { stdio: "ignore" });
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: "Live output is not available for standalone processes. Start agents inside tmux for live monitoring."
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 tmux session. It may no longer exist."
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: `Streaming not yet supported for session type: ${sessionId.split(":")[0]}`
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(reconnectDelay / 1e3)}s...`
2283
+ `[rAgent] Disconnected. Reconnecting in ${Math.round(jitteredDelay / 1e3)}s...`
1965
2284
  );
1966
- await wait(reconnectDelay);
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.2.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": "^20.17.0",
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
- "vitest": "^3.0.0"
66
+ "typescript-eslint": "^8.56.1",
67
+ "vitest": "^4.0.18"
59
68
  }
60
69
  }