ragent-cli 1.5.2 → 1.6.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 +239 -36
  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.2",
34
+ version: "1.6.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: {
@@ -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";
@@ -2889,7 +3016,7 @@ function discoverViaProc(panePid) {
2889
3016
  return null;
2890
3017
  }
2891
3018
  function discoverViaCwd(paneCwd) {
2892
- const claudeProjectsDir = path2.join(os6.homedir(), ".claude", "projects");
3019
+ const claudeProjectsDir = path2.join(os7.homedir(), ".claude", "projects");
2893
3020
  if (!fs3.existsSync(claudeProjectsDir)) return null;
2894
3021
  const resolvedCwd = fs3.realpathSync(paneCwd);
2895
3022
  const expectedDirName = resolvedCwd.replace(/\//g, "-");
@@ -2907,7 +3034,7 @@ function discoverViaCwd(paneCwd) {
2907
3034
  }
2908
3035
  }
2909
3036
  function discoverCodexTranscript() {
2910
- const codexSessionsDir = path2.join(os6.homedir(), ".codex", "sessions");
3037
+ const codexSessionsDir = path2.join(os7.homedir(), ".codex", "sessions");
2911
3038
  if (!fs3.existsSync(codexSessionsDir)) return null;
2912
3039
  try {
2913
3040
  const dateDirs = fs3.readdirSync(codexSessionsDir).filter((d) => /^\d{4}-\d{2}-\d{2}/.test(d)).sort().reverse();
@@ -2962,6 +3089,7 @@ function discoverTranscriptFile(sessionId, agentType) {
2962
3089
  var MAX_PARTIAL_BUFFER = 256 * 1024;
2963
3090
  var MAX_REPLAY_TURNS = 200;
2964
3091
  var POLL_INTERVAL_MS = 800;
3092
+ var REDISCOVERY_INTERVAL_MS = 5e3;
2965
3093
  var TranscriptWatcher = class {
2966
3094
  filePath;
2967
3095
  parser;
@@ -3132,6 +3260,7 @@ function getParser(agentType) {
3132
3260
  }
3133
3261
  var TranscriptWatcherManager = class {
3134
3262
  active = /* @__PURE__ */ new Map();
3263
+ rediscoveryTimers = /* @__PURE__ */ new Map();
3135
3264
  sendFn;
3136
3265
  sendSnapshotFn;
3137
3266
  constructor(sendFn, sendSnapshotFn) {
@@ -3170,6 +3299,7 @@ var TranscriptWatcherManager = class {
3170
3299
  }
3171
3300
  this.active.set(sessionId, { watcher, filePath, agentType: agentType ?? "" });
3172
3301
  watcher.replayFromStart();
3302
+ this.startRediscovery(sessionId, agentType);
3173
3303
  return true;
3174
3304
  }
3175
3305
  /** Disable markdown streaming for a session. */
@@ -3178,8 +3308,50 @@ var TranscriptWatcherManager = class {
3178
3308
  if (!session) return;
3179
3309
  session.watcher.removeSubscriber();
3180
3310
  this.active.delete(sessionId);
3311
+ this.stopRediscovery(sessionId);
3181
3312
  console.log(`[rAgent] Stopped watching transcript for ${sessionId}`);
3182
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
+ }
3183
3355
  /** Handle sync-markdown request — send replay snapshot. */
3184
3356
  handleSyncRequest(sessionId, fromSeq) {
3185
3357
  const session = this.active.get(sessionId);
@@ -3191,9 +3363,11 @@ var TranscriptWatcherManager = class {
3191
3363
  stopAll() {
3192
3364
  for (const [sessionId, session] of this.active) {
3193
3365
  session.watcher.stop();
3366
+ this.stopRediscovery(sessionId);
3194
3367
  console.log(`[rAgent] Stopped transcript watcher for ${sessionId}`);
3195
3368
  }
3196
3369
  this.active.clear();
3370
+ this.rediscoveryTimers.clear();
3197
3371
  }
3198
3372
  /** Check if a session has an active watcher. */
3199
3373
  isWatching(sessionId) {
@@ -3249,7 +3423,7 @@ function resolveRunOptions(commandOptions) {
3249
3423
  const config = loadConfig();
3250
3424
  const portal = commandOptions.portal || config.portal || DEFAULT_PORTAL;
3251
3425
  const hostId = sanitizeHostId(commandOptions.id || config.hostId || inferHostId());
3252
- const hostName = commandOptions.name || config.hostName || os7.hostname();
3426
+ const hostName = commandOptions.name || config.hostName || os8.hostname();
3253
3427
  const command = commandOptions.command || config.command || "bash";
3254
3428
  const agentToken = commandOptions.agentToken || config.agentToken || "";
3255
3429
  return { portal, hostId, hostName, command, agentToken };
@@ -3260,6 +3434,11 @@ async function runAgent(rawOptions) {
3260
3434
  throw new Error("No agent token found. Run `ragent connect` first.");
3261
3435
  }
3262
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
+ }
3263
3442
  console.log(`[rAgent] Connector started for ${options.hostName} (${options.hostId})`);
3264
3443
  console.log(`[rAgent] Portal: ${options.portal}`);
3265
3444
  try {
@@ -3274,7 +3453,10 @@ async function runAgent(rawOptions) {
3274
3453
  const ptySessionId = `pty:${options.hostId}`;
3275
3454
  const sessionStreamer = new SessionStreamer(
3276
3455
  (sessionId, data) => {
3277
- if (!conn.isReady()) return;
3456
+ if (!conn.isReady()) {
3457
+ sessionStreamer.bufferOutput(sessionId, data);
3458
+ return;
3459
+ }
3278
3460
  if (conn.sessionKey) {
3279
3461
  const { enc, iv } = encryptPayload(data, conn.sessionKey);
3280
3462
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, sessionId });
@@ -3382,7 +3564,7 @@ async function runAgent(rawOptions) {
3382
3564
  sendToGroup(ws, groups.privateGroup, {
3383
3565
  type: "register",
3384
3566
  hostName: options.hostName,
3385
- environment: os7.platform()
3567
+ environment: os8.platform()
3386
3568
  });
3387
3569
  conn.replayBufferedOutput((chunk) => {
3388
3570
  sendToGroup(ws, groups.privateGroup, {
@@ -3391,6 +3573,17 @@ async function runAgent(rawOptions) {
3391
3573
  sessionId: ptySessionId
3392
3574
  });
3393
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
+ }
3394
3587
  await inventory.syncInventory(ws, groups, true);
3395
3588
  conn.startTimers(
3396
3589
  () => inventory.announceToRegistry(ws, groups.registryGroup, "heartbeat"),
@@ -3569,7 +3762,7 @@ function printCommandArt(title, subtitle = "remote agent control") {
3569
3762
  async function connectMachine(opts) {
3570
3763
  const portal = opts.portal || DEFAULT_PORTAL;
3571
3764
  const hostId = sanitizeHostId(opts.id || inferHostId());
3572
- const hostName = opts.name || os8.hostname();
3765
+ const hostName = opts.name || os9.hostname();
3573
3766
  const command = opts.command || "bash";
3574
3767
  if (!opts.token) {
3575
3768
  throw new Error("Connection token is required.");
@@ -3643,12 +3836,12 @@ function registerRunCommand(program2) {
3643
3836
  }
3644
3837
 
3645
3838
  // src/commands/doctor.ts
3646
- var os9 = __toESM(require("os"));
3839
+ var os10 = __toESM(require("os"));
3647
3840
  async function runDoctor(opts) {
3648
3841
  const options = resolveRunOptions(opts);
3649
3842
  const checks = [];
3650
- const platformOk = os9.platform() === "linux";
3651
- 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() });
3652
3845
  checks.push({
3653
3846
  name: "node",
3654
3847
  ok: Number(process.versions.node.split(".")[0]) >= 20,
@@ -3714,7 +3907,7 @@ function registerDoctorCommand(program2) {
3714
3907
 
3715
3908
  // src/commands/update.ts
3716
3909
  function registerUpdateCommand(program2) {
3717
- program2.command("update").description("Update ragent CLI from npm").option("--check", "Check for updates only; do not install").action(async (opts) => {
3910
+ program2.command("update").description("Update ragent CLI from npm").option("--check", "Check for updates only; do not install").option("--no-restart", "Do not restart the service after updating").action(async (opts) => {
3718
3911
  printCommandArt("Update", "checking npm registry for newer ragent-cli versions");
3719
3912
  const latestVersion = await checkForUpdate({ force: true });
3720
3913
  if (!latestVersion) {
@@ -3727,9 +3920,19 @@ function registerUpdateCommand(program2) {
3727
3920
  timeout: 5 * 60 * 1e3,
3728
3921
  maxBuffer: 10 * 1024 * 1024
3729
3922
  });
3730
- console.log(
3731
- "[rAgent] Update command completed. Restart any running ragent service."
3732
- );
3923
+ console.log(`[rAgent] Updated to ${latestVersion}.`);
3924
+ const backend = getConfiguredServiceBackend();
3925
+ if (backend && opts.restart) {
3926
+ console.log(`[rAgent] Restarting ${backend} service...`);
3927
+ try {
3928
+ await restartService();
3929
+ } catch (err) {
3930
+ console.error(`[rAgent] Failed to restart service: ${err instanceof Error ? err.message : err}`);
3931
+ console.log("[rAgent] Please restart manually: ragent service restart");
3932
+ }
3933
+ } else if (!backend) {
3934
+ console.log("[rAgent] No service backend detected. If ragent is running manually, restart it to use the new version.");
3935
+ }
3733
3936
  });
3734
3937
  }
3735
3938
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ragent-cli",
3
- "version": "1.5.2",
3
+ "version": "1.6.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": {