ragent-cli 1.10.0 → 1.11.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 (3) hide show
  1. package/dist/index.js +646 -231
  2. package/dist/sbom.json +1370 -2
  3. package/package.json +3 -2
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.10.0",
34
+ version: "1.11.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: {
@@ -86,6 +86,7 @@ var require_package = __commonJS({
86
86
  },
87
87
  dependencies: {
88
88
  "@azure/web-pubsub-client": "^1.0.2",
89
+ "@ragent/transcript-protocol": "*",
89
90
  commander: "^14.0.3",
90
91
  figlet: "^1.9.3",
91
92
  "node-pty": "^1.1.0",
@@ -97,7 +98,7 @@ var require_package = __commonJS({
97
98
  "@cyclonedx/cyclonedx-npm": "^4.2.1",
98
99
  "@eslint/js": "^10.0.1",
99
100
  "@types/figlet": "^1.7.0",
100
- "@types/node": "^20.17.0",
101
+ "@types/node": "^25.5.0",
101
102
  "@types/ws": "^8.5.13",
102
103
  eslint: "^10.0.2",
103
104
  tsup: "^8.4.0",
@@ -135,10 +136,51 @@ var DEFAULT_RECONNECT_DELAY_MS = 2500;
135
136
  var MAX_RECONNECT_DELAY_MS = 3e4;
136
137
  var OUTPUT_BUFFER_MAX_BYTES = 100 * 1024;
137
138
 
139
+ // src/log.ts
140
+ var LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
141
+ var configuredLevel = process.env.RAGENT_LOG_LEVEL && LEVELS[process.env.RAGENT_LOG_LEVEL] !== void 0 ? process.env.RAGENT_LOG_LEVEL : "info";
142
+ var threshold = LEVELS[configuredLevel];
143
+ function fmt(module2, message, fields) {
144
+ if (!fields || Object.keys(fields).length === 0) {
145
+ return `[${module2}] ${message}`;
146
+ }
147
+ const pairs = Object.entries(fields).map(([k, v]) => `${k}=${formatValue(v)}`).join(" ");
148
+ return `[${module2}] ${message} ${pairs}`;
149
+ }
150
+ function formatValue(v) {
151
+ if (v === null) return "null";
152
+ if (v === void 0) return "undefined";
153
+ if (typeof v === "string") return v.includes(" ") ? JSON.stringify(v) : v;
154
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
155
+ if (v instanceof Error) return JSON.stringify(v.message);
156
+ try {
157
+ return JSON.stringify(v);
158
+ } catch {
159
+ return "[unserializable]";
160
+ }
161
+ }
162
+ function createLogger(module2) {
163
+ return {
164
+ debug(message, fields) {
165
+ if (threshold <= LEVELS.debug) console.debug(fmt(module2, message, fields));
166
+ },
167
+ info(message, fields) {
168
+ if (threshold <= LEVELS.info) console.log(fmt(module2, message, fields));
169
+ },
170
+ warn(message, fields) {
171
+ if (threshold <= LEVELS.warn) console.warn(fmt(module2, message, fields));
172
+ },
173
+ error(message, fields) {
174
+ if (threshold <= LEVELS.error) console.error(fmt(module2, message, fields));
175
+ }
176
+ };
177
+ }
178
+
138
179
  // src/config.ts
139
180
  var crypto = __toESM(require("crypto"));
140
181
  var fs = __toESM(require("fs"));
141
182
  var os2 = __toESM(require("os"));
183
+ var log = createLogger("config");
142
184
  function ensureConfigDir() {
143
185
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
144
186
  }
@@ -147,9 +189,10 @@ function loadConfig() {
147
189
  const stat = fs.statSync(CONFIG_FILE);
148
190
  const mode = stat.mode & 511;
149
191
  if (mode & 63) {
150
- console.warn(
151
- `[rAgent] WARNING: Config file ${CONFIG_FILE} has overly permissive permissions (${mode.toString(8)}). Fixing to 0600.`
152
- );
192
+ log.warn("config file has overly permissive permissions; fixing to 0600", {
193
+ path: CONFIG_FILE,
194
+ mode: mode.toString(8)
195
+ });
153
196
  fs.chmodSync(CONFIG_FILE, 384);
154
197
  }
155
198
  return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
@@ -204,6 +247,7 @@ function inferHostId() {
204
247
  }
205
248
 
206
249
  // src/version.ts
250
+ var log2 = createLogger("version");
207
251
  function parseSemver(version) {
208
252
  const match = String(version || "").trim().match(/^(\d+)\.(\d+)\.(\d+)/);
209
253
  if (!match) return null;
@@ -260,9 +304,10 @@ async function maybeWarnUpdate() {
260
304
  if (process.env.RAGENT_DISABLE_UPDATE_CHECK === "true") return;
261
305
  const latestVersion = await checkForUpdate({ force: false });
262
306
  if (!latestVersion) return;
263
- console.log(
264
- `[rAgent] Update available: ${CURRENT_VERSION} -> ${latestVersion}. Run: ragent update`
265
- );
307
+ log2.info("update available; run: ragent update", {
308
+ current: CURRENT_VERSION,
309
+ latest: latestVersion
310
+ });
266
311
  }
267
312
 
268
313
  // src/commands/connect.ts
@@ -284,6 +329,7 @@ var os4 = __toESM(require("os"));
284
329
  // src/system.ts
285
330
  var import_node_child_process = require("child_process");
286
331
  var readline = __toESM(require("readline/promises"));
332
+ var log3 = createLogger("system");
287
333
  function wait(ms) {
288
334
  return new Promise((resolve) => setTimeout(resolve, ms));
289
335
  }
@@ -349,8 +395,8 @@ async function askYesNo(question, defaultYes = true) {
349
395
  async function installTmuxInteractively() {
350
396
  const recipe = await chooseTmuxInstallCommand();
351
397
  if (!recipe) {
352
- console.log("[rAgent] No supported package manager was detected for tmux auto-install.");
353
- console.log("[rAgent] Install tmux manually, then rerun: ragent doctor --fix");
398
+ log3.info("no supported package manager was detected for tmux auto-install");
399
+ log3.info("install tmux manually, then rerun: ragent doctor --fix");
354
400
  return false;
355
401
  }
356
402
  const approved = await askYesNo(
@@ -376,6 +422,14 @@ var os3 = __toESM(require("os"));
376
422
  var import_child_process = require("child_process");
377
423
 
378
424
  // src/transcript-parser-v2.ts
425
+ var log4 = createLogger("transcript-parser-v2");
426
+ var DEBUG_PARSE = process.env.RAGENT_DEBUG_TRANSCRIPT === "1";
427
+ function debugTranscriptParse(parser, line, error) {
428
+ if (!DEBUG_PARSE) return;
429
+ const message = error instanceof Error ? error.message : String(error);
430
+ const preview = line.length > 120 ? line.slice(0, 117) + "\u2026" : line;
431
+ log4.debug("skipping unparseable line", { parser, error: message, preview });
432
+ }
379
433
  var blockCounter = 0;
380
434
  function genBlockId(prefix) {
381
435
  return `${prefix}-${Date.now()}-${(++blockCounter).toString(36)}`;
@@ -421,7 +475,8 @@ var ClaudeCodeParserV2 = class {
421
475
  let obj;
422
476
  try {
423
477
  obj = JSON.parse(line);
424
- } catch {
478
+ } catch (error) {
479
+ debugTranscriptParse("claude", line, error);
425
480
  return [];
426
481
  }
427
482
  if (!obj.message?.content) return [];
@@ -613,7 +668,8 @@ var CodexCliParserV2 = class {
613
668
  let obj;
614
669
  try {
615
670
  obj = JSON.parse(line);
616
- } catch {
671
+ } catch (error) {
672
+ debugTranscriptParse("codex", line, error);
617
673
  return [];
618
674
  }
619
675
  const item = obj.response_item;
@@ -662,10 +718,10 @@ function getParserV2(agentType) {
662
718
  return factory ? factory() : void 0;
663
719
  }
664
720
 
665
- // src/transcript-protocol.ts
721
+ // ../packages/transcript-protocol/src/protocol.ts
666
722
  var TRANSCRIPT_SCHEMA_VERSION = 2;
667
723
 
668
- // src/transcript-reducer.ts
724
+ // ../packages/transcript-protocol/src/reducer.ts
669
725
  function createEmptySnapshot(sessionId) {
670
726
  return {
671
727
  schemaVersion: TRANSCRIPT_SCHEMA_VERSION,
@@ -868,6 +924,7 @@ function trimSnapshot(snapshot, maxTurns = MAX_SNAPSHOT_TURNS) {
868
924
  }
869
925
 
870
926
  // src/transcript-watcher.ts
927
+ var log5 = createLogger("transcript-watcher");
871
928
  var ClaudeCodeParser = class {
872
929
  name = "claude-code";
873
930
  /** Maps tool_use id → tool name for matching results back to tools. */
@@ -1211,12 +1268,18 @@ var TranscriptWatcher = class {
1211
1268
  addSubscriber() {
1212
1269
  this.subscriberCount++;
1213
1270
  }
1214
- /** Remove a subscriber. Stops watching when last subscriber leaves. */
1271
+ /**
1272
+ * Remove a subscriber. Stops watching when the last subscriber leaves.
1273
+ * Returns the remaining subscriber count so callers can synchronize
1274
+ * manager-side bookkeeping (e.g. removing the entry from the active map
1275
+ * only on the last disconnect).
1276
+ */
1215
1277
  removeSubscriber() {
1216
1278
  this.subscriberCount = Math.max(0, this.subscriberCount - 1);
1217
1279
  if (this.subscriberCount === 0) {
1218
1280
  this.stop();
1219
1281
  }
1282
+ return this.subscriberCount;
1220
1283
  }
1221
1284
  /** Stop watching. */
1222
1285
  stop() {
@@ -1379,15 +1442,15 @@ var TranscriptWatcherManager = class {
1379
1442
  }
1380
1443
  const parser = getParser(agentType);
1381
1444
  if (!parser) {
1382
- console.log(`[rAgent] No transcript parser for agent type "${agentType}"`);
1445
+ log5.info("no transcript parser for agent type", { agentType });
1383
1446
  return { ok: false, reason: "unsupported_agent" };
1384
1447
  }
1385
1448
  const filePath = discoverTranscriptFile(sessionId, agentType);
1386
1449
  if (!filePath) {
1387
- console.log(`[rAgent] No transcript file found for session ${sessionId}`);
1450
+ log5.info("no transcript file found", { sessionId });
1388
1451
  return { ok: false, reason: "no_transcript" };
1389
1452
  }
1390
- console.log(`[rAgent] Watching transcript: ${filePath} (${parser.name})`);
1453
+ log5.info("watching transcript", { path: filePath, parser: parser.name });
1391
1454
  const v2Parser = getParserV2(agentType);
1392
1455
  const watcher = new TranscriptWatcher(
1393
1456
  filePath,
@@ -1400,14 +1463,14 @@ var TranscriptWatcherManager = class {
1400
1463
  this.sendOpsFn(sessionId, ops, seq);
1401
1464
  } : void 0,
1402
1465
  onError: (error) => {
1403
- console.warn(`[rAgent] Transcript watcher error (${sessionId}): ${error}`);
1466
+ log5.warn("transcript watcher error", { sessionId, error: String(error) });
1404
1467
  }
1405
1468
  },
1406
1469
  v2Parser,
1407
1470
  sessionId
1408
1471
  );
1409
1472
  if (!watcher.start()) {
1410
- console.warn(`[rAgent] Failed to start watching ${filePath}`);
1473
+ log5.warn("failed to start watching", { path: filePath });
1411
1474
  return { ok: false, reason: "watch_failed" };
1412
1475
  }
1413
1476
  this.active.set(sessionId, { watcher, filePath, agentType: agentType ?? "" });
@@ -1419,10 +1482,17 @@ var TranscriptWatcherManager = class {
1419
1482
  disableMarkdown(sessionId) {
1420
1483
  const session = this.active.get(sessionId);
1421
1484
  if (!session) return;
1422
- session.watcher.removeSubscriber();
1485
+ const remaining = session.watcher.removeSubscriber();
1486
+ if (remaining > 0) {
1487
+ log5.info("viewer left transcript watcher; other viewers still subscribed", {
1488
+ sessionId,
1489
+ remaining
1490
+ });
1491
+ return;
1492
+ }
1423
1493
  this.active.delete(sessionId);
1424
1494
  this.stopRediscovery(sessionId);
1425
- console.log(`[rAgent] Stopped watching transcript for ${sessionId}`);
1495
+ log5.info("stopped watching transcript", { sessionId });
1426
1496
  }
1427
1497
  /** Periodically check if the transcript file has changed (new conversation). */
1428
1498
  startRediscovery(sessionId, agentType) {
@@ -1435,7 +1505,7 @@ var TranscriptWatcherManager = class {
1435
1505
  }
1436
1506
  const newPath = discoverTranscriptFile(sessionId, agentType);
1437
1507
  if (!newPath || newPath === session.filePath) return;
1438
- console.log(`[rAgent] Transcript file changed for ${sessionId}: ${newPath}`);
1508
+ log5.info("transcript file changed", { sessionId, newPath });
1439
1509
  session.watcher.stop();
1440
1510
  const parser = getParser(agentType);
1441
1511
  if (!parser) return;
@@ -1451,14 +1521,14 @@ var TranscriptWatcherManager = class {
1451
1521
  this.sendOpsFn(sessionId, ops, seq);
1452
1522
  } : void 0,
1453
1523
  onError: (error) => {
1454
- console.warn(`[rAgent] Transcript watcher error (${sessionId}): ${error}`);
1524
+ log5.warn("transcript watcher error", { sessionId, error: String(error) });
1455
1525
  }
1456
1526
  },
1457
1527
  newV2Parser,
1458
1528
  sessionId
1459
1529
  );
1460
1530
  if (!newWatcher.start()) {
1461
- console.warn(`[rAgent] Failed to start watching new transcript ${newPath}`);
1531
+ log5.warn("failed to start watching new transcript", { newPath });
1462
1532
  return;
1463
1533
  }
1464
1534
  this.sendSnapshotFn(sessionId, [], 0);
@@ -1494,7 +1564,7 @@ var TranscriptWatcherManager = class {
1494
1564
  for (const [sessionId, session] of this.active) {
1495
1565
  session.watcher.stop();
1496
1566
  this.stopRediscovery(sessionId);
1497
- console.log(`[rAgent] Stopped transcript watcher for ${sessionId}`);
1567
+ log5.info("stopped transcript watcher", { sessionId });
1498
1568
  }
1499
1569
  this.active.clear();
1500
1570
  this.rediscoveryTimers.clear();
@@ -1990,6 +2060,7 @@ async function getChildAgentInfo(parentPid, visited = /* @__PURE__ */ new Set())
1990
2060
  }
1991
2061
 
1992
2062
  // src/auth.ts
2063
+ var log6 = createLogger("auth");
1993
2064
  var AuthError = class extends Error {
1994
2065
  constructor(message) {
1995
2066
  super(message);
@@ -2007,7 +2078,7 @@ function decodeJwtExp(token) {
2007
2078
  }
2008
2079
  }
2009
2080
  async function startSessionWithMachineSecret(params) {
2010
- console.log("[rAgent] Attempting session recovery with machine credential...");
2081
+ log6.info("attempting session recovery with machine credential");
2011
2082
  const response = await fetch(`${params.portal}/api/agent/session/start`, {
2012
2083
  method: "POST",
2013
2084
  headers: { "Content-Type": "application/json" },
@@ -2040,7 +2111,7 @@ async function startSessionWithMachineSecret(params) {
2040
2111
  patch.refreshExpiresAt = data.refreshExpiresAt || "";
2041
2112
  }
2042
2113
  saveConfigPatch(patch);
2043
- console.log("[rAgent] Session recovered via machine credential.");
2114
+ log6.info("session recovered via machine credential");
2044
2115
  return data.agentToken;
2045
2116
  }
2046
2117
  async function refreshTokenIfNeeded(params) {
@@ -2049,11 +2120,9 @@ async function refreshTokenIfNeeded(params) {
2049
2120
  const exp = decodeJwtExp(params.agentToken);
2050
2121
  if (!exp) return params.agentToken;
2051
2122
  const remainingSeconds = exp - Math.floor(Date.now() / 1e3);
2052
- const threshold = remainingSeconds < 3600 ? 120 : 3600;
2053
- if (remainingSeconds > threshold) return params.agentToken;
2054
- console.log(
2055
- `[rAgent] Token expires in ${Math.round(remainingSeconds / 60)}m \u2014 refreshing...`
2056
- );
2123
+ const threshold2 = remainingSeconds < 3600 ? 120 : 3600;
2124
+ if (remainingSeconds > threshold2) return params.agentToken;
2125
+ log6.info("token expiring \u2014 refreshing", { minutesRemaining: Math.round(remainingSeconds / 60) });
2057
2126
  try {
2058
2127
  const headers = { "Content-Type": "application/json" };
2059
2128
  let body = {};
@@ -2069,9 +2138,7 @@ async function refreshTokenIfNeeded(params) {
2069
2138
  });
2070
2139
  if (!response.ok) {
2071
2140
  const errorData = await response.json().catch(() => ({}));
2072
- console.warn(
2073
- `[rAgent] Token refresh failed: ${errorData.error || response.status}`
2074
- );
2141
+ log6.warn("token refresh failed", { error: errorData.error || response.status });
2075
2142
  if (config.machineSecret && config.hostId) {
2076
2143
  try {
2077
2144
  return await startSessionWithMachineSecret({
@@ -2082,7 +2149,7 @@ async function refreshTokenIfNeeded(params) {
2082
2149
  } catch (mcError) {
2083
2150
  if (mcError instanceof AuthError) throw mcError;
2084
2151
  const mcMessage = mcError instanceof Error ? mcError.message : String(mcError);
2085
- console.warn(`[rAgent] Machine credential recovery failed: ${mcMessage}`);
2152
+ log6.warn("machine credential recovery failed", { error: mcMessage });
2086
2153
  }
2087
2154
  }
2088
2155
  return params.agentToken;
@@ -2101,12 +2168,12 @@ async function refreshTokenIfNeeded(params) {
2101
2168
  patch.machineSecret = data.machineSecret;
2102
2169
  }
2103
2170
  saveConfigPatch(patch);
2104
- console.log("[rAgent] Token refreshed successfully.");
2171
+ log6.info("token refreshed successfully");
2105
2172
  return data.agentToken;
2106
2173
  } catch (error) {
2107
2174
  if (error instanceof AuthError) throw error;
2108
2175
  const message = error instanceof Error ? error.message : String(error);
2109
- console.warn(`[rAgent] Token refresh error: ${message}`);
2176
+ log6.warn("token refresh error", { error: message });
2110
2177
  return params.agentToken;
2111
2178
  }
2112
2179
  }
@@ -2169,7 +2236,7 @@ async function deregisterHost(params) {
2169
2236
  body: JSON.stringify({})
2170
2237
  });
2171
2238
  if (response.status === 401 || response.status === 403) {
2172
- console.warn("[rAgent] Token expired \u2014 could not deregister from server. Host may remain visible in dashboard until manually removed.");
2239
+ log6.warn("token expired \u2014 could not deregister from server. Host may remain visible in dashboard until manually removed");
2173
2240
  return false;
2174
2241
  }
2175
2242
  return response.ok;
@@ -2275,13 +2342,42 @@ function redactSecrets(str) {
2275
2342
  }
2276
2343
  return result;
2277
2344
  }
2345
+ var streamingTails = /* @__PURE__ */ new Map();
2346
+ var MAX_STREAMING_TAIL = 1024;
2347
+ var SECRET_PREFIX_RE = /(?:AKIA[0-9A-Z]*|gh[ps]_[A-Za-z0-9_]*|sk_(?:live|test)_[A-Za-z0-9]*|eyJ[A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]*){0,2})$/;
2348
+ function redactStreamingChunk(sessionId, chunk) {
2349
+ if (!redactionEnabled) return chunk;
2350
+ const prevTail = streamingTails.get(sessionId) ?? "";
2351
+ const combined = prevTail + chunk;
2352
+ const redacted = redactSecrets(combined);
2353
+ const prefix = SECRET_PREFIX_RE.exec(redacted);
2354
+ if (prefix && prefix.index + prefix[0].length === redacted.length && redacted.length - prefix.index <= MAX_STREAMING_TAIL) {
2355
+ streamingTails.set(sessionId, redacted.slice(prefix.index));
2356
+ return redacted.slice(0, prefix.index);
2357
+ }
2358
+ streamingTails.delete(sessionId);
2359
+ return redacted;
2360
+ }
2361
+ function flushStreamingRedaction(sessionId) {
2362
+ const tail = streamingTails.get(sessionId) ?? "";
2363
+ streamingTails.delete(sessionId);
2364
+ return redactionEnabled ? redactSecrets(tail) : tail;
2365
+ }
2278
2366
  var pendingQueue = [];
2279
2367
  var drainTimer = null;
2280
2368
  var drainWs = null;
2281
2369
  var droppedFrames = 0;
2370
+ var PRESSURE_HIGH = 0.75;
2371
+ var PRESSURE_LOW = 0.25;
2372
+ var pressureIsHigh = false;
2373
+ function getQueuePressure() {
2374
+ const level = pendingQueue.length / MAX_PENDING_QUEUE;
2375
+ if (level >= PRESSURE_HIGH) pressureIsHigh = true;
2376
+ else if (level < PRESSURE_LOW) pressureIsHigh = false;
2377
+ return { level, isHigh: pressureIsHigh };
2378
+ }
2282
2379
  function drainQueue() {
2283
2380
  if (!drainWs || drainWs.readyState !== import_ws.default.OPEN) {
2284
- pendingQueue.length = 0;
2285
2381
  stopDrainTimer();
2286
2382
  return;
2287
2383
  }
@@ -2330,6 +2426,7 @@ function sendToGroup(ws, group, data) {
2330
2426
  }
2331
2427
  if (pendingQueue.length > 0) {
2332
2428
  pendingQueue.push(frame);
2429
+ drainWs = ws;
2333
2430
  drainQueue();
2334
2431
  return;
2335
2432
  }
@@ -2362,6 +2459,8 @@ var import_node_path = require("path");
2362
2459
  var import_node_os = require("os");
2363
2460
  var import_node_string_decoder = require("string_decoder");
2364
2461
  var pty = __toESM(require("node-pty"));
2462
+ var log7 = createLogger("session-streamer");
2463
+ var BACKPRESSURE_RESUME_CHECK_MS = 100;
2365
2464
  var STOP_DEBOUNCE_MS = 2e3;
2366
2465
  var MAX_CONCURRENT_STREAMS = 20;
2367
2466
  var MAX_SESSION_BUFFER_BYTES = 64 * 1024;
@@ -2427,10 +2526,54 @@ var SessionStreamer = class {
2427
2526
  */
2428
2527
  disconnectBuffers = /* @__PURE__ */ new Map();
2429
2528
  disconnectBufferBytes = /* @__PURE__ */ new Map();
2529
+ /** Timer that resumes paused PTYs once the WS queue drains. See #332. */
2530
+ backpressureResumeTimer = null;
2430
2531
  constructor(sendFn, onStreamStopped) {
2431
2532
  this.sendFn = sendFn;
2432
2533
  this.onStreamStopped = onStreamStopped;
2433
2534
  this.cleanupStaleStreams();
2535
+ this.startBackpressureResumeTimer();
2536
+ }
2537
+ /**
2538
+ * When the WS queue is saturated, PTY onData stops firing after we call
2539
+ * ptyProc.pause(). We need an independent loop to check pressure and
2540
+ * resume each paused PTY once the queue drains. See #332.
2541
+ */
2542
+ startBackpressureResumeTimer() {
2543
+ if (this.backpressureResumeTimer) return;
2544
+ this.backpressureResumeTimer = setInterval(() => {
2545
+ const pressure = getQueuePressure();
2546
+ if (pressure.isHigh) return;
2547
+ for (const stream of this.active.values()) {
2548
+ if (stream.ptyPaused && stream.ptyProc && !stream.stopped) {
2549
+ try {
2550
+ stream.ptyProc.resume();
2551
+ stream.ptyPaused = false;
2552
+ log7.info("pty-attach resumed after WS queue drained", { sessionId: stream.sessionId });
2553
+ } catch {
2554
+ }
2555
+ }
2556
+ }
2557
+ }, BACKPRESSURE_RESUME_CHECK_MS);
2558
+ }
2559
+ /**
2560
+ * Pause a PTY-attach stream when WS queue pressure is high. Only applies
2561
+ * to `pty-attach` streams — tmux-pipe (FIFO cat) and process-trace
2562
+ * (strace) don't have a pause primitive in this implementation.
2563
+ */
2564
+ applyBackpressure(stream) {
2565
+ if (stream.streamType !== "pty-attach" || !stream.ptyProc || stream.ptyPaused) return;
2566
+ const pressure = getQueuePressure();
2567
+ if (!pressure.isHigh) return;
2568
+ try {
2569
+ stream.ptyProc.pause();
2570
+ stream.ptyPaused = true;
2571
+ log7.warn("pty-attach paused under WS backpressure", {
2572
+ sessionId: stream.sessionId,
2573
+ queueLevel: pressure.level.toFixed(2)
2574
+ });
2575
+ } catch {
2576
+ }
2434
2577
  }
2435
2578
  /**
2436
2579
  * Remove orphaned FIFO directories left behind by a previous crash.
@@ -2449,7 +2592,7 @@ var SessionStreamer = class {
2449
2592
  }
2450
2593
  }
2451
2594
  if (entries.some((e) => e.startsWith("s-"))) {
2452
- console.log("[rAgent] Cleaned up stale stream directories from previous run.");
2595
+ log7.info("cleaned up stale stream directories from previous run");
2453
2596
  }
2454
2597
  } catch {
2455
2598
  }
@@ -2467,7 +2610,7 @@ var SessionStreamer = class {
2467
2610
  if (viewerKey) {
2468
2611
  this.active.get(sessionId)?.viewerIds.add(viewerKey);
2469
2612
  }
2470
- console.log(`[rAgent] Cancelled pending stop for: ${sessionId} (re-mounted)`);
2613
+ log7.info("cancelled pending stop (re-mounted)", { sessionId });
2471
2614
  return true;
2472
2615
  }
2473
2616
  }
@@ -2478,9 +2621,10 @@ var SessionStreamer = class {
2478
2621
  return true;
2479
2622
  }
2480
2623
  if (this.active.size >= MAX_CONCURRENT_STREAMS) {
2481
- console.warn(
2482
- `[rAgent] Stream limit reached (${MAX_CONCURRENT_STREAMS}). Rejecting: ${sessionId}`
2483
- );
2624
+ log7.warn("stream limit reached; rejecting", {
2625
+ limit: MAX_CONCURRENT_STREAMS,
2626
+ sessionId
2627
+ });
2484
2628
  return false;
2485
2629
  }
2486
2630
  if (sessionId.startsWith("tmux:")) {
@@ -2508,7 +2652,7 @@ var SessionStreamer = class {
2508
2652
  stream.ptyProc.write(data);
2509
2653
  } catch (error) {
2510
2654
  const message = error instanceof Error ? error.message : String(error);
2511
- console.warn(`[rAgent] Failed to write input to ${sessionId}: ${message}`);
2655
+ log7.warn("failed to write input", { sessionId, error: message });
2512
2656
  }
2513
2657
  }
2514
2658
  /**
@@ -2522,7 +2666,7 @@ var SessionStreamer = class {
2522
2666
  } catch (error) {
2523
2667
  const message = error instanceof Error ? error.message : String(error);
2524
2668
  if (!message.includes("EBADF")) {
2525
- console.warn(`[rAgent] Resize failed for ${sessionId}: ${message}`);
2669
+ log7.warn("resize failed", { sessionId, error: message });
2526
2670
  }
2527
2671
  }
2528
2672
  }
@@ -2548,7 +2692,7 @@ var SessionStreamer = class {
2548
2692
  this.active.delete(sessionId);
2549
2693
  this.disconnectBuffers.delete(sessionId);
2550
2694
  this.disconnectBufferBytes.delete(sessionId);
2551
- console.log(`[rAgent] Stopped streaming: ${sessionId}`);
2695
+ log7.info("stopped streaming", { sessionId });
2552
2696
  }, STOP_DEBOUNCE_MS);
2553
2697
  this.pendingStops.set(sessionId, timer);
2554
2698
  }
@@ -2568,11 +2712,15 @@ var SessionStreamer = class {
2568
2712
  this.pendingStops.clear();
2569
2713
  for (const [id, stream] of this.active) {
2570
2714
  this.cleanupStream(stream);
2571
- console.log(`[rAgent] Stopped streaming: ${id}`);
2715
+ log7.info("stopped streaming", { sessionId: id });
2572
2716
  }
2573
2717
  this.active.clear();
2574
2718
  this.disconnectBuffers.clear();
2575
2719
  this.disconnectBufferBytes.clear();
2720
+ if (this.backpressureResumeTimer) {
2721
+ clearInterval(this.backpressureResumeTimer);
2722
+ this.backpressureResumeTimer = null;
2723
+ }
2576
2724
  }
2577
2725
  /**
2578
2726
  * Get the number of active streams.
@@ -2608,7 +2756,9 @@ var SessionStreamer = class {
2608
2756
  currentBytes -= Buffer.byteLength(evicted, "utf8");
2609
2757
  }
2610
2758
  if (dataBytes > MAX_SESSION_BUFFER_BYTES) {
2611
- const tail = data.slice(data.length - MAX_SESSION_BUFFER_BYTES);
2759
+ const buf = Buffer.from(data, "utf8");
2760
+ const tailBuf = buf.subarray(buf.length - MAX_SESSION_BUFFER_BYTES);
2761
+ const tail = tailBuf.toString("utf8");
2612
2762
  chunks.push(tail);
2613
2763
  this.disconnectBufferBytes.set(sessionId, Buffer.byteLength(tail, "utf8"));
2614
2764
  return;
@@ -2695,13 +2845,13 @@ var SessionStreamer = class {
2695
2845
  }
2696
2846
  stream.initializing = false;
2697
2847
  stream.initBuffer = [];
2698
- console.log(`[rAgent] Resync capture for: ${sessionId}`);
2848
+ log7.info("resync capture", { sessionId });
2699
2849
  return true;
2700
2850
  } catch (error) {
2701
2851
  stream.initializing = false;
2702
2852
  stream.initBuffer = [];
2703
2853
  const message = error instanceof Error ? error.message : String(error);
2704
- console.warn(`[rAgent] Failed to resync stream for ${sessionId}: ${message}`);
2854
+ log7.warn("failed to resync stream", { sessionId, error: message });
2705
2855
  return false;
2706
2856
  }
2707
2857
  }
@@ -2754,7 +2904,7 @@ var SessionStreamer = class {
2754
2904
  } catch (error) {
2755
2905
  this.cleanupStream(stream);
2756
2906
  const message = error instanceof Error ? error.message : String(error);
2757
- console.warn(`[rAgent] Failed pipe-pane for ${sessionId}: ${message}`);
2907
+ log7.warn("failed pipe-pane", { sessionId, error: message });
2758
2908
  return false;
2759
2909
  }
2760
2910
  const catProc = (0, import_node_child_process2.spawn)("cat", [fifoPath], { env: cleanEnv, stdio: ["ignore", "pipe", "ignore"] });
@@ -2776,7 +2926,7 @@ var SessionStreamer = class {
2776
2926
  this.cleanupStream(stream);
2777
2927
  this.active.delete(sessionId);
2778
2928
  this.pendingStops.delete(sessionId);
2779
- console.log(`[rAgent] Session stream ended: ${sessionId}`);
2929
+ log7.info("session stream ended", { sessionId });
2780
2930
  this.onStreamStopped?.(sessionId);
2781
2931
  }
2782
2932
  });
@@ -2836,11 +2986,11 @@ var SessionStreamer = class {
2836
2986
  }
2837
2987
  stream.initializing = false;
2838
2988
  stream.initBuffer = [];
2839
- console.log(`[rAgent] Started streaming: ${sessionId} (pane: ${paneTarget})`);
2989
+ log7.info("started streaming (tmux)", { sessionId, pane: paneTarget });
2840
2990
  return true;
2841
2991
  } catch (error) {
2842
2992
  const message = error instanceof Error ? error.message : String(error);
2843
- console.warn(`[rAgent] Failed to start stream for ${sessionId}: ${message}`);
2993
+ log7.warn("failed to start stream", { sessionId, error: message });
2844
2994
  return false;
2845
2995
  }
2846
2996
  }
@@ -2877,6 +3027,7 @@ var SessionStreamer = class {
2877
3027
  proc.onData((data) => {
2878
3028
  if (!stream.stopped) {
2879
3029
  this.sendFn(sessionId, data);
3030
+ this.applyBackpressure(stream);
2880
3031
  }
2881
3032
  });
2882
3033
  proc.onExit(() => {
@@ -2884,15 +3035,15 @@ var SessionStreamer = class {
2884
3035
  stream.stopped = true;
2885
3036
  this.active.delete(sessionId);
2886
3037
  this.pendingStops.delete(sessionId);
2887
- console.log(`[rAgent] Session stream ended: ${sessionId}`);
3038
+ log7.info("session stream ended", { sessionId });
2888
3039
  this.onStreamStopped?.(sessionId);
2889
3040
  }
2890
3041
  });
2891
- console.log(`[rAgent] Started streaming: ${sessionId} (screen: ${sessionName})`);
3042
+ log7.info("started streaming (screen)", { sessionId, screen: sessionName });
2892
3043
  return true;
2893
3044
  } catch (error) {
2894
3045
  const message = error instanceof Error ? error.message : String(error);
2895
- console.warn(`[rAgent] Failed to start screen stream for ${sessionId}: ${message}`);
3046
+ log7.warn("failed to start screen stream", { sessionId, error: message });
2896
3047
  return false;
2897
3048
  }
2898
3049
  }
@@ -2929,6 +3080,7 @@ var SessionStreamer = class {
2929
3080
  proc.onData((data) => {
2930
3081
  if (!stream.stopped) {
2931
3082
  this.sendFn(sessionId, data);
3083
+ this.applyBackpressure(stream);
2932
3084
  }
2933
3085
  });
2934
3086
  proc.onExit(() => {
@@ -2936,15 +3088,15 @@ var SessionStreamer = class {
2936
3088
  stream.stopped = true;
2937
3089
  this.active.delete(sessionId);
2938
3090
  this.pendingStops.delete(sessionId);
2939
- console.log(`[rAgent] Session stream ended: ${sessionId}`);
3091
+ log7.info("session stream ended", { sessionId });
2940
3092
  this.onStreamStopped?.(sessionId);
2941
3093
  }
2942
3094
  });
2943
- console.log(`[rAgent] Started streaming: ${sessionId} (zellij: ${sessionName})`);
3095
+ log7.info("started streaming (zellij)", { sessionId, zellij: sessionName });
2944
3096
  return true;
2945
3097
  } catch (error) {
2946
3098
  const message = error instanceof Error ? error.message : String(error);
2947
- console.warn(`[rAgent] Failed to start zellij stream for ${sessionId}: ${message}`);
3099
+ log7.warn("failed to start zellij stream", { sessionId, error: message });
2948
3100
  return false;
2949
3101
  }
2950
3102
  }
@@ -2955,14 +3107,14 @@ var SessionStreamer = class {
2955
3107
  const pid = parseProcessPid2(sessionId);
2956
3108
  if (!pid) return false;
2957
3109
  if (!(0, import_node_fs.existsSync)(`/proc/${pid}`)) {
2958
- console.warn(`[rAgent] Process ${pid} does not exist (no /proc/${pid}).`);
3110
+ log7.warn("process does not exist (no /proc entry)", { pid });
2959
3111
  this.sendFn(sessionId, "\r\n[rAgent] Process is no longer running. It will be removed on next sync.\r\n");
2960
3112
  return false;
2961
3113
  }
2962
3114
  try {
2963
3115
  const stat = (0, import_node_fs.readFileSync)(`/proc/${pid}/stat`, "utf8");
2964
3116
  if (stat.includes(") Z")) {
2965
- console.warn(`[rAgent] Process ${pid} is a zombie.`);
3117
+ log7.warn("process is a zombie", { pid });
2966
3118
  this.sendFn(sessionId, "\r\n[rAgent] Process has exited (zombie). It will be removed on next sync.\r\n");
2967
3119
  return false;
2968
3120
  }
@@ -3035,14 +3187,14 @@ var SessionStreamer = class {
3035
3187
  stream.stopped = true;
3036
3188
  this.active.delete(sessionId);
3037
3189
  this.pendingStops.delete(sessionId);
3038
- console.log(`[rAgent] Session stream ended: ${sessionId}`);
3190
+ log7.info("session stream ended", { sessionId });
3039
3191
  this.onStreamStopped?.(sessionId);
3040
3192
  });
3041
- console.log(`[rAgent] Started streaming: ${sessionId} (strace PID: ${pid})`);
3193
+ log7.info("started streaming (process trace)", { sessionId, pid });
3042
3194
  return true;
3043
3195
  } catch (error) {
3044
3196
  const message = error instanceof Error ? error.message : String(error);
3045
- console.warn(`[rAgent] Failed to start process stream for ${sessionId}: ${message}`);
3197
+ log7.warn("failed to start process stream", { sessionId, error: message });
3046
3198
  return false;
3047
3199
  }
3048
3200
  }
@@ -3093,25 +3245,34 @@ var SessionStreamer = class {
3093
3245
 
3094
3246
  // src/crypto-channel.ts
3095
3247
  var import_node_crypto = require("crypto");
3248
+ var log8 = createLogger("crypto-channel");
3249
+ var AAD_BYTES = 4;
3096
3250
  function deriveAesKey(sessionKey) {
3097
3251
  return Buffer.from(sessionKey.slice(0, 64), "hex");
3098
3252
  }
3099
- function encryptPayload(data, sessionKey) {
3253
+ function seqToAad(seq) {
3254
+ if (!Number.isInteger(seq) || seq < 0 || seq > 4294967295) {
3255
+ throw new Error(`Invalid seq ${seq}: must be a UInt32`);
3256
+ }
3257
+ const buf = Buffer.alloc(AAD_BYTES);
3258
+ buf.writeUInt32BE(seq, 0);
3259
+ return buf;
3260
+ }
3261
+ function encryptPayload(data, sessionKey, seq) {
3100
3262
  const key = deriveAesKey(sessionKey);
3101
3263
  const iv = (0, import_node_crypto.randomBytes)(12);
3102
3264
  const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", key, iv);
3103
- const encrypted = Buffer.concat([
3104
- cipher.update(data, "utf-8"),
3105
- cipher.final()
3106
- ]);
3265
+ cipher.setAAD(seqToAad(seq));
3266
+ const encrypted = Buffer.concat([cipher.update(data, "utf-8"), cipher.final()]);
3107
3267
  const authTag = cipher.getAuthTag();
3108
3268
  const combined = Buffer.concat([encrypted, authTag]);
3109
3269
  return {
3110
3270
  enc: combined.toString("base64"),
3111
- iv: iv.toString("base64")
3271
+ iv: iv.toString("base64"),
3272
+ seq
3112
3273
  };
3113
3274
  }
3114
- function decryptPayload(enc, iv, sessionKey) {
3275
+ function decryptPayload(enc, iv, sessionKey, seq) {
3115
3276
  try {
3116
3277
  const key = deriveAesKey(sessionKey);
3117
3278
  const ivBuf = Buffer.from(iv, "base64");
@@ -3120,20 +3281,47 @@ function decryptPayload(enc, iv, sessionKey) {
3120
3281
  const ciphertext = combined.subarray(0, combined.length - 16);
3121
3282
  const authTag = combined.subarray(combined.length - 16);
3122
3283
  const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", key, ivBuf);
3284
+ decipher.setAAD(seqToAad(seq));
3123
3285
  decipher.setAuthTag(authTag);
3124
- const decrypted = Buffer.concat([
3125
- decipher.update(ciphertext),
3126
- decipher.final()
3127
- ]);
3286
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
3128
3287
  return decrypted.toString("utf-8");
3129
- } catch {
3288
+ } catch (error) {
3289
+ const message = error instanceof Error ? error.message : String(error);
3290
+ log8.warn("decrypt failed", { seq, encBytes: enc.length, error: message });
3130
3291
  return null;
3131
3292
  }
3132
3293
  }
3294
+ var SequenceTracker = class {
3295
+ last = 0;
3296
+ /** Sender: produce the next outgoing seq. */
3297
+ next() {
3298
+ if (this.last >= 4294967295) {
3299
+ throw new Error("Sequence counter overflow; channel must re-handshake.");
3300
+ }
3301
+ this.last += 1;
3302
+ return this.last;
3303
+ }
3304
+ /** Receiver: accept if strictly greater than last-seen; else reject. */
3305
+ accept(seq) {
3306
+ if (!Number.isInteger(seq) || seq < 1 || seq > 4294967295) return false;
3307
+ if (seq <= this.last) return false;
3308
+ this.last = seq;
3309
+ return true;
3310
+ }
3311
+ /** Reset to zero (call on new handshake / sessionKey change). */
3312
+ reset() {
3313
+ this.last = 0;
3314
+ }
3315
+ /** Inspect the last-seen seq (0 means "nothing yet"). */
3316
+ get lastSeq() {
3317
+ return this.last;
3318
+ }
3319
+ };
3133
3320
 
3134
3321
  // src/pty.ts
3135
3322
  var import_node_child_process3 = require("child_process");
3136
3323
  var pty2 = __toESM(require("node-pty"));
3324
+ var log9 = createLogger("pty");
3137
3325
  function isInteractiveShell(command) {
3138
3326
  const trimmed = String(command).trim();
3139
3327
  return ["bash", "sh", "zsh", "fish"].includes(trimmed);
@@ -3166,7 +3354,7 @@ async function sendInputToTmux(sessionId, data) {
3166
3354
  if (!target) return;
3167
3355
  const sessionName = target.split(":")[0].split(".")[0];
3168
3356
  if (!/^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/.test(sessionName)) {
3169
- console.warn(`[rAgent] Invalid tmux session name: ${sessionName}`);
3357
+ log9.warn("invalid tmux session name", { sessionName });
3170
3358
  return;
3171
3359
  }
3172
3360
  try {
@@ -3178,7 +3366,7 @@ async function sendInputToTmux(sessionId, data) {
3178
3366
  });
3179
3367
  } catch (error) {
3180
3368
  const message = error instanceof Error ? error.message : String(error);
3181
- console.warn(`[rAgent] Failed to send input to ${sessionId}: ${message}`);
3369
+ log9.warn("failed to send input", { sessionId, error: message });
3182
3370
  }
3183
3371
  }
3184
3372
  async function stopAllDetachedTmuxSessions() {
@@ -3206,6 +3394,7 @@ async function stopAllDetachedTmuxSessions() {
3206
3394
  }
3207
3395
 
3208
3396
  // src/shell-manager.ts
3397
+ var log10 = createLogger("shell");
3209
3398
  var ShellManager = class {
3210
3399
  ptyProcess = null;
3211
3400
  suppressNextRespawn = false;
@@ -3246,7 +3435,7 @@ var ShellManager = class {
3246
3435
  } catch (error) {
3247
3436
  const message = error instanceof Error ? error.message : String(error);
3248
3437
  if (!message.includes("EBADF")) {
3249
- console.warn(`[rAgent] Resize failed: ${message}`);
3438
+ log10.warn("resize failed", { error: message });
3250
3439
  }
3251
3440
  }
3252
3441
  }
@@ -3262,7 +3451,7 @@ var ShellManager = class {
3262
3451
  return;
3263
3452
  }
3264
3453
  if (!this.shouldRun) return;
3265
- console.warn("[rAgent] Shell exited. Restarting shell process.");
3454
+ log10.warn("shell exited; restarting shell process");
3266
3455
  setTimeout(() => {
3267
3456
  if (this.shouldRun) this.spawnOrRespawn();
3268
3457
  }, 200);
@@ -3368,6 +3557,7 @@ var InventoryManager = class {
3368
3557
 
3369
3558
  // src/connection-manager.ts
3370
3559
  var import_ws3 = __toESM(require("ws"));
3560
+ var log11 = createLogger("connection");
3371
3561
  var OPPORTUNISTIC_INVENTORY_SYNC_MS = 1200;
3372
3562
  var MIN_INVENTORY_SYNC_GAP_MS = 1500;
3373
3563
  var ConnectionManager = class {
@@ -3375,6 +3565,31 @@ var ConnectionManager = class {
3375
3565
  activeGroups = { privateGroup: "", registryGroup: "" };
3376
3566
  sessionKey = null;
3377
3567
  reconnectDelay = DEFAULT_RECONNECT_DELAY_MS;
3568
+ /** Outbound frame counter; reset on every new handshake. See #310. */
3569
+ txSeq = new SequenceTracker();
3570
+ /**
3571
+ * Inbound frame trackers, keyed by viewerId. The portal's Terminal.tsx
3572
+ * initializes a fresh txSeq (starting at 1) on every component mount, so a
3573
+ * browser refresh — or a second viewer joining — cannot reuse the tracker
3574
+ * state of a prior session without triggering replay rejection. See #392.
3575
+ * A reserved `_legacy` key catches frames from pre-viewerId clients.
3576
+ */
3577
+ rxSeqByViewer = /* @__PURE__ */ new Map();
3578
+ /**
3579
+ * Fetch (or lazily create) the inbound sequence tracker for `viewerId`.
3580
+ * Untrusted input: an empty or missing viewerId falls back to `_legacy`,
3581
+ * which preserves compatibility with older portal builds that didn't send
3582
+ * the field.
3583
+ */
3584
+ getRxSeqForViewer(viewerId) {
3585
+ const key = typeof viewerId === "string" && viewerId.length > 0 ? viewerId : "_legacy";
3586
+ let tracker = this.rxSeqByViewer.get(key);
3587
+ if (!tracker) {
3588
+ tracker = new SequenceTracker();
3589
+ this.rxSeqByViewer.set(key, tracker);
3590
+ }
3591
+ return tracker;
3592
+ }
3378
3593
  wsHeartbeatTimer = null;
3379
3594
  inventorySyncTimer = null;
3380
3595
  inventorySyncKickTimer = null;
@@ -3399,6 +3614,10 @@ var ConnectionManager = class {
3399
3614
  setConnection(ws, groups, sessionKey) {
3400
3615
  this.activeSocket = ws;
3401
3616
  this.activeGroups = groups;
3617
+ if (sessionKey !== this.sessionKey) {
3618
+ this.txSeq.reset();
3619
+ this.rxSeqByViewer.clear();
3620
+ }
3402
3621
  this.sessionKey = sessionKey;
3403
3622
  }
3404
3623
  /** Reset reconnect delay on successful connection. */
@@ -3421,7 +3640,7 @@ var ConnectionManager = class {
3421
3640
  if (!this.activeSocket || this.activeSocket.readyState !== import_ws3.default.OPEN) return;
3422
3641
  this.activeSocket.ping();
3423
3642
  this.wsPongTimeout = setTimeout(() => {
3424
- console.warn("[rAgent] No pong received within 10s \u2014 closing stale connection.");
3643
+ log11.warn("no pong received within 10s; closing stale connection");
3425
3644
  try {
3426
3645
  this.activeSocket?.terminate();
3427
3646
  } catch {
@@ -3510,9 +3729,10 @@ var ConnectionManager = class {
3510
3729
  replayBufferedOutput(sendChunk) {
3511
3730
  const buffered = this.outputBuffer.drain();
3512
3731
  if (buffered.length > 0) {
3513
- console.log(
3514
- `[rAgent] Replaying ${buffered.length} buffered output chunks (${buffered.reduce((sum, c) => sum + Buffer.byteLength(c, "utf8"), 0)} bytes)`
3515
- );
3732
+ log11.info("replaying buffered output chunks", {
3733
+ chunks: buffered.length,
3734
+ bytes: buffered.reduce((sum, c) => sum + Buffer.byteLength(c, "utf8"), 0)
3735
+ });
3516
3736
  for (const chunk of buffered) {
3517
3737
  sendChunk(chunk);
3518
3738
  }
@@ -3548,6 +3768,7 @@ var import_child_process3 = require("child_process");
3548
3768
  var fs4 = __toESM(require("fs"));
3549
3769
  var os7 = __toESM(require("os"));
3550
3770
  var path3 = __toESM(require("path"));
3771
+ var log12 = createLogger("service");
3551
3772
  function assertConfiguredAgentToken() {
3552
3773
  const config = loadConfig();
3553
3774
  if (!config.agentToken) {
@@ -3613,7 +3834,7 @@ async function installSystemdService(opts = {}) {
3613
3834
  await runSystemctlUser(["restart", SERVICE_NAME]);
3614
3835
  }
3615
3836
  saveConfigPatch({ serviceBackend: "systemd" });
3616
- console.log(`[rAgent] Installed systemd user service at ${SERVICE_FILE}`);
3837
+ log12.info("installed systemd user service", { path: SERVICE_FILE });
3617
3838
  }
3618
3839
  function readFallbackPid() {
3619
3840
  try {
@@ -3652,7 +3873,7 @@ async function startPidfileService() {
3652
3873
  assertConfiguredAgentToken();
3653
3874
  const existingPid = readFallbackPid();
3654
3875
  if (existingPid && isProcessRunning(existingPid)) {
3655
- console.log(`[rAgent] Service already running (pid ${existingPid})`);
3876
+ log12.info("service already running", { pid: existingPid });
3656
3877
  return;
3657
3878
  }
3658
3879
  ensureConfigDir();
@@ -3669,8 +3890,8 @@ async function startPidfileService() {
3669
3890
  fs4.writeFileSync(FALLBACK_PID_FILE, `${child.pid}
3670
3891
  `, "utf8");
3671
3892
  saveConfigPatch({ serviceBackend: "pidfile" });
3672
- console.log(`[rAgent] Started fallback background service (pid ${child.pid})`);
3673
- console.log(`[rAgent] Logs: ${FALLBACK_LOG_FILE}`);
3893
+ log12.info("started fallback background service", { pid: child.pid });
3894
+ log12.info("logs", { path: FALLBACK_LOG_FILE });
3674
3895
  }
3675
3896
  async function stopPidfileService() {
3676
3897
  const pid = readFallbackPid();
@@ -3679,7 +3900,7 @@ async function stopPidfileService() {
3679
3900
  fs4.unlinkSync(FALLBACK_PID_FILE);
3680
3901
  } catch {
3681
3902
  }
3682
- console.log("[rAgent] Service is not running.");
3903
+ log12.info("service is not running");
3683
3904
  return;
3684
3905
  }
3685
3906
  process.kill(pid, "SIGTERM");
@@ -3694,7 +3915,7 @@ async function stopPidfileService() {
3694
3915
  fs4.unlinkSync(FALLBACK_PID_FILE);
3695
3916
  } catch {
3696
3917
  }
3697
- console.log(`[rAgent] Stopped fallback background service (pid ${pid})`);
3918
+ log12.info("stopped fallback background service", { pid });
3698
3919
  }
3699
3920
  async function ensureServiceInstalled(opts = {}) {
3700
3921
  const wantsSystemd = await canUseSystemdUser();
@@ -3706,9 +3927,7 @@ async function ensureServiceInstalled(opts = {}) {
3706
3927
  if (opts.start) {
3707
3928
  await startPidfileService();
3708
3929
  } else {
3709
- console.log(
3710
- "[rAgent] systemd user manager unavailable; using fallback pidfile backend."
3711
- );
3930
+ log12.info("systemd user manager unavailable; using fallback pidfile backend");
3712
3931
  }
3713
3932
  return "pidfile";
3714
3933
  }
@@ -3716,7 +3935,7 @@ async function startService() {
3716
3935
  const backend = getConfiguredServiceBackend();
3717
3936
  if (backend === "systemd") {
3718
3937
  await runSystemctlUser(["start", SERVICE_NAME]);
3719
- console.log("[rAgent] Started service via systemd.");
3938
+ log12.info("started service via systemd");
3720
3939
  return;
3721
3940
  }
3722
3941
  if (backend === "pidfile") {
@@ -3729,7 +3948,7 @@ async function stopService() {
3729
3948
  const backend = getConfiguredServiceBackend();
3730
3949
  if (backend === "systemd") {
3731
3950
  await runSystemctlUser(["stop", SERVICE_NAME]);
3732
- console.log("[rAgent] Stopped service via systemd.");
3951
+ log12.info("stopped service via systemd");
3733
3952
  return;
3734
3953
  }
3735
3954
  await stopPidfileService();
@@ -3747,7 +3966,7 @@ function killStaleAgentProcesses() {
3747
3966
  try {
3748
3967
  process.kill(pid, 0);
3749
3968
  process.kill(pid, "SIGTERM");
3750
- console.log(`[rAgent] Stopped stale agent process (pid ${pid})`);
3969
+ log12.info("stopped stale agent process", { pid });
3751
3970
  } catch {
3752
3971
  }
3753
3972
  fs4.unlinkSync(pidPath);
@@ -3762,7 +3981,7 @@ async function restartService() {
3762
3981
  killStaleAgentProcesses();
3763
3982
  if (backend === "systemd") {
3764
3983
  await runSystemctlUser(["restart", SERVICE_NAME]);
3765
- console.log("[rAgent] Restarted service via systemd.");
3984
+ log12.info("restarted service via systemd");
3766
3985
  return;
3767
3986
  }
3768
3987
  await stopPidfileService();
@@ -3778,17 +3997,17 @@ async function printServiceStatus() {
3778
3997
  if (status) {
3779
3998
  process.stdout.write(status);
3780
3999
  } else {
3781
- console.log("[rAgent] systemd service is not installed or has no status.");
4000
+ log12.info("systemd service is not installed or has no status");
3782
4001
  }
3783
4002
  return;
3784
4003
  }
3785
4004
  const pid = readFallbackPid();
3786
4005
  if (pid && isProcessRunning(pid)) {
3787
- console.log(`[rAgent] fallback service running (pid ${pid})`);
3788
- console.log(`[rAgent] logs: ${FALLBACK_LOG_FILE}`);
4006
+ log12.info("fallback service running", { pid });
4007
+ log12.info("logs", { path: FALLBACK_LOG_FILE });
3789
4008
  return;
3790
4009
  }
3791
- console.log("[rAgent] service is not running.");
4010
+ log12.info("service is not running");
3792
4011
  }
3793
4012
  async function printServiceLogs(opts) {
3794
4013
  const lines = Number.parseInt(String(opts.lines || 100), 10) || 100;
@@ -3814,7 +4033,7 @@ async function printServiceLogs(opts) {
3814
4033
  return;
3815
4034
  }
3816
4035
  if (!fs4.existsSync(FALLBACK_LOG_FILE)) {
3817
- console.log(`[rAgent] No log file found at ${FALLBACK_LOG_FILE}`);
4036
+ log12.info("no log file found", { path: FALLBACK_LOG_FILE });
3818
4037
  return;
3819
4038
  }
3820
4039
  if (follow) {
@@ -3846,7 +4065,7 @@ async function uninstallService() {
3846
4065
  const config = loadConfig();
3847
4066
  delete config.serviceBackend;
3848
4067
  saveConfig(config);
3849
- console.log("[rAgent] Service uninstalled.");
4068
+ log12.info("service uninstalled");
3850
4069
  }
3851
4070
  function requestStopSelfService() {
3852
4071
  const backend = getConfiguredServiceBackend();
@@ -4071,7 +4290,105 @@ async function executeProvision(request, onProgress) {
4071
4290
  return startAgent(request, emit);
4072
4291
  }
4073
4292
 
4293
+ // src/command-guard.ts
4294
+ var import_node_path2 = require("path");
4295
+ var posixNormalize = import_node_path2.posix.normalize;
4296
+ var PATTERNS = [
4297
+ // Any command/expression substitution. Legitimate agent launchers never
4298
+ // need these — forbidding them blocks smuggled commands like
4299
+ // `claude $(rm -rf /)` or `aider `rm -rf /``.
4300
+ {
4301
+ category: "subshell_expansion",
4302
+ re: /\$\(|`/,
4303
+ pattern: "subshell expansion $(...) or backticks"
4304
+ },
4305
+ // Download-and-pipe-to-shell: `curl … | sh`, `wget … | bash`, etc.
4306
+ {
4307
+ category: "pipe_to_shell",
4308
+ re: /\b(?:curl|wget|fetch|lwp-request)\b[^\n]{0,400}?\|[^\n]{0,100}?\b(?:sh|bash|zsh|fish|dash|ksh|csh|tcsh|ash)\b/i,
4309
+ pattern: "curl/wget piped into a shell"
4310
+ },
4311
+ // base64 → decode → shell.
4312
+ {
4313
+ category: "base64_pipe",
4314
+ re: /\bbase64\b[^\n]{0,200}?(?:-d|--decode)\b[^\n]{0,400}?\|[^\n]{0,100}?\b(?:sh|bash|zsh|fish|dash)\b/i,
4315
+ pattern: "base64 decode piped into a shell"
4316
+ },
4317
+ // `rm -rf /`, `rm -r ~`, `rm -rf *`, `rm -fr $HOME`, etc.
4318
+ // Matches -r, -R, -f, -e in any order/combination.
4319
+ {
4320
+ category: "rm_dangerous_target",
4321
+ re: /\brm\b(?:\s+-[a-zA-Z]*[rRfe][a-zA-Z]*){1,4}\s+(?:\/(?:\s|$)|~(?:\s|$|\/)|\$HOME\b|\*\s*$|\*\s+|\.{1,2}\s*$)/i,
4322
+ pattern: "rm against /, ~, $HOME, *, or ."
4323
+ },
4324
+ // mkfs.ext4, mkfs.btrfs, plain mkfs, etc.
4325
+ {
4326
+ category: "mkfs",
4327
+ re: /\bmkfs(?:\.[a-z0-9]+)?\b/i,
4328
+ pattern: "mkfs* filesystem format"
4329
+ },
4330
+ // dd of=/dev/sda, dd of=/dev/nvme0n1, etc.
4331
+ {
4332
+ category: "dd_write_device",
4333
+ re: /\bdd\b[^\n|&;]{0,300}?\bof=\/dev\/[a-z]/i,
4334
+ pattern: "dd writing to a block device"
4335
+ },
4336
+ // Shell redirects writing to block devices.
4337
+ {
4338
+ category: "write_device",
4339
+ re: />\s*\/dev\/(?:sd|nvme|hd|mmc|xvd|disk|loop)/i,
4340
+ pattern: "redirect to a block device"
4341
+ },
4342
+ // Classic fork bomb :(){ :|: & };:
4343
+ // Whitespace-tolerant; covers `:() { :|: & };:` variants.
4344
+ {
4345
+ category: "fork_bomb",
4346
+ re: /:\s*\(\s*\)\s*\{[^}]*:\s*\|\s*:\s*&\s*;?\s*\}\s*;?\s*:/,
4347
+ pattern: "fork bomb definition"
4348
+ },
4349
+ // chmod -R 7xx / (world-writable on root).
4350
+ {
4351
+ category: "chmod_permissive_root",
4352
+ re: /\bchmod\b(?:\s+-[rR]+)?\s+[0-7]*7[0-7]*\s+\/(?:\s|$)/i,
4353
+ pattern: "chmod with permissive perms on /"
4354
+ },
4355
+ // chown … /
4356
+ {
4357
+ category: "chown_root",
4358
+ re: /\bchown\b(?:\s+-[rR]+)?\s+\S+\s+\/(?:\s|$)/i,
4359
+ pattern: "chown targeting /"
4360
+ },
4361
+ // ln -s … /dev/… — creating symlinks into /dev lets later redirects hit
4362
+ // device nodes through a benign-looking relative path.
4363
+ {
4364
+ category: "symlink_to_device",
4365
+ re: /\bln\b\s+-[^\s]*s[^\s]*\s+\/dev\//i,
4366
+ pattern: "ln -s into /dev"
4367
+ },
4368
+ // Host-level power actions.
4369
+ {
4370
+ category: "shutdown",
4371
+ re: /\b(?:shutdown|reboot|halt|poweroff|kexec)\b/i,
4372
+ pattern: "shutdown/reboot/halt/poweroff/kexec"
4373
+ }
4374
+ ];
4375
+ function detectDangerousCommand(cmd) {
4376
+ if (typeof cmd !== "string" || cmd.length === 0) return null;
4377
+ for (const { category, re, pattern } of PATTERNS) {
4378
+ if (re.test(cmd)) return { category, pattern };
4379
+ }
4380
+ return null;
4381
+ }
4382
+ function isSafeWorkingDir(raw) {
4383
+ if (typeof raw !== "string" || raw.length === 0) return false;
4384
+ if (!raw.startsWith("/")) return false;
4385
+ const normalized = posixNormalize(raw);
4386
+ if (normalized === "/dev" || normalized.startsWith("/dev/")) return false;
4387
+ return true;
4388
+ }
4389
+
4074
4390
  // src/control-dispatcher.ts
4391
+ var log13 = createLogger("control");
4075
4392
  function collectProcessTreePids(rootPid, visited = /* @__PURE__ */ new Set()) {
4076
4393
  if (visited.has(rootPid)) return [];
4077
4394
  visited.add(rootPid);
@@ -4153,11 +4470,11 @@ var ControlDispatcher = class {
4153
4470
  if (!sessionKey) return true;
4154
4471
  const receivedHmac = payload.hmac;
4155
4472
  if (typeof receivedHmac !== "string") {
4156
- console.warn("[rAgent] Control message missing HMAC signature.");
4473
+ log13.warn("control message missing HMAC signature");
4157
4474
  return false;
4158
4475
  }
4159
4476
  if (!/^[0-9a-f]{64}$/i.test(receivedHmac)) {
4160
- console.warn("[rAgent] Control message has malformed HMAC signature.");
4477
+ log13.warn("control message has malformed HMAC signature");
4161
4478
  return false;
4162
4479
  }
4163
4480
  const { hmac: _, ...payloadWithoutHmac } = payload;
@@ -4183,7 +4500,7 @@ var ControlDispatcher = class {
4183
4500
  ]);
4184
4501
  if (dangerousActions.has(action) && this.connection.sessionKey) {
4185
4502
  if (!this.verifyMessageHmac(payload)) {
4186
- console.warn(`[rAgent] Rejecting control action "${action}" \u2014 HMAC verification failed.`);
4503
+ log13.warn("rejecting control action \u2014 HMAC verification failed", { action });
4187
4504
  return;
4188
4505
  }
4189
4506
  }
@@ -4219,10 +4536,10 @@ var ControlDispatcher = class {
4219
4536
  this.transcriptWatcher?.disableMarkdown(sessionId);
4220
4537
  try {
4221
4538
  await stopTmuxPaneBySessionId(sessionId);
4222
- console.log(`[rAgent] Closed remote session ${sessionId}.`);
4539
+ log13.info("closed remote session", { sessionId });
4223
4540
  } catch (error) {
4224
4541
  const message = error instanceof Error ? error.message : String(error);
4225
- console.warn(`[rAgent] Failed to close ${sessionId}: ${message}`);
4542
+ log13.warn("failed to close session", { sessionId, error: message });
4226
4543
  }
4227
4544
  await delay(300);
4228
4545
  await this.syncInventory(true);
@@ -4233,7 +4550,7 @@ var ControlDispatcher = class {
4233
4550
  {
4234
4551
  const pid = parseProcessPid2(sessionId);
4235
4552
  if (pid === null) {
4236
- console.warn(`[rAgent] kill-process: could not parse PID from ${sessionId}`);
4553
+ log13.warn("kill-process: could not parse PID", { sessionId });
4237
4554
  return;
4238
4555
  }
4239
4556
  this.transcriptWatcher?.disableMarkdown(sessionId);
@@ -4249,11 +4566,11 @@ var ControlDispatcher = class {
4249
4566
  const code = error.code;
4250
4567
  if (code !== "ESRCH") {
4251
4568
  const message = error instanceof Error ? error.message : String(error);
4252
- console.warn(`[rAgent] Failed to send SIGTERM to process ${targetPid}: ${message}`);
4569
+ log13.warn("failed to send SIGTERM", { pid: targetPid, error: message });
4253
4570
  }
4254
4571
  }
4255
4572
  }
4256
- console.log(`[rAgent] Sent SIGTERM to ${signaled} process(es) for ${sessionId}.`);
4573
+ log13.info("sent SIGTERM to processes", { signaled, sessionId });
4257
4574
  await new Promise((resolve) => setTimeout(resolve, 1500));
4258
4575
  for (const targetPid of processTree) {
4259
4576
  if (!isProcessAlive(targetPid)) continue;
@@ -4263,13 +4580,13 @@ var ControlDispatcher = class {
4263
4580
  const code = error.code;
4264
4581
  if (code !== "ESRCH") {
4265
4582
  const message = error instanceof Error ? error.message : String(error);
4266
- console.warn(`[rAgent] Failed to send SIGKILL to process ${targetPid}: ${message}`);
4583
+ log13.warn("failed to send SIGKILL", { pid: targetPid, error: message });
4267
4584
  }
4268
4585
  }
4269
4586
  }
4270
4587
  } catch (error) {
4271
4588
  const message = error instanceof Error ? error.message : String(error);
4272
- console.warn(`[rAgent] Failed to kill ${sessionId}: ${message}`);
4589
+ log13.warn("failed to kill session", { sessionId, error: message });
4273
4590
  }
4274
4591
  await this.syncInventory(true);
4275
4592
  }
@@ -4284,7 +4601,7 @@ var ControlDispatcher = class {
4284
4601
  }
4285
4602
  }
4286
4603
  const killed = await stopAllDetachedTmuxSessions();
4287
- console.log(`[rAgent] Killed ${killed} detached tmux session(s).`);
4604
+ log13.info("killed detached tmux sessions", { count: killed });
4288
4605
  await delay(300);
4289
4606
  await this.syncInventory(true);
4290
4607
  return;
@@ -4340,10 +4657,10 @@ var ControlDispatcher = class {
4340
4657
  async handleProvision(payload) {
4341
4658
  const provReq = validateProvisionRequest(payload);
4342
4659
  if (!provReq) {
4343
- console.warn("[rAgent] Rejecting provision request \u2014 malformed payload.");
4660
+ log13.warn("rejecting provision request \u2014 malformed payload");
4344
4661
  return;
4345
4662
  }
4346
- console.log(`[rAgent] Provision request: ${provReq.manifest.name} (${provReq.provisionId})`);
4663
+ log13.info("provision request", { name: provReq.manifest.name, provisionId: provReq.provisionId });
4347
4664
  const sendProgress = (progress) => {
4348
4665
  const ws = this.connection.activeSocket;
4349
4666
  if (ws && ws.readyState === import_ws4.default.OPEN && this.connection.activeGroups.registryGroup) {
@@ -4380,7 +4697,7 @@ var ControlDispatcher = class {
4380
4697
  status: "unavailable",
4381
4698
  reason: result.reason
4382
4699
  });
4383
- console.log(`[rAgent] Could not enable markdown for ${sessionId}: ${result.reason}`);
4700
+ log13.info("could not enable markdown", { sessionId, reason: result.reason });
4384
4701
  } else {
4385
4702
  this.sendMarkdownStatus({
4386
4703
  type: "markdown",
@@ -4397,7 +4714,7 @@ var ControlDispatcher = class {
4397
4714
  const ws = this.connection.activeSocket;
4398
4715
  const group = this.connection.activeGroups.privateGroup;
4399
4716
  if (!ws || ws.readyState !== import_ws4.default.OPEN || !group) return;
4400
- sendToGroup(ws, group, payload);
4717
+ sendToGroup(ws, group, { ...payload });
4401
4718
  }
4402
4719
  /** Stop all transcript watchers (called on disconnect/cleanup). */
4403
4720
  stopTranscriptWatchers() {
@@ -4414,19 +4731,59 @@ var ControlDispatcher = class {
4414
4731
  const sessionName = typeof payload?.sessionName === "string" && payload.sessionName.trim().length > 0 ? payload.sessionName.trim() : `agent-${Date.now().toString(36)}`;
4415
4732
  const cmd = typeof payload?.command === "string" && payload.command.trim().length > 0 ? payload.command.trim() : null;
4416
4733
  if (!cmd) {
4417
- console.warn("[rAgent] start-agent: no command provided, ignoring.");
4734
+ log13.warn("start-agent: no command provided, ignoring");
4418
4735
  return;
4419
4736
  }
4420
4737
  if (sessionName.length > 128 || !/^[a-zA-Z0-9_-]+$/.test(sessionName)) {
4421
- console.warn("[rAgent] start-agent: invalid session name, ignoring.");
4738
+ log13.warn("start-agent: invalid session name, ignoring");
4422
4739
  return;
4423
4740
  }
4424
- const dangerous = /rm\s+-r[fe]*\s+\/|mkfs|dd\s+if=|:()\s*\{|>\s*\/dev\/sd/i;
4425
- if (dangerous.test(cmd)) {
4426
- console.warn(`[rAgent] start-agent: rejected dangerous command: ${cmd}`);
4741
+ const danger = detectDangerousCommand(cmd);
4742
+ if (danger) {
4743
+ if (!this._approvalEnforcer) {
4744
+ log13.warn("start-agent: rejected dangerous command (no approval enforcer)", {
4745
+ category: danger.category,
4746
+ pattern: danger.pattern,
4747
+ command: cmd
4748
+ });
4749
+ return;
4750
+ }
4751
+ log13.info("start-agent: dangerous command \u2014 awaiting approval", {
4752
+ category: danger.category,
4753
+ pattern: danger.pattern,
4754
+ sessionName
4755
+ });
4756
+ const response = await this._approvalEnforcer.checkCommand(
4757
+ cmd,
4758
+ this.options.hostId,
4759
+ sessionName
4760
+ );
4761
+ if (response === null) {
4762
+ log13.warn("start-agent: dangerous command not covered by approval policy, rejecting", {
4763
+ category: danger.category,
4764
+ command: cmd
4765
+ });
4766
+ return;
4767
+ }
4768
+ if (response.type !== "approved") {
4769
+ log13.warn("start-agent: dangerous command denied or timed out", {
4770
+ category: danger.category,
4771
+ sessionName,
4772
+ responseType: response.type
4773
+ });
4774
+ return;
4775
+ }
4776
+ log13.info("start-agent: dangerous command approved", {
4777
+ category: danger.category,
4778
+ sessionName
4779
+ });
4780
+ }
4781
+ const rawWorkingDir = typeof payload?.workingDir === "string" && payload.workingDir.trim().length > 0 ? payload.workingDir.trim() : void 0;
4782
+ if (rawWorkingDir !== void 0 && !isSafeWorkingDir(rawWorkingDir)) {
4783
+ log13.warn("start-agent: rejected unsafe workingDir", { workingDir: rawWorkingDir });
4427
4784
  return;
4428
4785
  }
4429
- const workingDir = typeof payload?.workingDir === "string" && payload.workingDir.trim().length > 0 ? payload.workingDir.trim() : void 0;
4786
+ const workingDir = rawWorkingDir;
4430
4787
  const envVars = payload?.envVars && typeof payload.envVars === "object" ? payload.envVars : void 0;
4431
4788
  const tmuxArgs = ["new-session", "-d", "-s", sessionName];
4432
4789
  if (workingDir) {
@@ -4440,10 +4797,10 @@ var ControlDispatcher = class {
4440
4797
  tmuxArgs.push(fullCmd);
4441
4798
  try {
4442
4799
  (0, import_child_process5.execFileSync)("tmux", tmuxArgs, { stdio: "ignore" });
4443
- console.log(`[rAgent] Started agent session "${sessionName}": ${cmd}`);
4800
+ log13.info("started agent session", { sessionName, command: cmd });
4444
4801
  } catch (error) {
4445
4802
  const message = error instanceof Error ? error.message : String(error);
4446
- console.error(`[rAgent] Failed to start agent session "${sessionName}": ${message}`);
4803
+ log13.error("failed to start agent session", { sessionName, error: message });
4447
4804
  }
4448
4805
  await this.syncInventory(true);
4449
4806
  }
@@ -4706,6 +5063,7 @@ function isGrantExpired(grant) {
4706
5063
  }
4707
5064
 
4708
5065
  // src/approval-enforcer.ts
5066
+ var log14 = createLogger("approval");
4709
5067
  var ApprovalEnforcer = class {
4710
5068
  policy;
4711
5069
  secret;
@@ -4729,16 +5087,20 @@ var ApprovalEnforcer = class {
4729
5087
  async checkCommand(command, hostId, sessionId) {
4730
5088
  const request = matchPolicy(command, this.policy, hostId, sessionId);
4731
5089
  if (!request) return null;
4732
- console.log(
4733
- `[rAgent] Approval: ${request.action} (${request.severity}) \u2014 pending [${request.requestId}]`
4734
- );
5090
+ log14.info("approval pending", {
5091
+ action: request.action,
5092
+ severity: request.severity,
5093
+ requestId: request.requestId
5094
+ });
4735
5095
  return new Promise((resolve) => {
4736
5096
  const timer = setTimeout(() => {
4737
5097
  this.pendingRequests.delete(request.requestId);
4738
5098
  const response = { type: "timeout" };
4739
- console.log(
4740
- `[rAgent] Approval: ${request.action} (${request.severity}) \u2014 timeout [${request.requestId}]`
4741
- );
5099
+ log14.info("approval timeout", {
5100
+ action: request.action,
5101
+ severity: request.severity,
5102
+ requestId: request.requestId
5103
+ });
4742
5104
  resolve(response);
4743
5105
  }, request.timeoutMs);
4744
5106
  this.pendingRequests.set(request.requestId, { request, resolve, timer });
@@ -4754,29 +5116,33 @@ var ApprovalEnforcer = class {
4754
5116
  if (!requestId) return;
4755
5117
  const pending = this.pendingRequests.get(requestId);
4756
5118
  if (!pending) {
4757
- console.warn(
4758
- `[rAgent] Approval: received response for unknown request [${requestId}]`
4759
- );
5119
+ log14.warn("received response for unknown approval request", { requestId });
4760
5120
  return;
4761
5121
  }
4762
5122
  if (response.type === "approved") {
4763
5123
  if (!this.acceptGrant(response.grant)) {
4764
- console.log(
4765
- `[rAgent] Approval: ${pending.request.action} (${pending.request.severity}) \u2014 rejected (invalid grant) [${requestId}]`
4766
- );
5124
+ log14.info("approval rejected (invalid grant)", {
5125
+ action: pending.request.action,
5126
+ severity: pending.request.severity,
5127
+ requestId
5128
+ });
4767
5129
  clearTimeout(pending.timer);
4768
5130
  this.pendingRequests.delete(requestId);
4769
5131
  pending.resolve({ type: "denied", denial: { requestId, deniedBy: "system", deniedAt: (/* @__PURE__ */ new Date()).toISOString(), reason: "Invalid or expired grant" } });
4770
5132
  return;
4771
5133
  }
4772
- console.log(
4773
- `[rAgent] Approval: ${pending.request.action} (${pending.request.severity}) \u2014 approved [${requestId}]`
4774
- );
5134
+ log14.info("approval approved", {
5135
+ action: pending.request.action,
5136
+ severity: pending.request.severity,
5137
+ requestId
5138
+ });
4775
5139
  } else if (response.type === "denied") {
4776
- const reason = response.denial.reason ? ` (${response.denial.reason})` : "";
4777
- console.log(
4778
- `[rAgent] Approval: ${pending.request.action} (${pending.request.severity}) \u2014 denied${reason} [${requestId}]`
4779
- );
5140
+ log14.info("approval denied", {
5141
+ action: pending.request.action,
5142
+ severity: pending.request.severity,
5143
+ requestId,
5144
+ reason: response.denial.reason ?? ""
5145
+ });
4780
5146
  }
4781
5147
  clearTimeout(pending.timer);
4782
5148
  this.pendingRequests.delete(requestId);
@@ -4812,9 +5178,11 @@ var ApprovalEnforcer = class {
4812
5178
  for (const [id, entry] of this.pendingRequests) {
4813
5179
  clearTimeout(entry.timer);
4814
5180
  this.pendingRequests.delete(id);
4815
- console.log(
4816
- `[rAgent] Approval: ${entry.request.action} (${entry.request.severity}) \u2014 stopped [${id}]`
4817
- );
5181
+ log14.info("approval stopped", {
5182
+ action: entry.request.action,
5183
+ severity: entry.request.severity,
5184
+ requestId: id
5185
+ });
4818
5186
  entry.resolve({ type: "timeout" });
4819
5187
  }
4820
5188
  }
@@ -4839,6 +5207,7 @@ var ApprovalEnforcer = class {
4839
5207
  };
4840
5208
 
4841
5209
  // src/agent.ts
5210
+ var log15 = createLogger("agent");
4842
5211
  var MAX_TRANSPORT_CHUNK_BYTES = 24 * 1024;
4843
5212
  function pidFilePath(hostId) {
4844
5213
  return path4.join(CONFIG_DIR, `agent-${hostId}.pid`);
@@ -4923,15 +5292,15 @@ async function runAgent(rawOptions) {
4923
5292
  const config = loadConfig();
4924
5293
  if (config.redaction?.enabled === false || rawOptions.redact === false) {
4925
5294
  setRedactionEnabled(false);
4926
- console.log("[rAgent] Secret redaction disabled.");
5295
+ log15.info("secret redaction disabled");
4927
5296
  }
4928
- console.log(`[rAgent] Connector started for ${options.hostName} (${options.hostId})`);
4929
- console.log(`[rAgent] Portal: ${options.portal}`);
5297
+ log15.info("connector started", { hostName: options.hostName, hostId: options.hostId });
5298
+ log15.info("portal", { portal: options.portal });
4930
5299
  try {
4931
5300
  const existingSessions = await collectSessionInventory(options.hostId, options.command);
4932
5301
  const tmuxCount = existingSessions.filter((s) => s.type === "tmux").length;
4933
5302
  if (tmuxCount > 0) {
4934
- console.log(`[rAgent] Found ${tmuxCount} existing tmux session(s) on this machine.`);
5303
+ log15.info("found existing tmux sessions", { count: tmuxCount });
4935
5304
  }
4936
5305
  } catch {
4937
5306
  }
@@ -4944,16 +5313,26 @@ async function runAgent(rawOptions) {
4944
5313
  sessionStreamer.bufferOutput(sessionId, data);
4945
5314
  return;
4946
5315
  }
4947
- for (const chunk of splitTransportChunks(data)) {
5316
+ const redactedData = redactStreamingChunk(sessionId, data);
5317
+ for (const chunk of splitTransportChunks(redactedData)) {
4948
5318
  if (conn.sessionKey) {
4949
- const { enc, iv } = encryptPayload(chunk, conn.sessionKey);
4950
- sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, sessionId });
5319
+ const { enc, iv, seq } = encryptPayload(chunk, conn.sessionKey, conn.txSeq.next());
5320
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, seq, sessionId });
4951
5321
  } else {
4952
5322
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", data: chunk, sessionId });
4953
5323
  }
4954
5324
  }
4955
5325
  },
4956
5326
  (sessionId) => {
5327
+ const tail = flushStreamingRedaction(sessionId);
5328
+ if (tail && conn.isReady()) {
5329
+ if (conn.sessionKey) {
5330
+ const { enc, iv, seq } = encryptPayload(tail, conn.sessionKey, conn.txSeq.next());
5331
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, seq, sessionId });
5332
+ } else {
5333
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", data: tail, sessionId });
5334
+ }
5335
+ }
4957
5336
  if (!conn.isReady()) return;
4958
5337
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "stream-stopped", sessionId });
4959
5338
  }
@@ -4965,10 +5344,11 @@ async function runAgent(rawOptions) {
4965
5344
  outputBuffer.push(chunk);
4966
5345
  return;
4967
5346
  }
4968
- for (const outputChunk of splitTransportChunks(chunk)) {
5347
+ const redacted = redactStreamingChunk(ptySessionId, chunk);
5348
+ for (const outputChunk of splitTransportChunks(redacted)) {
4969
5349
  if (conn.sessionKey) {
4970
- const { enc, iv } = encryptPayload(outputChunk, conn.sessionKey);
4971
- sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, sessionId: ptySessionId });
5350
+ const { enc, iv, seq } = encryptPayload(outputChunk, conn.sessionKey, conn.txSeq.next());
5351
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, seq, sessionId: ptySessionId });
4972
5352
  } else {
4973
5353
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", data: outputChunk, sessionId: ptySessionId });
4974
5354
  }
@@ -4990,8 +5370,8 @@ async function runAgent(rawOptions) {
4990
5370
  };
4991
5371
  if (conn.sessionKey) {
4992
5372
  const contentStr = JSON.stringify(payload);
4993
- const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
4994
- sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
5373
+ const { enc, iv, seq: frameSeq } = encryptPayload(contentStr, conn.sessionKey, conn.txSeq.next());
5374
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, seq: frameSeq, sessionId });
4995
5375
  } else {
4996
5376
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
4997
5377
  }
@@ -5007,8 +5387,8 @@ async function runAgent(rawOptions) {
5007
5387
  };
5008
5388
  if (conn.sessionKey) {
5009
5389
  const contentStr = JSON.stringify(payload);
5010
- const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
5011
- sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
5390
+ const { enc, iv, seq: frameSeq } = encryptPayload(contentStr, conn.sessionKey, conn.txSeq.next());
5391
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, seq: frameSeq, sessionId });
5012
5392
  } else {
5013
5393
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
5014
5394
  }
@@ -5026,8 +5406,8 @@ async function runAgent(rawOptions) {
5026
5406
  };
5027
5407
  if (conn.sessionKey) {
5028
5408
  const contentStr = JSON.stringify(payload);
5029
- const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
5030
- sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
5409
+ const { enc, iv, seq: frameSeq } = encryptPayload(contentStr, conn.sessionKey, conn.txSeq.next());
5410
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, seq: frameSeq, sessionId });
5031
5411
  } else {
5032
5412
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
5033
5413
  }
@@ -5045,8 +5425,8 @@ async function runAgent(rawOptions) {
5045
5425
  };
5046
5426
  if (conn.sessionKey) {
5047
5427
  const contentStr = JSON.stringify(payload);
5048
- const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
5049
- sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
5428
+ const { enc, iv, seq: frameSeq } = encryptPayload(contentStr, conn.sessionKey, conn.txSeq.next());
5429
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, seq: frameSeq, sessionId });
5050
5430
  } else {
5051
5431
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
5052
5432
  }
@@ -5105,7 +5485,7 @@ async function runAgent(rawOptions) {
5105
5485
  dispatcher.setApprovalEnforcer(null);
5106
5486
  }
5107
5487
  ws.on("open", async () => {
5108
- console.log("[rAgent] Connector connected to relay.");
5488
+ log15.info("connector connected to relay");
5109
5489
  conn.resetReconnectDelay();
5110
5490
  ws.send(JSON.stringify({ type: "joinGroup", group: groups.privateGroup }));
5111
5491
  ws.send(JSON.stringify({ type: "joinGroup", group: groups.registryGroup }));
@@ -5116,11 +5496,22 @@ async function runAgent(rawOptions) {
5116
5496
  });
5117
5497
  conn.replayBufferedOutput((chunk) => {
5118
5498
  for (const outputChunk of splitTransportChunks(chunk)) {
5119
- sendToGroup(ws, groups.privateGroup, {
5120
- type: "output",
5121
- data: outputChunk,
5122
- sessionId: ptySessionId
5123
- });
5499
+ if (conn.sessionKey) {
5500
+ const { enc, iv, seq } = encryptPayload(outputChunk, conn.sessionKey, conn.txSeq.next());
5501
+ sendToGroup(ws, groups.privateGroup, {
5502
+ type: "output",
5503
+ enc,
5504
+ iv,
5505
+ seq,
5506
+ sessionId: ptySessionId
5507
+ });
5508
+ } else {
5509
+ sendToGroup(ws, groups.privateGroup, {
5510
+ type: "output",
5511
+ data: outputChunk,
5512
+ sessionId: ptySessionId
5513
+ });
5514
+ }
5124
5515
  }
5125
5516
  });
5126
5517
  for (const [sessionId] of sessionStreamer.activeStreams()) {
@@ -5128,8 +5519,8 @@ async function runAgent(rawOptions) {
5128
5519
  for (const chunk of buffered) {
5129
5520
  for (const outputChunk of splitTransportChunks(chunk)) {
5130
5521
  if (conn.sessionKey) {
5131
- const { enc, iv } = encryptPayload(outputChunk, conn.sessionKey);
5132
- sendToGroup(ws, groups.privateGroup, { type: "output", enc, iv, sessionId });
5522
+ const { enc, iv, seq } = encryptPayload(outputChunk, conn.sessionKey, conn.txSeq.next());
5523
+ sendToGroup(ws, groups.privateGroup, { type: "output", enc, iv, seq, sessionId });
5133
5524
  } else {
5134
5525
  sendToGroup(ws, groups.privateGroup, { type: "output", data: outputChunk, sessionId });
5135
5526
  }
@@ -5155,9 +5546,26 @@ async function runAgent(rawOptions) {
5155
5546
  if (payload.type === "input") {
5156
5547
  let inputData = null;
5157
5548
  if (typeof payload.enc === "string" && typeof payload.iv === "string" && conn.sessionKey) {
5158
- inputData = decryptPayload(payload.enc, payload.iv, conn.sessionKey);
5159
- if (inputData === null) {
5160
- console.warn("[rAgent] Failed to decrypt input \u2014 ignoring.");
5549
+ const inSeq = typeof payload.seq === "number" ? payload.seq : null;
5550
+ const rxSeq = conn.getRxSeqForViewer(payload.viewerId);
5551
+ if (inSeq === null) {
5552
+ log15.warn("rejecting encrypted input frame without seq (#310)");
5553
+ } else if (!rxSeq.accept(inSeq)) {
5554
+ log15.warn("rejecting replayed/out-of-order input", {
5555
+ seq: inSeq,
5556
+ lastSeen: rxSeq.lastSeq,
5557
+ viewerId: typeof payload.viewerId === "string" ? payload.viewerId : "_legacy"
5558
+ });
5559
+ } else {
5560
+ inputData = decryptPayload(
5561
+ payload.enc,
5562
+ payload.iv,
5563
+ conn.sessionKey,
5564
+ inSeq
5565
+ );
5566
+ if (inputData === null) {
5567
+ log15.warn("failed to decrypt input \u2014 ignoring");
5568
+ }
5161
5569
  }
5162
5570
  } else if (typeof payload.data === "string") {
5163
5571
  inputData = payload.data;
@@ -5187,11 +5595,13 @@ async function runAgent(rawOptions) {
5187
5595
  }
5188
5596
  });
5189
5597
  ws.on("error", (error) => {
5190
- console.error("[rAgent] WebSocket error:", error.message);
5598
+ log15.error("websocket error", { error: error.message });
5191
5599
  });
5192
5600
  ws.on("close", (code, reason) => {
5193
- const details = code || reason.length > 0 ? ` (code=${code}${reason.length > 0 ? ` reason=${reason.toString()}` : ""})` : "";
5194
- console.log(`[rAgent] Relay disconnected${details}. Output will be buffered until reconnect.`);
5601
+ const fields = {};
5602
+ if (code) fields.code = code;
5603
+ if (reason.length > 0) fields.reason = reason.toString();
5604
+ log15.info("relay disconnected; output will be buffered until reconnect", fields);
5195
5605
  conn.cleanup();
5196
5606
  resolve();
5197
5607
  });
@@ -5208,35 +5618,35 @@ async function runAgent(rawOptions) {
5208
5618
  });
5209
5619
  inventory.updateOptions(options);
5210
5620
  dispatcher.updateOptions(options);
5211
- console.log("[rAgent] Recovered from auth failure via machine credential. Reconnecting...");
5621
+ log15.info("recovered from auth failure via machine credential; reconnecting");
5212
5622
  continue;
5213
5623
  } catch (mcError) {
5214
5624
  if (mcError instanceof AuthError) {
5215
- console.error(`[rAgent] ${mcError.message}`);
5625
+ log15.error("auth recovery failed", { error: mcError.message });
5216
5626
  } else {
5217
5627
  const mcMsg = mcError instanceof Error ? mcError.message : String(mcError);
5218
- console.error(`[rAgent] Machine credential recovery failed: ${mcMsg}`);
5628
+ log15.error("machine credential recovery failed", { error: mcMsg });
5219
5629
  }
5220
5630
  }
5221
5631
  }
5222
- console.error(`[rAgent] ${error.message}`);
5223
- console.error(
5224
- "[rAgent] Connector token is invalid or revoked. Stopping. Re-connect with: ragent connect --token <token>"
5632
+ log15.error("auth error", { error: error.message });
5633
+ log15.error(
5634
+ "connector token is invalid or revoked. Stopping. Re-connect with: ragent connect --token <token>"
5225
5635
  );
5226
5636
  dispatcher.shouldRun = false;
5227
5637
  break;
5228
5638
  }
5229
5639
  const message = error instanceof Error ? error.message : String(error);
5230
- console.error(`[rAgent] Relay connect failed: ${message}`);
5640
+ log15.error("relay connect failed", { error: message });
5231
5641
  }
5232
5642
  if (!dispatcher.shouldRun) break;
5233
5643
  if (dispatcher.reconnectRequested) {
5234
- console.log("[rAgent] Reconnecting to relay...");
5644
+ log15.info("reconnecting to relay");
5235
5645
  await wait(300);
5236
5646
  continue;
5237
5647
  }
5238
5648
  const jitteredDelay = conn.reconnectDelay * (0.5 + Math.random());
5239
- console.log(`[rAgent] Disconnected. Reconnecting in ${Math.round(jitteredDelay / 1e3)}s...`);
5649
+ log15.info("disconnected; reconnecting", { seconds: Math.round(jitteredDelay / 1e3) });
5240
5650
  await wait(jitteredDelay);
5241
5651
  conn.reconnectDelay = Math.min(conn.reconnectDelay * 1.5, MAX_RECONNECT_DELAY_MS);
5242
5652
  }
@@ -5335,6 +5745,7 @@ function printCommandArt(title, subtitle = "remote agent control") {
5335
5745
  }
5336
5746
 
5337
5747
  // src/commands/connect.ts
5748
+ var log16 = createLogger("connect");
5338
5749
  async function connectMachine(opts) {
5339
5750
  const portal = opts.portal || DEFAULT_PORTAL;
5340
5751
  const hostId = sanitizeHostId(opts.id || inferHostId());
@@ -5366,12 +5777,12 @@ async function connectMachine(opts) {
5366
5777
  nextConfig.refreshExpiresAt = claimed.refreshExpiresAt;
5367
5778
  }
5368
5779
  saveConfig(nextConfig);
5369
- console.log(`[rAgent] Machine connected: ${hostName} (${hostId})`);
5370
- console.log(`[rAgent] Machine name: ${hostName} (override with --name <friendly-name>)`);
5371
- console.log(`[rAgent] Config saved to ${CONFIG_FILE}`);
5780
+ log16.info("machine connected", { hostName, hostId });
5781
+ log16.info("machine name (override with --name <friendly-name>)", { hostName });
5782
+ log16.info("config saved", { path: CONFIG_FILE });
5372
5783
  if (opts.asService) {
5373
5784
  await ensureServiceInstalled({ enable: true, start: true });
5374
- console.log("[rAgent] Service mode enabled.");
5785
+ log16.info("service mode enabled");
5375
5786
  return;
5376
5787
  }
5377
5788
  if (opts.run !== false) {
@@ -5391,13 +5802,14 @@ function registerConnectCommand(program2) {
5391
5802
  await connectMachine(opts);
5392
5803
  } catch (error) {
5393
5804
  const message = error instanceof Error ? error.message : String(error);
5394
- console.error(`[rAgent] Connect failed: ${message}`);
5805
+ log16.error("connect failed", { error: message });
5395
5806
  process.exit(1);
5396
5807
  }
5397
5808
  });
5398
5809
  }
5399
5810
 
5400
5811
  // src/commands/run.ts
5812
+ var log17 = createLogger("run");
5401
5813
  function registerRunCommand(program2) {
5402
5814
  program2.command("run").description("Run connector for a connected machine").option("--portal <url>", "Portal base URL").option("--agent-token <token>", "Connector token").option("-i, --id <id>", "Machine ID").option("-n, --name <name>", "Machine name").option("-c, --command <command>", "CLI command to run").option("--no-redact", "Disable secret redaction in terminal output").action(async (opts) => {
5403
5815
  try {
@@ -5405,7 +5817,7 @@ function registerRunCommand(program2) {
5405
5817
  await runAgent(opts);
5406
5818
  } catch (error) {
5407
5819
  const message = error instanceof Error ? error.message : String(error);
5408
- console.error(`[rAgent] Run failed: ${message}`);
5820
+ log17.error("run failed", { error: message });
5409
5821
  process.exit(1);
5410
5822
  }
5411
5823
  });
@@ -5413,6 +5825,7 @@ function registerRunCommand(program2) {
5413
5825
 
5414
5826
  // src/commands/doctor.ts
5415
5827
  var os10 = __toESM(require("os"));
5828
+ var log18 = createLogger("doctor");
5416
5829
  async function runDoctor(opts) {
5417
5830
  const options = resolveRunOptions(opts);
5418
5831
  const checks = [];
@@ -5448,29 +5861,27 @@ async function runDoctor(opts) {
5448
5861
  console.log(`${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`);
5449
5862
  });
5450
5863
  if (!platformOk) {
5451
- console.log("[rAgent] Linux is required for the connector.");
5864
+ log18.info("Linux is required for the connector");
5452
5865
  }
5453
5866
  if (!tmuxOk) {
5454
5867
  const recipe = await chooseTmuxInstallCommand();
5455
5868
  if (recipe) {
5456
- console.log(`[rAgent] tmux install suggestion: ${recipe.command}`);
5869
+ log18.info("tmux install suggestion", { command: recipe.command });
5457
5870
  }
5458
5871
  if (opts.fix) {
5459
5872
  try {
5460
5873
  const installed = await installTmuxInteractively();
5461
5874
  if (installed) {
5462
- console.log("[rAgent] tmux installation complete.");
5875
+ log18.info("tmux installation complete");
5463
5876
  } else {
5464
- console.log("[rAgent] tmux installation skipped or incomplete.");
5877
+ log18.info("tmux installation skipped or incomplete");
5465
5878
  }
5466
5879
  } catch (error) {
5467
5880
  const message = error instanceof Error ? error.message : String(error);
5468
- console.error(`[rAgent] Failed to install tmux: ${message}`);
5881
+ log18.error("failed to install tmux", { error: message });
5469
5882
  }
5470
5883
  } else {
5471
- console.log(
5472
- "[rAgent] Run `ragent doctor --fix` to install missing dependencies interactively."
5473
- );
5884
+ log18.info("run `ragent doctor --fix` to install missing dependencies interactively");
5474
5885
  }
5475
5886
  }
5476
5887
  }
@@ -5482,32 +5893,33 @@ function registerDoctorCommand(program2) {
5482
5893
  }
5483
5894
 
5484
5895
  // src/commands/update.ts
5896
+ var log19 = createLogger("update");
5485
5897
  function registerUpdateCommand(program2) {
5486
5898
  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) => {
5487
5899
  printCommandArt("Update", "checking npm registry for newer ragent-cli versions");
5488
5900
  const latestVersion = await checkForUpdate({ force: true });
5489
5901
  if (!latestVersion) {
5490
- console.log(`[rAgent] You are up to date (${CURRENT_VERSION}).`);
5902
+ log19.info("you are up to date", { version: CURRENT_VERSION });
5491
5903
  return;
5492
5904
  }
5493
- console.log(`[rAgent] Update available: ${CURRENT_VERSION} -> ${latestVersion}`);
5905
+ log19.info("update available", { current: CURRENT_VERSION, latest: latestVersion });
5494
5906
  if (opts.check) return;
5495
5907
  await execAsync(`npm install -g ${shellQuote(PACKAGE_NAME)}`, {
5496
5908
  timeout: 5 * 60 * 1e3,
5497
5909
  maxBuffer: 10 * 1024 * 1024
5498
5910
  });
5499
- console.log(`[rAgent] Updated to ${latestVersion}.`);
5911
+ log19.info("updated", { version: latestVersion });
5500
5912
  const backend = getConfiguredServiceBackend();
5501
5913
  if (backend && opts.restart) {
5502
- console.log(`[rAgent] Restarting ${backend} service...`);
5914
+ log19.info("restarting service", { backend });
5503
5915
  try {
5504
5916
  await restartService();
5505
5917
  } catch (err) {
5506
- console.error(`[rAgent] Failed to restart service: ${err instanceof Error ? err.message : err}`);
5507
- console.log("[rAgent] Please restart manually: ragent service restart");
5918
+ log19.error("failed to restart service", { error: err instanceof Error ? err.message : String(err) });
5919
+ log19.info("please restart manually: ragent service restart");
5508
5920
  }
5509
5921
  } else if (!backend) {
5510
- console.log("[rAgent] No service backend detected. If ragent is running manually, restart it to use the new version.");
5922
+ log19.info("no service backend detected. If ragent is running manually, restart it to use the new version");
5511
5923
  }
5512
5924
  });
5513
5925
  }
@@ -5531,6 +5943,7 @@ function registerSessionsCommand(program2) {
5531
5943
  }
5532
5944
 
5533
5945
  // src/commands/service.ts
5946
+ var log20 = createLogger("service-cmd");
5534
5947
  function registerServiceCommand(program2) {
5535
5948
  const service = program2.command("service").description("Manage background connector service");
5536
5949
  service.command("install").description("Install service (systemd user unit when available)").option("--start", "Start service immediately").option("--no-enable", "Do not enable autostart in systemd").action(async (opts) => {
@@ -5541,7 +5954,7 @@ function registerServiceCommand(program2) {
5541
5954
  });
5542
5955
  } catch (error) {
5543
5956
  const message = error instanceof Error ? error.message : String(error);
5544
- console.error(`[rAgent] Service install failed: ${message}`);
5957
+ log20.error("service install failed", { error: message });
5545
5958
  process.exit(1);
5546
5959
  }
5547
5960
  });
@@ -5550,7 +5963,7 @@ function registerServiceCommand(program2) {
5550
5963
  await startService();
5551
5964
  } catch (error) {
5552
5965
  const message = error instanceof Error ? error.message : String(error);
5553
- console.error(`[rAgent] Service start failed: ${message}`);
5966
+ log20.error("service start failed", { error: message });
5554
5967
  process.exit(1);
5555
5968
  }
5556
5969
  });
@@ -5559,7 +5972,7 @@ function registerServiceCommand(program2) {
5559
5972
  await stopService();
5560
5973
  } catch (error) {
5561
5974
  const message = error instanceof Error ? error.message : String(error);
5562
- console.error(`[rAgent] Service stop failed: ${message}`);
5975
+ log20.error("service stop failed", { error: message });
5563
5976
  process.exit(1);
5564
5977
  }
5565
5978
  });
@@ -5568,7 +5981,7 @@ function registerServiceCommand(program2) {
5568
5981
  await restartService();
5569
5982
  } catch (error) {
5570
5983
  const message = error instanceof Error ? error.message : String(error);
5571
- console.error(`[rAgent] Service restart failed: ${message}`);
5984
+ log20.error("service restart failed", { error: message });
5572
5985
  process.exit(1);
5573
5986
  }
5574
5987
  });
@@ -5577,7 +5990,7 @@ function registerServiceCommand(program2) {
5577
5990
  await printServiceStatus();
5578
5991
  } catch (error) {
5579
5992
  const message = error instanceof Error ? error.message : String(error);
5580
- console.error(`[rAgent] Service status failed: ${message}`);
5993
+ log20.error("service status failed", { error: message });
5581
5994
  process.exit(1);
5582
5995
  }
5583
5996
  });
@@ -5586,7 +5999,7 @@ function registerServiceCommand(program2) {
5586
5999
  await printServiceLogs(opts);
5587
6000
  } catch (error) {
5588
6001
  const message = error instanceof Error ? error.message : String(error);
5589
- console.error(`[rAgent] Service logs failed: ${message}`);
6002
+ log20.error("service logs failed", { error: message });
5590
6003
  process.exit(1);
5591
6004
  }
5592
6005
  });
@@ -5595,7 +6008,7 @@ function registerServiceCommand(program2) {
5595
6008
  await uninstallService();
5596
6009
  } catch (error) {
5597
6010
  const message = error instanceof Error ? error.message : String(error);
5598
- console.error(`[rAgent] Service uninstall failed: ${message}`);
6011
+ log20.error("service uninstall failed", { error: message });
5599
6012
  process.exit(1);
5600
6013
  }
5601
6014
  });
@@ -5603,6 +6016,7 @@ function registerServiceCommand(program2) {
5603
6016
 
5604
6017
  // src/commands/uninstall.ts
5605
6018
  var fs6 = __toESM(require("fs"));
6019
+ var log21 = createLogger("uninstall");
5606
6020
  async function uninstallAgent(opts) {
5607
6021
  const config = loadConfig();
5608
6022
  const hostName = config.hostName || config.hostId || "this machine";
@@ -5612,12 +6026,12 @@ async function uninstallAgent(opts) {
5612
6026
  false
5613
6027
  );
5614
6028
  if (!confirmed) {
5615
- console.log("[rAgent] Uninstall cancelled.");
6029
+ log21.info("uninstall cancelled");
5616
6030
  return;
5617
6031
  }
5618
6032
  }
5619
6033
  if (config.portal && config.agentToken) {
5620
- console.log("[rAgent] Deregistering from server...");
6034
+ log21.info("deregistering from server");
5621
6035
  try {
5622
6036
  const token = await refreshTokenIfNeeded({
5623
6037
  portal: config.portal,
@@ -5625,24 +6039,24 @@ async function uninstallAgent(opts) {
5625
6039
  });
5626
6040
  const ok = await deregisterHost({ portal: config.portal, agentToken: token });
5627
6041
  if (ok) {
5628
- console.log("[rAgent] Host removed from server.");
6042
+ log21.info("host removed from server");
5629
6043
  }
5630
6044
  } catch {
5631
- console.warn("[rAgent] Could not reach server \u2014 host may need manual removal from dashboard.");
6045
+ log21.warn("could not reach server \u2014 host may need manual removal from dashboard");
5632
6046
  }
5633
6047
  }
5634
- console.log("[rAgent] Stopping and removing service...");
6048
+ log21.info("stopping and removing service");
5635
6049
  await uninstallService().catch(() => void 0);
5636
6050
  if (fs6.existsSync(CONFIG_DIR)) {
5637
6051
  fs6.rmSync(CONFIG_DIR, { recursive: true, force: true });
5638
- console.log(`[rAgent] Removed config directory: ${CONFIG_DIR}`);
6052
+ log21.info("removed config directory", { path: CONFIG_DIR });
5639
6053
  }
5640
6054
  try {
5641
6055
  await execAsync(`npm unlink -g ${PACKAGE_NAME}`, { timeout: 15e3 });
5642
- console.log(`[rAgent] Unlinked global package: ${PACKAGE_NAME}`);
6056
+ log21.info("unlinked global package", { package: PACKAGE_NAME });
5643
6057
  } catch {
5644
6058
  }
5645
- console.log("[rAgent] Uninstall complete. rAgent has been removed from this machine.");
6059
+ log21.info("uninstall complete. rAgent has been removed from this machine");
5646
6060
  }
5647
6061
  function registerUninstallCommand(parent) {
5648
6062
  parent.command("uninstall").description("Remove rAgent from this machine (stops service, deletes config, unlinks CLI)").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
@@ -5651,7 +6065,7 @@ function registerUninstallCommand(parent) {
5651
6065
  await uninstallAgent(opts);
5652
6066
  } catch (error) {
5653
6067
  const message = error instanceof Error ? error.message : String(error);
5654
- console.error(`[rAgent] Uninstall failed: ${message}`);
6068
+ log21.error("uninstall failed", { error: message });
5655
6069
  process.exit(1);
5656
6070
  }
5657
6071
  });
@@ -5710,9 +6124,10 @@ function registerDiscoverCommand(program2) {
5710
6124
  }
5711
6125
 
5712
6126
  // src/index.ts
6127
+ var log22 = createLogger("cli");
5713
6128
  process.on("unhandledRejection", (reason) => {
5714
6129
  const message = reason instanceof Error ? reason.stack || reason.message : String(reason);
5715
- console.error(`[rAgent] Unhandled promise rejection: ${message}`);
6130
+ log22.error("unhandled promise rejection", { error: message });
5716
6131
  });
5717
6132
  import_commander.program.name("ragent").description("Connect machines to rAgent Live").version(CURRENT_VERSION);
5718
6133
  registerConnectCommand(import_commander.program);