ragent-cli 1.5.1 → 1.6.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 +247 -46
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "ragent-cli",
34
- version: "1.5.1",
34
+ version: "1.6.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: {
@@ -257,15 +257,18 @@ async function maybeWarnUpdate() {
257
257
  }
258
258
 
259
259
  // src/commands/connect.ts
260
- var os8 = __toESM(require("os"));
260
+ var os9 = __toESM(require("os"));
261
261
 
262
262
  // src/agent.ts
263
263
  var fs4 = __toESM(require("fs"));
264
- var os7 = __toESM(require("os"));
264
+ var os8 = __toESM(require("os"));
265
265
  var path3 = __toESM(require("path"));
266
266
  var import_ws5 = __toESM(require("ws"));
267
267
 
268
268
  // src/auth.ts
269
+ var os4 = __toESM(require("os"));
270
+
271
+ // src/sessions.ts
269
272
  var os3 = __toESM(require("os"));
270
273
 
271
274
  // src/system.ts
@@ -357,6 +360,7 @@ async function installTmuxInteractively() {
357
360
  }
358
361
 
359
362
  // src/sessions.ts
363
+ var isMac = os3.platform() === "darwin";
360
364
  async function collectTmuxSessions() {
361
365
  try {
362
366
  await execAsync("tmux -V");
@@ -467,7 +471,7 @@ async function collectZellijSessions() {
467
471
  let serverPids;
468
472
  try {
469
473
  const psOut = await execAsync(
470
- `ps axo pid,args --no-headers | grep 'zellij.*--session ${sessionName}' | grep -v grep`
474
+ `ps axo pid,args | grep 'zellij.*--session ${sessionName}' | grep -v grep`
471
475
  );
472
476
  serverPids = psOut.split("\n").map((l) => Number(l.trim().split(/\s+/)[0])).filter((p) => p > 0);
473
477
  } catch {
@@ -502,6 +506,17 @@ async function collectZellijSessions() {
502
506
  }
503
507
  async function getProcessWorkingDir(pid) {
504
508
  try {
509
+ if (isMac) {
510
+ const raw = await execAsync(
511
+ `lsof -a -p ${pid} -d cwd -Fn 2>/dev/null`
512
+ );
513
+ for (const line of raw.split("\n")) {
514
+ if (line.startsWith("n/")) {
515
+ return line.slice(1).trim() || null;
516
+ }
517
+ }
518
+ return null;
519
+ }
505
520
  const cwd = await execAsync(`readlink -f /proc/${pid}/cwd`);
506
521
  return cwd.trim() || null;
507
522
  } catch {
@@ -510,12 +525,14 @@ async function getProcessWorkingDir(pid) {
510
525
  }
511
526
  async function collectBareAgentProcesses(excludePids) {
512
527
  try {
513
- const raw = await execAsync("ps axo pid,ppid,stat,comm,args --no-headers");
528
+ const raw = await execAsync("ps axo pid,ppid,stat,comm,args");
514
529
  const sessions = [];
515
530
  const seen = /* @__PURE__ */ new Set();
516
- for (const line of raw.split("\n")) {
517
- const trimmed = line.trim();
531
+ const lines = raw.split("\n");
532
+ for (let i = 0; i < lines.length; i++) {
533
+ const trimmed = lines[i].trim();
518
534
  if (!trimmed) continue;
535
+ if (i === 0 && /^\s*PID\b/i.test(lines[i])) continue;
519
536
  const parts = trimmed.split(/\s+/);
520
537
  if (parts.length < 5) continue;
521
538
  const pid = Number(parts[0]);
@@ -618,11 +635,16 @@ function sessionInventoryFingerprint(sessions) {
618
635
  }
619
636
  async function getChildAgentInfo(parentPid) {
620
637
  try {
621
- const raw = await execAsync(`ps --ppid ${parentPid} -o pid,args --no-headers`);
638
+ const pgrepOut = await execAsync(`pgrep -P ${parentPid}`);
639
+ const pidList = pgrepOut.split("\n").map((l) => l.trim()).filter(Boolean).map(Number).filter((p) => p > 0);
640
+ if (pidList.length === 0) return null;
641
+ const raw = await execAsync(`ps -p ${pidList.join(",")} -o pid,args`);
622
642
  const childPids = [];
623
- for (const line of raw.split("\n")) {
624
- const trimmed = line.trim();
643
+ const lines = raw.split("\n");
644
+ for (let i = 0; i < lines.length; i++) {
645
+ const trimmed = lines[i].trim();
625
646
  if (!trimmed) continue;
647
+ if (i === 0 && /^\s*PID\b/i.test(lines[i])) continue;
626
648
  const spaceIdx = trimmed.indexOf(" ");
627
649
  if (spaceIdx < 0) continue;
628
650
  const pid = Number(trimmed.slice(0, spaceIdx));
@@ -719,7 +741,7 @@ async function claimHost(params) {
719
741
  connectionToken: params.connectionToken,
720
742
  hostId: params.hostId,
721
743
  hostName: params.hostName,
722
- environment: os3.platform(),
744
+ environment: os4.platform(),
723
745
  sessions,
724
746
  agentVersion: CURRENT_VERSION
725
747
  })
@@ -781,7 +803,7 @@ async function postHeartbeat(params) {
781
803
  Authorization: `Bearer ${params.agentToken}`
782
804
  },
783
805
  body: JSON.stringify({
784
- environment: os3.platform(),
806
+ environment: os4.platform(),
785
807
  hostName: params.hostName,
786
808
  sessions,
787
809
  agentVersion: CURRENT_VERSION
@@ -845,6 +867,33 @@ var DRAIN_INTERVAL_MS = 50;
845
867
  function sanitizeForJson(str) {
846
868
  return str.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
847
869
  }
870
+ var DANGEROUS_OSC_RE = /\x1b\](?:52|7|8);[^\x07\x1b]*(?:\x07|\x1b\\)/g;
871
+ function stripDangerousEscapes(str) {
872
+ return str.replace(DANGEROUS_OSC_RE, "");
873
+ }
874
+ var SECRET_PATTERNS = [
875
+ /AKIA[0-9A-Z]{16}/g,
876
+ // AWS access key
877
+ /gh[ps]_[A-Za-z0-9_]{36,}/g,
878
+ // GitHub token
879
+ /sk_(?:live|test)_[A-Za-z0-9]{24,}/g,
880
+ // Stripe secret key
881
+ /eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g
882
+ // JWT
883
+ ];
884
+ var redactionEnabled = false;
885
+ function setRedactionEnabled(enabled) {
886
+ redactionEnabled = enabled;
887
+ }
888
+ function redactSecrets(str) {
889
+ if (!redactionEnabled) return str;
890
+ let result = str;
891
+ for (const pattern of SECRET_PATTERNS) {
892
+ pattern.lastIndex = 0;
893
+ result = result.replace(pattern, "[REDACTED]");
894
+ }
895
+ return result;
896
+ }
848
897
  var pendingQueue = [];
849
898
  var drainTimer = null;
850
899
  var drainWs = null;
@@ -909,7 +958,7 @@ function sanitizePayload(obj) {
909
958
  const result = {};
910
959
  for (const [key, value] of Object.entries(obj)) {
911
960
  if (typeof value === "string") {
912
- result[key] = sanitizeForJson(value);
961
+ result[key] = redactSecrets(stripDangerousEscapes(sanitizeForJson(value)));
913
962
  } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
914
963
  result[key] = sanitizePayload(value);
915
964
  } else {
@@ -927,6 +976,7 @@ var import_node_os = require("os");
927
976
  var import_node_string_decoder = require("string_decoder");
928
977
  var pty = __toESM(require("node-pty"));
929
978
  var STOP_DEBOUNCE_MS = 2e3;
979
+ var MAX_SESSION_BUFFER_BYTES = 64 * 1024;
930
980
  function parsePaneTarget(sessionId) {
931
981
  if (!sessionId.startsWith("tmux:")) return null;
932
982
  const rest = sessionId.slice("tmux:".length);
@@ -982,6 +1032,13 @@ var SessionStreamer = class {
982
1032
  pendingStops = /* @__PURE__ */ new Map();
983
1033
  sendFn;
984
1034
  onStreamStopped;
1035
+ /**
1036
+ * Per-session ring buffer for output captured while the WebSocket is disconnected.
1037
+ * Each entry is a string chunk; oldest entries are evicted when the total byte
1038
+ * size exceeds MAX_SESSION_BUFFER_BYTES.
1039
+ */
1040
+ disconnectBuffers = /* @__PURE__ */ new Map();
1041
+ disconnectBufferBytes = /* @__PURE__ */ new Map();
985
1042
  constructor(sendFn, onStreamStopped) {
986
1043
  this.sendFn = sendFn;
987
1044
  this.onStreamStopped = onStreamStopped;
@@ -1081,6 +1138,8 @@ var SessionStreamer = class {
1081
1138
  if (!s) return;
1082
1139
  this.cleanupStream(s);
1083
1140
  this.active.delete(sessionId);
1141
+ this.disconnectBuffers.delete(sessionId);
1142
+ this.disconnectBufferBytes.delete(sessionId);
1084
1143
  console.log(`[rAgent] Stopped streaming: ${sessionId}`);
1085
1144
  }, STOP_DEBOUNCE_MS);
1086
1145
  this.pendingStops.set(sessionId, timer);
@@ -1104,6 +1163,8 @@ var SessionStreamer = class {
1104
1163
  console.log(`[rAgent] Stopped streaming: ${id}`);
1105
1164
  }
1106
1165
  this.active.clear();
1166
+ this.disconnectBuffers.clear();
1167
+ this.disconnectBufferBytes.clear();
1107
1168
  }
1108
1169
  /**
1109
1170
  * Get the number of active streams.
@@ -1111,6 +1172,54 @@ var SessionStreamer = class {
1111
1172
  get activeCount() {
1112
1173
  return this.active.size;
1113
1174
  }
1175
+ /**
1176
+ * Return an iterator over active stream entries (sessionId -> ActiveStream).
1177
+ * Used by the agent to replay disconnect-buffered output on reconnect.
1178
+ */
1179
+ activeStreams() {
1180
+ return this.active.entries();
1181
+ }
1182
+ // ---------------------------------------------------------------------------
1183
+ // Disconnect buffering — captures output when WebSocket is down
1184
+ // ---------------------------------------------------------------------------
1185
+ /**
1186
+ * Append a chunk of output to the per-session disconnect buffer.
1187
+ * Oldest entries are evicted when the buffer exceeds MAX_SESSION_BUFFER_BYTES.
1188
+ */
1189
+ bufferOutput(sessionId, data) {
1190
+ let chunks = this.disconnectBuffers.get(sessionId);
1191
+ if (!chunks) {
1192
+ chunks = [];
1193
+ this.disconnectBuffers.set(sessionId, chunks);
1194
+ this.disconnectBufferBytes.set(sessionId, 0);
1195
+ }
1196
+ const dataBytes = Buffer.byteLength(data, "utf8");
1197
+ let currentBytes = this.disconnectBufferBytes.get(sessionId) ?? 0;
1198
+ while (chunks.length > 0 && currentBytes + dataBytes > MAX_SESSION_BUFFER_BYTES) {
1199
+ const evicted = chunks.shift();
1200
+ currentBytes -= Buffer.byteLength(evicted, "utf8");
1201
+ }
1202
+ if (dataBytes > MAX_SESSION_BUFFER_BYTES) {
1203
+ const tail = data.slice(data.length - MAX_SESSION_BUFFER_BYTES);
1204
+ chunks.push(tail);
1205
+ this.disconnectBufferBytes.set(sessionId, Buffer.byteLength(tail, "utf8"));
1206
+ return;
1207
+ }
1208
+ chunks.push(data);
1209
+ this.disconnectBufferBytes.set(sessionId, currentBytes + dataBytes);
1210
+ }
1211
+ /**
1212
+ * Drain the disconnect buffer for a session, returning all buffered chunks
1213
+ * and clearing the buffer. Returns an empty array if nothing was buffered.
1214
+ */
1215
+ drainBuffer(sessionId) {
1216
+ const chunks = this.disconnectBuffers.get(sessionId);
1217
+ if (!chunks || chunks.length === 0) return [];
1218
+ const result = [...chunks];
1219
+ this.disconnectBuffers.delete(sessionId);
1220
+ this.disconnectBufferBytes.delete(sessionId);
1221
+ return result;
1222
+ }
1114
1223
  /**
1115
1224
  * Re-send capture-pane data for an already-active tmux stream.
1116
1225
  * Used when a viewer missed the initial burst (e.g. joinGroup race)
@@ -1707,7 +1816,7 @@ var ShellManager = class {
1707
1816
  };
1708
1817
 
1709
1818
  // src/inventory-manager.ts
1710
- var os4 = __toESM(require("os"));
1819
+ var os5 = __toESM(require("os"));
1711
1820
  var import_child_process = require("child_process");
1712
1821
  var import_ws2 = __toESM(require("ws"));
1713
1822
  var InventoryManager = class {
@@ -1731,7 +1840,7 @@ var InventoryManager = class {
1731
1840
  type,
1732
1841
  hostId: this.options.hostId,
1733
1842
  hostName: this.options.hostName,
1734
- environment: os4.hostname(),
1843
+ environment: os5.hostname(),
1735
1844
  sessions,
1736
1845
  vitals,
1737
1846
  agentVersion: CURRENT_VERSION,
@@ -1774,8 +1883,8 @@ var InventoryManager = class {
1774
1883
  cpuUsage = totalDelta > 0 ? Math.round((totalDelta - idleDelta) / totalDelta * 100) : 0;
1775
1884
  }
1776
1885
  this.prevCpuSnapshot = currentSnapshot;
1777
- const totalMem = os4.totalmem();
1778
- const freeMem = os4.freemem();
1886
+ const totalMem = os5.totalmem();
1887
+ const freeMem = os5.freemem();
1779
1888
  const memUsedPct = totalMem > 0 ? Math.round((totalMem - freeMem) / totalMem * 100) : 0;
1780
1889
  let diskUsedPct = 0;
1781
1890
  try {
@@ -1789,7 +1898,7 @@ var InventoryManager = class {
1789
1898
  return { cpu: cpuUsage, memUsedPct, diskUsedPct };
1790
1899
  }
1791
1900
  takeCpuSnapshot() {
1792
- const cpus2 = os4.cpus();
1901
+ const cpus2 = os5.cpus();
1793
1902
  let idle = 0;
1794
1903
  let total = 0;
1795
1904
  for (const cpu of cpus2) {
@@ -1920,7 +2029,7 @@ var import_ws4 = __toESM(require("ws"));
1920
2029
  // src/service.ts
1921
2030
  var import_child_process2 = require("child_process");
1922
2031
  var fs2 = __toESM(require("fs"));
1923
- var os5 = __toESM(require("os"));
2032
+ var os6 = __toESM(require("os"));
1924
2033
  function assertConfiguredAgentToken() {
1925
2034
  const config = loadConfig();
1926
2035
  if (!config.agentToken) {
@@ -1937,7 +2046,7 @@ function getConfiguredServiceBackend() {
1937
2046
  return null;
1938
2047
  }
1939
2048
  async function canUseSystemdUser() {
1940
- if (os5.platform() !== "linux") return false;
2049
+ if (os6.platform() !== "linux") return false;
1941
2050
  try {
1942
2051
  await execAsync("systemctl --user --version", { timeout: 4e3 });
1943
2052
  await execAsync("systemctl --user show-environment", { timeout: 4e3 });
@@ -1960,7 +2069,9 @@ Wants=network-online.target
1960
2069
  Type=simple
1961
2070
  ExecStart=${process.execPath} ${__filename} run
1962
2071
  Restart=always
1963
- RestartSec=3
2072
+ RestartSec=5
2073
+ StartLimitBurst=5
2074
+ StartLimitIntervalSec=300
1964
2075
  Environment=NODE_ENV=production
1965
2076
  NoNewPrivileges=true
1966
2077
  PrivateTmp=true
@@ -2004,6 +2115,21 @@ function isProcessRunning(pid) {
2004
2115
  return false;
2005
2116
  }
2006
2117
  }
2118
+ var LOG_ROTATION_MAX_BYTES = 10 * 1024 * 1024;
2119
+ function rotateLogIfNeeded() {
2120
+ try {
2121
+ const stat = fs2.statSync(FALLBACK_LOG_FILE);
2122
+ if (stat.size > LOG_ROTATION_MAX_BYTES) {
2123
+ const rotated = `${FALLBACK_LOG_FILE}.1`;
2124
+ try {
2125
+ fs2.unlinkSync(rotated);
2126
+ } catch {
2127
+ }
2128
+ fs2.renameSync(FALLBACK_LOG_FILE, rotated);
2129
+ }
2130
+ } catch {
2131
+ }
2132
+ }
2007
2133
  async function startPidfileService() {
2008
2134
  assertConfiguredAgentToken();
2009
2135
  const existingPid = readFallbackPid();
@@ -2012,11 +2138,12 @@ async function startPidfileService() {
2012
2138
  return;
2013
2139
  }
2014
2140
  ensureConfigDir();
2141
+ rotateLogIfNeeded();
2015
2142
  const logFd = fs2.openSync(FALLBACK_LOG_FILE, "a");
2016
2143
  const child = (0, import_child_process2.spawn)(process.execPath, [__filename, "run"], {
2017
2144
  detached: true,
2018
2145
  stdio: ["ignore", logFd, logFd],
2019
- cwd: os5.homedir(),
2146
+ cwd: os6.homedir(),
2020
2147
  env: process.env
2021
2148
  });
2022
2149
  child.unref();
@@ -2716,7 +2843,7 @@ var ControlDispatcher = class {
2716
2843
  // src/transcript-watcher.ts
2717
2844
  var fs3 = __toESM(require("fs"));
2718
2845
  var path2 = __toESM(require("path"));
2719
- var os6 = __toESM(require("os"));
2846
+ var os7 = __toESM(require("os"));
2720
2847
  var import_child_process5 = require("child_process");
2721
2848
  var ClaudeCodeParser = class {
2722
2849
  name = "claude-code";
@@ -2775,25 +2902,33 @@ var ClaudeCodeParser = class {
2775
2902
  parseUserToolResults(obj) {
2776
2903
  const content = obj.message.content;
2777
2904
  const tools = [];
2905
+ const textBlocks = [];
2778
2906
  for (const block of content) {
2779
- if (block.type !== "tool_result") continue;
2780
- const toolName = (block.tool_use_id && this.pendingTools.get(block.tool_use_id)) ?? "Tool";
2781
- const resultText = this.extractToolResultText(block);
2782
- if (block.tool_use_id) {
2783
- this.pendingTools.delete(block.tool_use_id);
2784
- }
2785
- tools.push({
2786
- name: toolName,
2787
- status: block.is_error ? "error" : "completed",
2788
- result: resultText || void 0
2789
- });
2907
+ if (block.type === "tool_result") {
2908
+ const toolName = (block.tool_use_id && this.pendingTools.get(block.tool_use_id)) ?? "Tool";
2909
+ const resultText = this.extractToolResultText(block);
2910
+ if (block.tool_use_id) {
2911
+ this.pendingTools.delete(block.tool_use_id);
2912
+ }
2913
+ tools.push({
2914
+ name: toolName,
2915
+ status: block.is_error ? "error" : "completed",
2916
+ result: resultText || void 0
2917
+ });
2918
+ } else if (block.type === "text" && typeof block.text === "string") {
2919
+ const t = block.text.trim();
2920
+ if (t && !t.startsWith("<") && !t.startsWith("[Request interrupted")) {
2921
+ textBlocks.push(t);
2922
+ }
2923
+ }
2790
2924
  }
2791
- if (tools.length === 0) return null;
2925
+ const text = textBlocks.join("\n").trim();
2926
+ if (tools.length === 0 && !text) return null;
2792
2927
  return {
2793
2928
  turnId: obj.uuid ?? `result-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2794
2929
  role: "user",
2795
- content: "",
2796
- tools,
2930
+ content: text,
2931
+ tools: tools.length > 0 ? tools : void 0,
2797
2932
  timestamp: obj.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
2798
2933
  };
2799
2934
  }
@@ -2881,7 +3016,7 @@ function discoverViaProc(panePid) {
2881
3016
  return null;
2882
3017
  }
2883
3018
  function discoverViaCwd(paneCwd) {
2884
- const claudeProjectsDir = path2.join(os6.homedir(), ".claude", "projects");
3019
+ const claudeProjectsDir = path2.join(os7.homedir(), ".claude", "projects");
2885
3020
  if (!fs3.existsSync(claudeProjectsDir)) return null;
2886
3021
  const resolvedCwd = fs3.realpathSync(paneCwd);
2887
3022
  const expectedDirName = resolvedCwd.replace(/\//g, "-");
@@ -2899,7 +3034,7 @@ function discoverViaCwd(paneCwd) {
2899
3034
  }
2900
3035
  }
2901
3036
  function discoverCodexTranscript() {
2902
- const codexSessionsDir = path2.join(os6.homedir(), ".codex", "sessions");
3037
+ const codexSessionsDir = path2.join(os7.homedir(), ".codex", "sessions");
2903
3038
  if (!fs3.existsSync(codexSessionsDir)) return null;
2904
3039
  try {
2905
3040
  const dateDirs = fs3.readdirSync(codexSessionsDir).filter((d) => /^\d{4}-\d{2}-\d{2}/.test(d)).sort().reverse();
@@ -2954,6 +3089,7 @@ function discoverTranscriptFile(sessionId, agentType) {
2954
3089
  var MAX_PARTIAL_BUFFER = 256 * 1024;
2955
3090
  var MAX_REPLAY_TURNS = 200;
2956
3091
  var POLL_INTERVAL_MS = 800;
3092
+ var REDISCOVERY_INTERVAL_MS = 5e3;
2957
3093
  var TranscriptWatcher = class {
2958
3094
  filePath;
2959
3095
  parser;
@@ -3124,6 +3260,7 @@ function getParser(agentType) {
3124
3260
  }
3125
3261
  var TranscriptWatcherManager = class {
3126
3262
  active = /* @__PURE__ */ new Map();
3263
+ rediscoveryTimers = /* @__PURE__ */ new Map();
3127
3264
  sendFn;
3128
3265
  sendSnapshotFn;
3129
3266
  constructor(sendFn, sendSnapshotFn) {
@@ -3162,6 +3299,7 @@ var TranscriptWatcherManager = class {
3162
3299
  }
3163
3300
  this.active.set(sessionId, { watcher, filePath, agentType: agentType ?? "" });
3164
3301
  watcher.replayFromStart();
3302
+ this.startRediscovery(sessionId, agentType);
3165
3303
  return true;
3166
3304
  }
3167
3305
  /** Disable markdown streaming for a session. */
@@ -3170,8 +3308,50 @@ var TranscriptWatcherManager = class {
3170
3308
  if (!session) return;
3171
3309
  session.watcher.removeSubscriber();
3172
3310
  this.active.delete(sessionId);
3311
+ this.stopRediscovery(sessionId);
3173
3312
  console.log(`[rAgent] Stopped watching transcript for ${sessionId}`);
3174
3313
  }
3314
+ /** Periodically check if the transcript file has changed (new conversation). */
3315
+ startRediscovery(sessionId, agentType) {
3316
+ this.stopRediscovery(sessionId);
3317
+ const timer = setInterval(() => {
3318
+ const session = this.active.get(sessionId);
3319
+ if (!session) {
3320
+ this.stopRediscovery(sessionId);
3321
+ return;
3322
+ }
3323
+ const newPath = discoverTranscriptFile(sessionId, agentType);
3324
+ if (!newPath || newPath === session.filePath) return;
3325
+ console.log(`[rAgent] Transcript file changed for ${sessionId}: ${newPath}`);
3326
+ session.watcher.stop();
3327
+ const parser = getParser(agentType);
3328
+ if (!parser) return;
3329
+ const newWatcher = new TranscriptWatcher(newPath, parser, {
3330
+ onTurn: (turn, seq) => {
3331
+ this.sendFn(sessionId, turn, seq);
3332
+ },
3333
+ onError: (error) => {
3334
+ console.warn(`[rAgent] Transcript watcher error (${sessionId}): ${error}`);
3335
+ }
3336
+ });
3337
+ if (!newWatcher.start()) {
3338
+ console.warn(`[rAgent] Failed to start watching new transcript ${newPath}`);
3339
+ return;
3340
+ }
3341
+ this.sendSnapshotFn(sessionId, [], 0);
3342
+ newWatcher.replayFromStart();
3343
+ this.active.set(sessionId, { watcher: newWatcher, filePath: newPath, agentType: agentType ?? "" });
3344
+ }, REDISCOVERY_INTERVAL_MS);
3345
+ this.rediscoveryTimers.set(sessionId, timer);
3346
+ }
3347
+ /** Stop the re-discovery timer for a session. */
3348
+ stopRediscovery(sessionId) {
3349
+ const timer = this.rediscoveryTimers.get(sessionId);
3350
+ if (timer) {
3351
+ clearInterval(timer);
3352
+ this.rediscoveryTimers.delete(sessionId);
3353
+ }
3354
+ }
3175
3355
  /** Handle sync-markdown request — send replay snapshot. */
3176
3356
  handleSyncRequest(sessionId, fromSeq) {
3177
3357
  const session = this.active.get(sessionId);
@@ -3183,9 +3363,11 @@ var TranscriptWatcherManager = class {
3183
3363
  stopAll() {
3184
3364
  for (const [sessionId, session] of this.active) {
3185
3365
  session.watcher.stop();
3366
+ this.stopRediscovery(sessionId);
3186
3367
  console.log(`[rAgent] Stopped transcript watcher for ${sessionId}`);
3187
3368
  }
3188
3369
  this.active.clear();
3370
+ this.rediscoveryTimers.clear();
3189
3371
  }
3190
3372
  /** Check if a session has an active watcher. */
3191
3373
  isWatching(sessionId) {
@@ -3241,7 +3423,7 @@ function resolveRunOptions(commandOptions) {
3241
3423
  const config = loadConfig();
3242
3424
  const portal = commandOptions.portal || config.portal || DEFAULT_PORTAL;
3243
3425
  const hostId = sanitizeHostId(commandOptions.id || config.hostId || inferHostId());
3244
- const hostName = commandOptions.name || config.hostName || os7.hostname();
3426
+ const hostName = commandOptions.name || config.hostName || os8.hostname();
3245
3427
  const command = commandOptions.command || config.command || "bash";
3246
3428
  const agentToken = commandOptions.agentToken || config.agentToken || "";
3247
3429
  return { portal, hostId, hostName, command, agentToken };
@@ -3252,6 +3434,11 @@ async function runAgent(rawOptions) {
3252
3434
  throw new Error("No agent token found. Run `ragent connect` first.");
3253
3435
  }
3254
3436
  const lockPath = acquirePidLock(options.hostId);
3437
+ const config = loadConfig();
3438
+ if (config.redaction?.enabled) {
3439
+ setRedactionEnabled(true);
3440
+ console.log("[rAgent] Secret redaction enabled.");
3441
+ }
3255
3442
  console.log(`[rAgent] Connector started for ${options.hostName} (${options.hostId})`);
3256
3443
  console.log(`[rAgent] Portal: ${options.portal}`);
3257
3444
  try {
@@ -3266,7 +3453,10 @@ async function runAgent(rawOptions) {
3266
3453
  const ptySessionId = `pty:${options.hostId}`;
3267
3454
  const sessionStreamer = new SessionStreamer(
3268
3455
  (sessionId, data) => {
3269
- if (!conn.isReady()) return;
3456
+ if (!conn.isReady()) {
3457
+ sessionStreamer.bufferOutput(sessionId, data);
3458
+ return;
3459
+ }
3270
3460
  if (conn.sessionKey) {
3271
3461
  const { enc, iv } = encryptPayload(data, conn.sessionKey);
3272
3462
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, sessionId });
@@ -3374,7 +3564,7 @@ async function runAgent(rawOptions) {
3374
3564
  sendToGroup(ws, groups.privateGroup, {
3375
3565
  type: "register",
3376
3566
  hostName: options.hostName,
3377
- environment: os7.platform()
3567
+ environment: os8.platform()
3378
3568
  });
3379
3569
  conn.replayBufferedOutput((chunk) => {
3380
3570
  sendToGroup(ws, groups.privateGroup, {
@@ -3383,6 +3573,17 @@ async function runAgent(rawOptions) {
3383
3573
  sessionId: ptySessionId
3384
3574
  });
3385
3575
  });
3576
+ for (const [sessionId] of sessionStreamer.activeStreams()) {
3577
+ const buffered = sessionStreamer.drainBuffer(sessionId);
3578
+ for (const chunk of buffered) {
3579
+ if (conn.sessionKey) {
3580
+ const { enc, iv } = encryptPayload(chunk, conn.sessionKey);
3581
+ sendToGroup(ws, groups.privateGroup, { type: "output", enc, iv, sessionId });
3582
+ } else {
3583
+ sendToGroup(ws, groups.privateGroup, { type: "output", data: chunk, sessionId });
3584
+ }
3585
+ }
3586
+ }
3386
3587
  await inventory.syncInventory(ws, groups, true);
3387
3588
  conn.startTimers(
3388
3589
  () => inventory.announceToRegistry(ws, groups.registryGroup, "heartbeat"),
@@ -3561,7 +3762,7 @@ function printCommandArt(title, subtitle = "remote agent control") {
3561
3762
  async function connectMachine(opts) {
3562
3763
  const portal = opts.portal || DEFAULT_PORTAL;
3563
3764
  const hostId = sanitizeHostId(opts.id || inferHostId());
3564
- const hostName = opts.name || os8.hostname();
3765
+ const hostName = opts.name || os9.hostname();
3565
3766
  const command = opts.command || "bash";
3566
3767
  if (!opts.token) {
3567
3768
  throw new Error("Connection token is required.");
@@ -3635,12 +3836,12 @@ function registerRunCommand(program2) {
3635
3836
  }
3636
3837
 
3637
3838
  // src/commands/doctor.ts
3638
- var os9 = __toESM(require("os"));
3839
+ var os10 = __toESM(require("os"));
3639
3840
  async function runDoctor(opts) {
3640
3841
  const options = resolveRunOptions(opts);
3641
3842
  const checks = [];
3642
- const platformOk = os9.platform() === "linux";
3643
- checks.push({ name: "platform", ok: platformOk, detail: os9.platform() });
3843
+ const platformOk = os10.platform() === "linux";
3844
+ checks.push({ name: "platform", ok: platformOk, detail: os10.platform() });
3644
3845
  checks.push({
3645
3846
  name: "node",
3646
3847
  ok: Number(process.versions.node.split(".")[0]) >= 20,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ragent-cli",
3
- "version": "1.5.1",
3
+ "version": "1.6.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": {