ragent-cli 1.2.1 → 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.
Files changed (2) hide show
  1. package/dist/index.js +372 -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.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": "^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,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
- const collectVitals = () => {
1865
+ let prevCpuSnapshot = null;
1866
+ function takeCpuSnapshot() {
1588
1867
  const cpus2 = os5.cpus();
1589
- let totalIdle = 0;
1590
- let totalTick = 0;
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
- totalTick += cpu.times[type];
1872
+ total += cpu.times[type];
1594
1873
  }
1595
- totalIdle += cpu.times.idle;
1874
+ idle += cpu.times.idle;
1596
1875
  }
1597
- const cpuUsage = totalTick > 0 ? Math.round((totalTick - totalIdle) / totalTick * 100) : 0;
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: execFileSync2 } = await import("child_process");
1729
- execFileSync2("tmux", tmuxArgs, { stdio: "ignore" });
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: "Live output is not available for standalone processes. Start agents inside tmux for live monitoring."
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 tmux session. It may no longer exist."
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: `Streaming not yet supported for session type: ${sessionId.split(":")[0]}`
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(reconnectDelay / 1e3)}s...`
2273
+ `[rAgent] Disconnected. Reconnecting in ${Math.round(jitteredDelay / 1e3)}s...`
1965
2274
  );
1966
- await wait(reconnectDelay);
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.2.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": "^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
  }