ragent-cli 1.8.0 → 1.9.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.
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.8.0",
34
+ version: "1.9.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: {
@@ -43,6 +43,8 @@ var require_package = __commonJS({
43
43
  test: "vitest run",
44
44
  typecheck: "tsc --noEmit",
45
45
  lint: "eslint src/",
46
+ sbom: "cyclonedx-npm --omit dev --output-reproducible --validate --mc-type application -o dist/sbom.json",
47
+ "verify-package": "bash scripts/verify-package.sh",
46
48
  changeset: "changeset",
47
49
  "version-packages": "changeset version",
48
50
  release: "npm run build && npm run test && changeset publish"
@@ -75,7 +77,8 @@ var require_package = __commonJS({
75
77
  "linux"
76
78
  ],
77
79
  files: [
78
- "dist"
80
+ "dist",
81
+ "SECURITY.md"
79
82
  ],
80
83
  dependencies: {
81
84
  "@azure/web-pubsub-client": "^1.0.2",
@@ -87,6 +90,7 @@ var require_package = __commonJS({
87
90
  devDependencies: {
88
91
  "@changesets/changelog-github": "^0.5.2",
89
92
  "@changesets/cli": "^2.29.8",
93
+ "@cyclonedx/cyclonedx-npm": "^4.2.1",
90
94
  "@eslint/js": "^10.0.1",
91
95
  "@types/figlet": "^1.7.0",
92
96
  "@types/node": "^20.17.0",
@@ -102,7 +106,7 @@ var require_package = __commonJS({
102
106
  });
103
107
 
104
108
  // src/index.ts
105
- var fs6 = __toESM(require("fs"));
109
+ var fs7 = __toESM(require("fs"));
106
110
  var import_commander = require("commander");
107
111
 
108
112
  // src/constants.ts
@@ -231,9 +235,9 @@ async function checkForUpdate(opts = {}) {
231
235
  const config = loadConfig();
232
236
  const lastChecked = config.updateCheckedAt ? new Date(config.updateCheckedAt).getTime() : 0;
233
237
  const intervalMs = 12 * 60 * 60 * 1e3;
234
- const now = Date.now();
238
+ const now2 = Date.now();
235
239
  let latestVersion = null;
236
- if (!force && lastChecked > 0 && now - lastChecked < intervalMs) {
240
+ if (!force && lastChecked > 0 && now2 - lastChecked < intervalMs) {
237
241
  latestVersion = config.latestKnownVersion || null;
238
242
  } else {
239
243
  latestVersion = await fetchLatestVersion();
@@ -260,7 +264,7 @@ async function maybeWarnUpdate() {
260
264
  var os9 = __toESM(require("os"));
261
265
 
262
266
  // src/agent.ts
263
- var fs4 = __toESM(require("fs"));
267
+ var fs5 = __toESM(require("fs"));
264
268
  var os8 = __toESM(require("os"));
265
269
  var path4 = __toESM(require("path"));
266
270
  var import_ws5 = __toESM(require("ws"));
@@ -269,6 +273,7 @@ var import_ws5 = __toESM(require("ws"));
269
273
  var os4 = __toESM(require("os"));
270
274
 
271
275
  // src/sessions.ts
276
+ var fs2 = __toESM(require("fs"));
272
277
  var os3 = __toESM(require("os"));
273
278
 
274
279
  // src/system.ts
@@ -361,6 +366,95 @@ async function installTmuxInteractively() {
361
366
 
362
367
  // src/sessions.ts
363
368
  var isMac = os3.platform() === "darwin";
369
+ function getProcessStartTime(pid) {
370
+ if (isMac) return null;
371
+ try {
372
+ const stat = fs2.readFileSync(`/proc/${pid}/stat`, "utf8");
373
+ const closeParen = stat.lastIndexOf(")");
374
+ if (closeParen < 0) return null;
375
+ const rest = stat.slice(closeParen + 2).split(" ");
376
+ const starttime = Number(rest[19]);
377
+ return Number.isFinite(starttime) && starttime >= 0 ? starttime : null;
378
+ } catch {
379
+ return null;
380
+ }
381
+ }
382
+ function collectStartTimes(pids) {
383
+ return pids.map((pid) => getProcessStartTime(pid) ?? 0);
384
+ }
385
+ async function detectVscodeEnvironment(pid) {
386
+ if (!isMac) {
387
+ try {
388
+ const environ = fs2.readFileSync(`/proc/${pid}/environ`, "utf8");
389
+ if (environ.includes("VSCODE_PID=") || environ.includes("TERM_PROGRAM=vscode")) {
390
+ return true;
391
+ }
392
+ } catch {
393
+ }
394
+ }
395
+ try {
396
+ const raw = await execAsync(`ps -p ${pid} -o ppid=`);
397
+ const ppid = Number(raw.trim());
398
+ if (ppid > 1) {
399
+ const parentInfo = await execAsync(`ps -p ${ppid} -o comm=`);
400
+ const parentComm = parentInfo.trim().toLowerCase();
401
+ if (parentComm === "code" || parentComm === "code-server") {
402
+ return true;
403
+ }
404
+ }
405
+ } catch {
406
+ }
407
+ return false;
408
+ }
409
+ function classifyAgent(command, options) {
410
+ if (command) {
411
+ const result = classifyFromCommand(command);
412
+ if (result) return result;
413
+ }
414
+ if (options?.workingDir) {
415
+ const dirName = options.workingDir.split("/").pop()?.toLowerCase() ?? "";
416
+ const cwdAgentMap = {
417
+ ".claude": "Claude Code",
418
+ "claude-code": "Claude Code",
419
+ ".codex": "Codex CLI",
420
+ ".aider": "aider"
421
+ };
422
+ const agentType = cwdAgentMap[dirName];
423
+ if (agentType) {
424
+ return { agentType, confidence: 0.5 };
425
+ }
426
+ }
427
+ if (options?.parentCommand) {
428
+ const parentAgent = detectAgentType(options.parentCommand);
429
+ if (parentAgent) {
430
+ return { agentType: parentAgent, confidence: 0.6 };
431
+ }
432
+ }
433
+ return null;
434
+ }
435
+ function classifyFromCommand(command) {
436
+ const agentType = detectAgentType(command);
437
+ if (!agentType) return null;
438
+ const cmd = command.toLowerCase();
439
+ const parts = cmd.split(/\s+/);
440
+ const binary = parts[0]?.split("/").pop() ?? "";
441
+ const exactMatches = {
442
+ "claude": "Claude Code",
443
+ "claude-code": "Claude Code",
444
+ "codex": "Codex CLI",
445
+ "aider": "aider",
446
+ "cursor": "Cursor",
447
+ "windsurf": "Windsurf",
448
+ "gemini": "Gemini CLI",
449
+ "amazon-q": "Amazon Q",
450
+ "amazon_q": "Amazon Q",
451
+ "copilot": "Copilot CLI"
452
+ };
453
+ if (exactMatches[binary] === agentType) {
454
+ return { agentType, confidence: 1 };
455
+ }
456
+ return { agentType, confidence: 0.7 };
457
+ }
364
458
  async function collectTmuxSessions() {
365
459
  try {
366
460
  await execAsync("tmux -V");
@@ -399,21 +493,26 @@ async function collectTmuxSessions() {
399
493
  const pids = [];
400
494
  const pid = Number(panePid);
401
495
  if (pid > 0) pids.push(pid);
496
+ const classification = classifyAgent(command, { workingDir: paneCurrentPath });
497
+ const vscodeDetected = pid > 0 ? await detectVscodeEnvironment(pid) : false;
402
498
  results.push({
403
499
  id,
404
500
  type: "tmux",
405
501
  name: `${sessionName}:${windowIndex}.${paneIndex}`,
406
502
  status: activeFlag === "1" ? "active" : "detached",
407
503
  command,
408
- agentType: detectAgentType(command),
504
+ agentType: classification?.agentType,
505
+ agentConfidence: classification?.confidence,
409
506
  runningCommand: command || void 0,
410
507
  windowLayout: windowLayout || void 0,
411
508
  workingDir: paneCurrentPath || void 0,
509
+ environment: vscodeDetected ? "vscode" : void 0,
412
510
  windowName: windowName || void 0,
413
511
  paneTitle: paneTitle || void 0,
414
512
  isZoomed: windowFlags?.includes("Z") ?? false,
415
513
  lastActivityAt,
416
514
  pids,
515
+ startTimes: pids.length > 0 ? collectStartTimes(pids) : void 0,
417
516
  sessionGroup: sessionGroup || void 0
418
517
  });
419
518
  }
@@ -440,15 +539,19 @@ async function collectScreenSessions() {
440
539
  const childInfo = await getChildAgentInfo(pid);
441
540
  if (!childInfo) continue;
442
541
  const id = `screen:${sessionName}:${screenPid}`;
542
+ const allPids = [pid, ...childInfo.childPids];
543
+ const classification = classifyAgent(childInfo.command);
443
544
  sessions.push({
444
545
  id,
445
546
  type: "screen",
446
547
  name: `${sessionName}`,
447
548
  status: state === "Attached" ? "active" : "detached",
448
549
  command: childInfo.command,
449
- agentType: childInfo.agentType,
550
+ agentType: classification?.agentType ?? childInfo.agentType,
551
+ agentConfidence: classification?.confidence,
450
552
  lastActivityAt: (/* @__PURE__ */ new Date()).toISOString(),
451
- pids: [pid, ...childInfo.childPids]
553
+ pids: allPids,
554
+ startTimes: collectStartTimes(allPids)
452
555
  });
453
556
  }
454
557
  return sessions;
@@ -488,15 +591,19 @@ async function collectZellijSessions() {
488
591
  }
489
592
  if (!foundAgent) continue;
490
593
  const id = `zellij:${sessionName}`;
594
+ const allPids = [...serverPids, ...allChildPids];
595
+ const classification = classifyAgent(foundAgent.command);
491
596
  sessions.push({
492
597
  id,
493
598
  type: "zellij",
494
599
  name: sessionName,
495
600
  status: "active",
496
601
  command: foundAgent.command,
497
- agentType: foundAgent.agentType,
602
+ agentType: classification?.agentType ?? foundAgent.agentType,
603
+ agentConfidence: classification?.confidence,
498
604
  lastActivityAt: (/* @__PURE__ */ new Date()).toISOString(),
499
- pids: [...serverPids, ...allChildPids]
605
+ pids: allPids,
606
+ startTimes: collectStartTimes(allPids)
500
607
  });
501
608
  }
502
609
  return sessions;
@@ -539,14 +646,15 @@ async function collectBareAgentProcesses(excludePids) {
539
646
  const stat = parts[2];
540
647
  const args = parts.slice(4).join(" ");
541
648
  if (stat.startsWith("Z")) continue;
542
- const agentType = detectAgentType(args);
543
- if (!agentType) continue;
649
+ const workingDir = await getProcessWorkingDir(pid);
650
+ const classification = classifyAgent(args, { workingDir: workingDir ?? void 0 });
651
+ if (!classification) continue;
544
652
  if (excludePids?.has(pid)) continue;
545
653
  if (seen.has(pid)) continue;
546
654
  seen.add(pid);
547
- const workingDir = await getProcessWorkingDir(pid);
548
655
  const dirName = workingDir ? workingDir.split("/").pop() : null;
549
- const displayName = dirName && dirName !== "/" && dirName !== "" ? `${agentType} (${dirName})` : `${agentType} (pid ${pid})`;
656
+ const displayName = dirName && dirName !== "/" && dirName !== "" ? `${classification.agentType} (${dirName})` : `${classification.agentType} (pid ${pid})`;
657
+ const vscodeDetected = await detectVscodeEnvironment(pid);
550
658
  const id = `process:${pid}`;
551
659
  sessions.push({
552
660
  id,
@@ -554,10 +662,13 @@ async function collectBareAgentProcesses(excludePids) {
554
662
  name: displayName,
555
663
  status: "active",
556
664
  command: args,
557
- agentType,
665
+ agentType: classification.agentType,
666
+ agentConfidence: classification.confidence,
558
667
  workingDir: workingDir || void 0,
668
+ environment: vscodeDetected ? "vscode" : void 0,
559
669
  lastActivityAt: (/* @__PURE__ */ new Date()).toISOString(),
560
- pids: [pid]
670
+ pids: [pid],
671
+ startTimes: collectStartTimes([pid])
561
672
  });
562
673
  }
563
674
  return sessions;
@@ -590,13 +701,15 @@ async function collectSessionInventory(hostId, command) {
590
701
  }
591
702
  }
592
703
  const bare = await collectBareAgentProcesses(multiplexerPids);
704
+ const ptyClassification = classifyAgent(command);
593
705
  const ptySession = {
594
706
  id: `pty:${hostId}`,
595
707
  type: "pty",
596
708
  name: command,
597
709
  status: "active",
598
710
  command,
599
- agentType: detectAgentType(command),
711
+ agentType: ptyClassification?.agentType,
712
+ agentConfidence: ptyClassification?.confidence,
600
713
  lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
601
714
  };
602
715
  return [...tmux, ...screen, ...zellij, ...bare, ptySession];
@@ -2147,7 +2260,7 @@ var import_ws4 = __toESM(require("ws"));
2147
2260
 
2148
2261
  // src/service.ts
2149
2262
  var import_child_process2 = require("child_process");
2150
- var fs2 = __toESM(require("fs"));
2263
+ var fs3 = __toESM(require("fs"));
2151
2264
  var os6 = __toESM(require("os"));
2152
2265
  var path2 = __toESM(require("path"));
2153
2266
  function assertConfiguredAgentToken() {
@@ -2161,8 +2274,8 @@ function getConfiguredServiceBackend() {
2161
2274
  if (config.serviceBackend === "systemd" || config.serviceBackend === "pidfile") {
2162
2275
  return config.serviceBackend;
2163
2276
  }
2164
- if (fs2.existsSync(SERVICE_FILE)) return "systemd";
2165
- if (fs2.existsSync(FALLBACK_PID_FILE)) return "pidfile";
2277
+ if (fs3.existsSync(SERVICE_FILE)) return "systemd";
2278
+ if (fs3.existsSync(FALLBACK_PID_FILE)) return "pidfile";
2166
2279
  return null;
2167
2280
  }
2168
2281
  async function canUseSystemdUser() {
@@ -2205,8 +2318,8 @@ WantedBy=default.target
2205
2318
  }
2206
2319
  async function installSystemdService(opts = {}) {
2207
2320
  assertConfiguredAgentToken();
2208
- fs2.mkdirSync(SERVICE_DIR, { recursive: true });
2209
- fs2.writeFileSync(SERVICE_FILE, buildSystemdUnit(), "utf8");
2321
+ fs3.mkdirSync(SERVICE_DIR, { recursive: true });
2322
+ fs3.writeFileSync(SERVICE_FILE, buildSystemdUnit(), "utf8");
2210
2323
  await runSystemctlUser(["daemon-reload"]);
2211
2324
  if (opts.enable !== false) {
2212
2325
  await runSystemctlUser(["enable", SERVICE_NAME]);
@@ -2219,7 +2332,7 @@ async function installSystemdService(opts = {}) {
2219
2332
  }
2220
2333
  function readFallbackPid() {
2221
2334
  try {
2222
- const raw = fs2.readFileSync(FALLBACK_PID_FILE, "utf8").trim();
2335
+ const raw = fs3.readFileSync(FALLBACK_PID_FILE, "utf8").trim();
2223
2336
  const pid = Number.parseInt(raw, 10);
2224
2337
  return Number.isInteger(pid) ? pid : null;
2225
2338
  } catch {
@@ -2238,14 +2351,14 @@ function isProcessRunning(pid) {
2238
2351
  var LOG_ROTATION_MAX_BYTES = 10 * 1024 * 1024;
2239
2352
  function rotateLogIfNeeded() {
2240
2353
  try {
2241
- const stat = fs2.statSync(FALLBACK_LOG_FILE);
2354
+ const stat = fs3.statSync(FALLBACK_LOG_FILE);
2242
2355
  if (stat.size > LOG_ROTATION_MAX_BYTES) {
2243
2356
  const rotated = `${FALLBACK_LOG_FILE}.1`;
2244
2357
  try {
2245
- fs2.unlinkSync(rotated);
2358
+ fs3.unlinkSync(rotated);
2246
2359
  } catch {
2247
2360
  }
2248
- fs2.renameSync(FALLBACK_LOG_FILE, rotated);
2361
+ fs3.renameSync(FALLBACK_LOG_FILE, rotated);
2249
2362
  }
2250
2363
  } catch {
2251
2364
  }
@@ -2259,7 +2372,7 @@ async function startPidfileService() {
2259
2372
  }
2260
2373
  ensureConfigDir();
2261
2374
  rotateLogIfNeeded();
2262
- const logFd = fs2.openSync(FALLBACK_LOG_FILE, "a");
2375
+ const logFd = fs3.openSync(FALLBACK_LOG_FILE, "a");
2263
2376
  const child = (0, import_child_process2.spawn)(process.execPath, [__filename, "run"], {
2264
2377
  detached: true,
2265
2378
  stdio: ["ignore", logFd, logFd],
@@ -2267,8 +2380,8 @@ async function startPidfileService() {
2267
2380
  env: process.env
2268
2381
  });
2269
2382
  child.unref();
2270
- fs2.closeSync(logFd);
2271
- fs2.writeFileSync(FALLBACK_PID_FILE, `${child.pid}
2383
+ fs3.closeSync(logFd);
2384
+ fs3.writeFileSync(FALLBACK_PID_FILE, `${child.pid}
2272
2385
  `, "utf8");
2273
2386
  saveConfigPatch({ serviceBackend: "pidfile" });
2274
2387
  console.log(`[rAgent] Started fallback background service (pid ${child.pid})`);
@@ -2278,7 +2391,7 @@ async function stopPidfileService() {
2278
2391
  const pid = readFallbackPid();
2279
2392
  if (!pid || !isProcessRunning(pid)) {
2280
2393
  try {
2281
- fs2.unlinkSync(FALLBACK_PID_FILE);
2394
+ fs3.unlinkSync(FALLBACK_PID_FILE);
2282
2395
  } catch {
2283
2396
  }
2284
2397
  console.log("[rAgent] Service is not running.");
@@ -2293,7 +2406,7 @@ async function stopPidfileService() {
2293
2406
  process.kill(pid, "SIGKILL");
2294
2407
  }
2295
2408
  try {
2296
- fs2.unlinkSync(FALLBACK_PID_FILE);
2409
+ fs3.unlinkSync(FALLBACK_PID_FILE);
2297
2410
  } catch {
2298
2411
  }
2299
2412
  console.log(`[rAgent] Stopped fallback background service (pid ${pid})`);
@@ -2338,12 +2451,12 @@ async function stopService() {
2338
2451
  }
2339
2452
  function killStaleAgentProcesses() {
2340
2453
  try {
2341
- const entries = fs2.readdirSync(CONFIG_DIR);
2454
+ const entries = fs3.readdirSync(CONFIG_DIR);
2342
2455
  for (const entry of entries) {
2343
2456
  if (!entry.startsWith("agent-") || !entry.endsWith(".pid")) continue;
2344
2457
  const pidPath = path2.join(CONFIG_DIR, entry);
2345
2458
  try {
2346
- const raw = fs2.readFileSync(pidPath, "utf8").trim();
2459
+ const raw = fs3.readFileSync(pidPath, "utf8").trim();
2347
2460
  const pid = Number.parseInt(raw, 10);
2348
2461
  if (!Number.isInteger(pid) || pid <= 0) continue;
2349
2462
  try {
@@ -2352,7 +2465,7 @@ function killStaleAgentProcesses() {
2352
2465
  console.log(`[rAgent] Stopped stale agent process (pid ${pid})`);
2353
2466
  } catch {
2354
2467
  }
2355
- fs2.unlinkSync(pidPath);
2468
+ fs3.unlinkSync(pidPath);
2356
2469
  } catch {
2357
2470
  }
2358
2471
  }
@@ -2415,7 +2528,7 @@ async function printServiceLogs(opts) {
2415
2528
  process.stdout.write(output);
2416
2529
  return;
2417
2530
  }
2418
- if (!fs2.existsSync(FALLBACK_LOG_FILE)) {
2531
+ if (!fs3.existsSync(FALLBACK_LOG_FILE)) {
2419
2532
  console.log(`[rAgent] No log file found at ${FALLBACK_LOG_FILE}`);
2420
2533
  return;
2421
2534
  }
@@ -2428,7 +2541,7 @@ async function printServiceLogs(opts) {
2428
2541
  });
2429
2542
  return;
2430
2543
  }
2431
- const content = fs2.readFileSync(FALLBACK_LOG_FILE, "utf8");
2544
+ const content = fs3.readFileSync(FALLBACK_LOG_FILE, "utf8");
2432
2545
  const tail = content.split("\n").slice(-lines).join("\n");
2433
2546
  process.stdout.write(`${tail}${tail.endsWith("\n") ? "" : "\n"}`);
2434
2547
  }
@@ -2438,7 +2551,7 @@ async function uninstallService() {
2438
2551
  await runSystemctlUser(["stop", SERVICE_NAME]).catch(() => void 0);
2439
2552
  await runSystemctlUser(["disable", SERVICE_NAME]).catch(() => void 0);
2440
2553
  try {
2441
- fs2.unlinkSync(SERVICE_FILE);
2554
+ fs3.unlinkSync(SERVICE_FILE);
2442
2555
  } catch {
2443
2556
  }
2444
2557
  await runSystemctlUser(["daemon-reload"]).catch(() => void 0);
@@ -2462,7 +2575,7 @@ function requestStopSelfService() {
2462
2575
  }
2463
2576
  if (backend === "pidfile") {
2464
2577
  try {
2465
- fs2.unlinkSync(FALLBACK_PID_FILE);
2578
+ fs3.unlinkSync(FALLBACK_PID_FILE);
2466
2579
  } catch {
2467
2580
  }
2468
2581
  }
@@ -2681,6 +2794,7 @@ var ControlDispatcher = class {
2681
2794
  connection;
2682
2795
  transcriptWatcher;
2683
2796
  options;
2797
+ _approvalEnforcer = null;
2684
2798
  /** Set to true when a reconnect was requested (restart-agent, disconnect). */
2685
2799
  reconnectRequested = false;
2686
2800
  /** Set to false to stop the agent. */
@@ -2697,6 +2811,25 @@ var ControlDispatcher = class {
2697
2811
  updateOptions(options) {
2698
2812
  this.options = options;
2699
2813
  }
2814
+ /** Get or lazily create the approval enforcer. Returns null if no session key. */
2815
+ get approvalEnforcer() {
2816
+ return this._approvalEnforcer;
2817
+ }
2818
+ /** Set the approval enforcer (called when connection is established with a session key). */
2819
+ setApprovalEnforcer(enforcer) {
2820
+ this._approvalEnforcer?.stopAll();
2821
+ this._approvalEnforcer = enforcer;
2822
+ }
2823
+ /**
2824
+ * Handle an incoming approval response message from the portal.
2825
+ * Routes `subtype: "response"` to the ApprovalEnforcer.
2826
+ */
2827
+ handleApprovalMessage(payload) {
2828
+ if (payload.subtype !== "response" || !this._approvalEnforcer) return;
2829
+ const response = payload.response;
2830
+ if (!response || typeof response !== "object" || Array.isArray(response)) return;
2831
+ this._approvalEnforcer.handleResponse(response);
2832
+ }
2700
2833
  /**
2701
2834
  * Verify HMAC-SHA256 signature on a control message.
2702
2835
  * Returns true if valid or no session key is available (backward compat).
@@ -3050,11 +3183,788 @@ function validateProvisionRequest(payload) {
3050
3183
  };
3051
3184
  }
3052
3185
 
3186
+ // src/approval-policy.ts
3187
+ var import_node_crypto2 = require("crypto");
3188
+ function hasToken(cmd, token) {
3189
+ const lower = cmd.toLowerCase();
3190
+ const t = token.toLowerCase();
3191
+ const idx = lower.indexOf(t);
3192
+ if (idx === -1) return false;
3193
+ const before = idx > 0 ? lower[idx - 1] : " ";
3194
+ const after = idx + t.length < lower.length ? lower[idx + t.length] : " ";
3195
+ const boundaryChars = " \n|;&(){}\"'`.";
3196
+ return boundaryChars.includes(before) && (boundaryChars.includes(after) || after === void 0);
3197
+ }
3198
+ function has(cmd, substr) {
3199
+ return cmd.toLowerCase().includes(substr.toLowerCase());
3200
+ }
3201
+ var MATCHERS = [
3202
+ // ── Critical ──────────────────────────────────────────────────────
3203
+ {
3204
+ test: (cmd) => {
3205
+ if (!has(cmd, "rm ")) return false;
3206
+ const lower = cmd.toLowerCase();
3207
+ const rmIdx = lower.indexOf("rm ");
3208
+ const afterRm = lower.slice(rmIdx + 3);
3209
+ if (!afterRm.includes("r")) return false;
3210
+ return has(afterRm, " /") || has(afterRm, " ~/") || has(afterRm, " ~") || has(afterRm, "$home") || has(afterRm, " /*");
3211
+ },
3212
+ action: "file-delete",
3213
+ severity: "critical",
3214
+ description: "Recursive deletion of root, home, or wildcard path"
3215
+ },
3216
+ {
3217
+ test: (cmd) => has(cmd, "> /dev/sd") || has(cmd, ">/dev/sd"),
3218
+ action: "destructive-command",
3219
+ severity: "critical",
3220
+ description: "Direct write to block device"
3221
+ },
3222
+ {
3223
+ test: (cmd) => {
3224
+ const lower = cmd.toLowerCase();
3225
+ return has(lower, "drop table") || has(lower, "drop database") || has(lower, "drop schema");
3226
+ },
3227
+ action: "database-drop",
3228
+ severity: "critical",
3229
+ description: "SQL DROP TABLE/DATABASE/SCHEMA"
3230
+ },
3231
+ {
3232
+ test: (cmd) => hasToken(cmd, "mkfs"),
3233
+ action: "destructive-command",
3234
+ severity: "critical",
3235
+ description: "Filesystem format command"
3236
+ },
3237
+ {
3238
+ test: (cmd) => has(cmd, "dd ") && has(cmd, "if="),
3239
+ action: "destructive-command",
3240
+ severity: "critical",
3241
+ description: "Low-level disk copy (dd)"
3242
+ },
3243
+ // ── High ──────────────────────────────────────────────────────────
3244
+ {
3245
+ test: (cmd) => {
3246
+ if (!has(cmd, "git ") || !has(cmd, "push")) return false;
3247
+ return has(cmd, "--force") || has(cmd, " -f") || has(cmd, " -f ");
3248
+ },
3249
+ action: "git-force-push",
3250
+ severity: "high",
3251
+ description: "Git force push"
3252
+ },
3253
+ {
3254
+ test: (cmd) => has(cmd, "git ") && has(cmd, "reset") && has(cmd, "--hard"),
3255
+ action: "git-reset-hard",
3256
+ severity: "high",
3257
+ description: "Git hard reset"
3258
+ },
3259
+ // ── Medium ────────────────────────────────────────────────────────
3260
+ {
3261
+ test: (cmd) => has(cmd, "kill ") && has(cmd, "-9"),
3262
+ action: "process-kill",
3263
+ severity: "medium",
3264
+ description: "Forceful process kill (SIGKILL)"
3265
+ },
3266
+ {
3267
+ test: (cmd) => hasToken(cmd, "killall"),
3268
+ action: "process-kill",
3269
+ severity: "medium",
3270
+ description: "Kill processes by name"
3271
+ },
3272
+ {
3273
+ test: (cmd) => has(cmd, "chmod ") && has(cmd, "777"),
3274
+ action: "destructive-command",
3275
+ severity: "medium",
3276
+ description: "World-writable permission change"
3277
+ }
3278
+ ];
3279
+ var DEFAULT_APPROVAL_POLICY = {
3280
+ patterns: MATCHERS.map((m) => ({
3281
+ match: "",
3282
+ // Not used — string matching via MATCHERS
3283
+ action: m.action,
3284
+ severity: m.severity,
3285
+ description: m.description
3286
+ })),
3287
+ defaultTimeoutMs: 12e4,
3288
+ fallbackOnDisconnect: "deny"
3289
+ };
3290
+ function matchPolicy(command, _policy = DEFAULT_APPROVAL_POLICY, hostId = "", sessionId = "") {
3291
+ for (const matcher of MATCHERS) {
3292
+ if (matcher.test(command)) {
3293
+ return {
3294
+ requestId: (0, import_node_crypto2.randomUUID)(),
3295
+ hostId,
3296
+ sessionId,
3297
+ action: matcher.action,
3298
+ severity: matcher.severity,
3299
+ description: matcher.description,
3300
+ command,
3301
+ timeoutMs: _policy.defaultTimeoutMs,
3302
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString()
3303
+ };
3304
+ }
3305
+ }
3306
+ return null;
3307
+ }
3308
+
3309
+ // src/approval-crypto.ts
3310
+ var crypto3 = __toESM(require("crypto"));
3311
+ function canonicalise(grant) {
3312
+ return JSON.stringify(grant, Object.keys(grant).sort());
3313
+ }
3314
+ function signApprovalGrant(grant, secret) {
3315
+ const payload = canonicalise(grant);
3316
+ return crypto3.createHmac("sha256", secret).update(payload).digest("hex");
3317
+ }
3318
+ function verifyApprovalGrant(grant, secret) {
3319
+ if (!grant.jti || typeof grant.jti !== "string") return false;
3320
+ if (!grant.signature || typeof grant.signature !== "string") return false;
3321
+ if (!/^[0-9a-f]{64}$/i.test(grant.signature)) return false;
3322
+ if (isGrantExpired(grant)) return false;
3323
+ const { signature: _sig, ...unsigned } = grant;
3324
+ const expected = signApprovalGrant(unsigned, secret);
3325
+ return crypto3.timingSafeEqual(
3326
+ Buffer.from(grant.signature, "hex"),
3327
+ Buffer.from(expected, "hex")
3328
+ );
3329
+ }
3330
+ function isGrantExpired(grant) {
3331
+ const expiry = new Date(grant.expiresAt).getTime();
3332
+ if (Number.isNaN(expiry)) return true;
3333
+ return Date.now() > expiry;
3334
+ }
3335
+
3336
+ // src/approval-enforcer.ts
3337
+ var ApprovalEnforcer = class {
3338
+ policy;
3339
+ secret;
3340
+ pendingRequests = /* @__PURE__ */ new Map();
3341
+ /** Map of JTI → expiry timestamp for one-time grant enforcement. */
3342
+ usedGrantJtis = /* @__PURE__ */ new Map();
3343
+ sendRequestFn;
3344
+ constructor(options) {
3345
+ this.policy = options.policy ?? DEFAULT_APPROVAL_POLICY;
3346
+ this.secret = options.secret;
3347
+ this.sendRequestFn = options.sendRequest;
3348
+ }
3349
+ // -----------------------------------------------------------------------
3350
+ // Public API
3351
+ // -----------------------------------------------------------------------
3352
+ /**
3353
+ * Check a command against the policy. If it matches, emit an approval
3354
+ * request and return a promise that resolves when approved / denied /
3355
+ * timed out. Returns `null` if no policy match (command is safe).
3356
+ */
3357
+ async checkCommand(command, hostId, sessionId) {
3358
+ const request = matchPolicy(command, this.policy, hostId, sessionId);
3359
+ if (!request) return null;
3360
+ console.log(
3361
+ `[rAgent] Approval: ${request.action} (${request.severity}) \u2014 pending [${request.requestId}]`
3362
+ );
3363
+ return new Promise((resolve) => {
3364
+ const timer = setTimeout(() => {
3365
+ this.pendingRequests.delete(request.requestId);
3366
+ const response = { type: "timeout" };
3367
+ console.log(
3368
+ `[rAgent] Approval: ${request.action} (${request.severity}) \u2014 timeout [${request.requestId}]`
3369
+ );
3370
+ resolve(response);
3371
+ }, request.timeoutMs);
3372
+ this.pendingRequests.set(request.requestId, { request, resolve, timer });
3373
+ this.sendRequestFn(request);
3374
+ });
3375
+ }
3376
+ /**
3377
+ * Handle an incoming approval response from the portal.
3378
+ * Routes to the pending promise for the corresponding request ID.
3379
+ */
3380
+ handleResponse(response) {
3381
+ const requestId = response.type === "approved" ? response.grant.requestId : response.type === "denied" ? response.denial.requestId : null;
3382
+ if (!requestId) return;
3383
+ const pending = this.pendingRequests.get(requestId);
3384
+ if (!pending) {
3385
+ console.warn(
3386
+ `[rAgent] Approval: received response for unknown request [${requestId}]`
3387
+ );
3388
+ return;
3389
+ }
3390
+ if (response.type === "approved") {
3391
+ if (!this.acceptGrant(response.grant)) {
3392
+ console.log(
3393
+ `[rAgent] Approval: ${pending.request.action} (${pending.request.severity}) \u2014 rejected (invalid grant) [${requestId}]`
3394
+ );
3395
+ clearTimeout(pending.timer);
3396
+ this.pendingRequests.delete(requestId);
3397
+ pending.resolve({ type: "denied", denial: { requestId, deniedBy: "system", deniedAt: (/* @__PURE__ */ new Date()).toISOString(), reason: "Invalid or expired grant" } });
3398
+ return;
3399
+ }
3400
+ console.log(
3401
+ `[rAgent] Approval: ${pending.request.action} (${pending.request.severity}) \u2014 approved [${requestId}]`
3402
+ );
3403
+ } else if (response.type === "denied") {
3404
+ const reason = response.denial.reason ? ` (${response.denial.reason})` : "";
3405
+ console.log(
3406
+ `[rAgent] Approval: ${pending.request.action} (${pending.request.severity}) \u2014 denied${reason} [${requestId}]`
3407
+ );
3408
+ }
3409
+ clearTimeout(pending.timer);
3410
+ this.pendingRequests.delete(requestId);
3411
+ pending.resolve(response);
3412
+ }
3413
+ /**
3414
+ * Clean up expired pending requests and stale used-JTI entries.
3415
+ *
3416
+ * Call periodically (e.g. every 5 minutes) to bound memory usage of the
3417
+ * `usedGrantJtis` set. In practice, JTIs are only relevant for the
3418
+ * lifetime of the grant (a few minutes), so clearing the set periodically
3419
+ * is safe.
3420
+ */
3421
+ cleanup() {
3422
+ const now2 = Date.now();
3423
+ for (const [id, entry] of this.pendingRequests) {
3424
+ const requestedAt = new Date(entry.request.requestedAt).getTime();
3425
+ if (!Number.isNaN(requestedAt) && now2 - requestedAt > entry.request.timeoutMs * 2) {
3426
+ clearTimeout(entry.timer);
3427
+ this.pendingRequests.delete(id);
3428
+ entry.resolve({ type: "timeout" });
3429
+ }
3430
+ }
3431
+ const jtiNow = Date.now();
3432
+ for (const [jti, expiresAt] of this.usedGrantJtis) {
3433
+ if (expiresAt < jtiNow) this.usedGrantJtis.delete(jti);
3434
+ }
3435
+ }
3436
+ /**
3437
+ * Stop all pending requests, resolving each with a timeout response.
3438
+ */
3439
+ stopAll() {
3440
+ for (const [id, entry] of this.pendingRequests) {
3441
+ clearTimeout(entry.timer);
3442
+ this.pendingRequests.delete(id);
3443
+ console.log(
3444
+ `[rAgent] Approval: ${entry.request.action} (${entry.request.severity}) \u2014 stopped [${id}]`
3445
+ );
3446
+ entry.resolve({ type: "timeout" });
3447
+ }
3448
+ }
3449
+ /** Number of pending requests (exposed for testing). */
3450
+ get pendingCount() {
3451
+ return this.pendingRequests.size;
3452
+ }
3453
+ // -----------------------------------------------------------------------
3454
+ // Private
3455
+ // -----------------------------------------------------------------------
3456
+ /**
3457
+ * Verify and accept a grant. Returns `true` if the grant is valid,
3458
+ * correctly signed, not expired, and its JTI has not been seen before.
3459
+ */
3460
+ acceptGrant(grant) {
3461
+ if (!verifyApprovalGrant(grant, this.secret)) return false;
3462
+ if (this.usedGrantJtis.has(grant.jti)) return false;
3463
+ const expiresAt = new Date(grant.expiresAt).getTime();
3464
+ this.usedGrantJtis.set(grant.jti, Number.isNaN(expiresAt) ? Date.now() + 6e5 : expiresAt);
3465
+ return true;
3466
+ }
3467
+ };
3468
+
3053
3469
  // src/transcript-watcher.ts
3054
- var fs3 = __toESM(require("fs"));
3470
+ var fs4 = __toESM(require("fs"));
3055
3471
  var path3 = __toESM(require("path"));
3056
3472
  var os7 = __toESM(require("os"));
3057
3473
  var import_child_process5 = require("child_process");
3474
+
3475
+ // src/transcript-parser-v2.ts
3476
+ var blockCounter = 0;
3477
+ function genBlockId(prefix) {
3478
+ return `${prefix}-${Date.now()}-${(++blockCounter).toString(36)}`;
3479
+ }
3480
+ function now() {
3481
+ return (/* @__PURE__ */ new Date()).toISOString();
3482
+ }
3483
+ function makeTextBlock(blockId, text, isComplete) {
3484
+ const ts = now();
3485
+ return {
3486
+ blockId,
3487
+ kind: "text",
3488
+ format: "markdown",
3489
+ text,
3490
+ isComplete,
3491
+ createdAt: ts,
3492
+ updatedAt: ts
3493
+ };
3494
+ }
3495
+ function makeToolBlock(blockId, callId, toolName, state, input, isComplete = false) {
3496
+ const ts = now();
3497
+ return {
3498
+ blockId,
3499
+ kind: "tool",
3500
+ callId,
3501
+ toolName,
3502
+ state,
3503
+ input,
3504
+ isComplete,
3505
+ createdAt: ts,
3506
+ updatedAt: ts
3507
+ };
3508
+ }
3509
+ var ClaudeCodeParserV2 = class {
3510
+ name = "claude-code-v2";
3511
+ /**
3512
+ * Maps tool_use id → { blockId, toolName } for correlating results.
3513
+ * The blockId is the V2 block key so we can update the same ToolBlock
3514
+ * when the tool_result arrives in a later user message.
3515
+ */
3516
+ pendingTools = /* @__PURE__ */ new Map();
3517
+ parseLine(line) {
3518
+ let obj;
3519
+ try {
3520
+ obj = JSON.parse(line);
3521
+ } catch {
3522
+ return [];
3523
+ }
3524
+ if (!obj.message?.content) return [];
3525
+ if (obj.type === "assistant") {
3526
+ return this.parseAssistant(obj);
3527
+ }
3528
+ if (obj.type === "user") {
3529
+ return this.parseUserToolResults(obj);
3530
+ }
3531
+ return [];
3532
+ }
3533
+ parseAssistant(obj) {
3534
+ const content = obj.message.content;
3535
+ const ops = [];
3536
+ const turnId = obj.uuid ?? `turn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3537
+ const timestamp = obj.timestamp ?? now();
3538
+ ops.push({
3539
+ op: "upsert_turn",
3540
+ turn: {
3541
+ turnId,
3542
+ role: "assistant",
3543
+ startedAt: timestamp,
3544
+ updatedAt: timestamp,
3545
+ isComplete: false
3546
+ }
3547
+ });
3548
+ for (const block of content) {
3549
+ if (block.type === "text" && typeof block.text === "string") {
3550
+ const text = block.text.trim();
3551
+ if (!text) continue;
3552
+ const blockId = genBlockId("txt");
3553
+ ops.push({
3554
+ op: "insert_block",
3555
+ turnId,
3556
+ block: makeTextBlock(blockId, text, true)
3557
+ });
3558
+ } else if (block.type === "tool_use" && block.name) {
3559
+ const callId = block.id ?? genBlockId("call");
3560
+ const blockId = genBlockId("tool");
3561
+ this.pendingTools.set(callId, { blockId, toolName: block.name });
3562
+ ops.push({
3563
+ op: "insert_block",
3564
+ turnId,
3565
+ block: makeToolBlock(
3566
+ blockId,
3567
+ callId,
3568
+ block.name,
3569
+ "running",
3570
+ block.input
3571
+ )
3572
+ });
3573
+ }
3574
+ }
3575
+ ops.push({
3576
+ op: "complete_turn",
3577
+ turnId,
3578
+ endedAt: timestamp,
3579
+ updatedAt: timestamp
3580
+ });
3581
+ if (this.pendingTools.size > 500) {
3582
+ const keys = [...this.pendingTools.keys()];
3583
+ for (let i = 0; i < 250; i++) {
3584
+ this.pendingTools.delete(keys[i]);
3585
+ }
3586
+ }
3587
+ return ops;
3588
+ }
3589
+ parseUserToolResults(obj) {
3590
+ const content = obj.message.content;
3591
+ const ops = [];
3592
+ const turnId = obj.uuid ?? `result-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3593
+ const timestamp = obj.timestamp ?? now();
3594
+ const userTextParts = [];
3595
+ for (const block of content) {
3596
+ if (block.type === "tool_result") {
3597
+ const callId = block.tool_use_id;
3598
+ const pending = callId ? this.pendingTools.get(callId) : void 0;
3599
+ const resultText = this.extractToolResultText(block);
3600
+ if (pending) {
3601
+ this.pendingTools.delete(callId);
3602
+ const newState = block.is_error ? "error" : "completed";
3603
+ if (resultText) {
3604
+ ops.push({
3605
+ op: "append_tool_output",
3606
+ turnId: "",
3607
+ // Will be resolved by the reducer (block lookup by blockId)
3608
+ blockId: pending.blockId,
3609
+ text: resultText
3610
+ });
3611
+ }
3612
+ ops.push({
3613
+ op: "set_tool_state",
3614
+ turnId: "",
3615
+ blockId: pending.blockId,
3616
+ state: newState,
3617
+ errorText: block.is_error ? resultText || "Tool error" : void 0,
3618
+ isComplete: true
3619
+ });
3620
+ ops.push({
3621
+ op: "complete_block",
3622
+ turnId: "",
3623
+ blockId: pending.blockId,
3624
+ updatedAt: timestamp
3625
+ });
3626
+ } else {
3627
+ if (!ops.some((o) => o.op === "upsert_turn")) {
3628
+ ops.unshift({
3629
+ op: "upsert_turn",
3630
+ turn: {
3631
+ turnId,
3632
+ role: "user",
3633
+ startedAt: timestamp,
3634
+ updatedAt: timestamp,
3635
+ isComplete: false
3636
+ }
3637
+ });
3638
+ }
3639
+ const blockId = genBlockId("tool");
3640
+ ops.push({
3641
+ op: "insert_block",
3642
+ turnId,
3643
+ block: makeToolBlock(
3644
+ blockId,
3645
+ callId ?? genBlockId("call"),
3646
+ "Tool",
3647
+ block.is_error ? "error" : "completed",
3648
+ void 0,
3649
+ true
3650
+ )
3651
+ });
3652
+ if (resultText) {
3653
+ ops.push({
3654
+ op: "append_tool_output",
3655
+ turnId,
3656
+ blockId,
3657
+ text: resultText
3658
+ });
3659
+ }
3660
+ }
3661
+ } else if (block.type === "text" && typeof block.text === "string") {
3662
+ const t = block.text.trim();
3663
+ if (t && !t.startsWith("<") && !t.startsWith("[Request interrupted")) {
3664
+ userTextParts.push(t);
3665
+ }
3666
+ }
3667
+ }
3668
+ if (userTextParts.length > 0) {
3669
+ if (!ops.some((o) => o.op === "upsert_turn")) {
3670
+ ops.unshift({
3671
+ op: "upsert_turn",
3672
+ turn: {
3673
+ turnId,
3674
+ role: "user",
3675
+ startedAt: timestamp,
3676
+ updatedAt: timestamp,
3677
+ isComplete: false
3678
+ }
3679
+ });
3680
+ }
3681
+ const blockId = genBlockId("txt");
3682
+ ops.push({
3683
+ op: "insert_block",
3684
+ turnId,
3685
+ block: makeTextBlock(blockId, userTextParts.join("\n"), true)
3686
+ });
3687
+ }
3688
+ if (ops.some((o) => o.op === "upsert_turn")) {
3689
+ ops.push({
3690
+ op: "complete_turn",
3691
+ turnId,
3692
+ endedAt: timestamp,
3693
+ updatedAt: timestamp
3694
+ });
3695
+ }
3696
+ return ops;
3697
+ }
3698
+ extractToolResultText(block) {
3699
+ const rc = block.content;
3700
+ if (typeof rc === "string") return rc;
3701
+ if (Array.isArray(rc)) {
3702
+ return rc.filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n");
3703
+ }
3704
+ return "";
3705
+ }
3706
+ };
3707
+ var CodexCliParserV2 = class {
3708
+ name = "codex-cli-v2";
3709
+ parseLine(line) {
3710
+ let obj;
3711
+ try {
3712
+ obj = JSON.parse(line);
3713
+ } catch {
3714
+ return [];
3715
+ }
3716
+ const item = obj.response_item;
3717
+ if (!item) return [];
3718
+ if (item.type === "response.output_item.done" && item.item?.type === "message") {
3719
+ const textParts = (item.item.content ?? []).filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text);
3720
+ const text = textParts.join("\n").trim();
3721
+ if (!text) return [];
3722
+ const turnId = item.id ?? `codex-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3723
+ const timestamp = obj.timestamp ?? now();
3724
+ const blockId = genBlockId("txt");
3725
+ return [
3726
+ {
3727
+ op: "upsert_turn",
3728
+ turn: {
3729
+ turnId,
3730
+ role: "assistant",
3731
+ startedAt: timestamp,
3732
+ updatedAt: timestamp,
3733
+ isComplete: false
3734
+ }
3735
+ },
3736
+ {
3737
+ op: "insert_block",
3738
+ turnId,
3739
+ block: makeTextBlock(blockId, text, true)
3740
+ },
3741
+ {
3742
+ op: "complete_turn",
3743
+ turnId,
3744
+ endedAt: timestamp,
3745
+ updatedAt: timestamp
3746
+ }
3747
+ ];
3748
+ }
3749
+ return [];
3750
+ }
3751
+ };
3752
+ var V2_FACTORIES = {
3753
+ "Claude Code": () => new ClaudeCodeParserV2(),
3754
+ "Codex CLI": () => new CodexCliParserV2()
3755
+ };
3756
+ function getParserV2(agentType) {
3757
+ if (!agentType) return void 0;
3758
+ const factory = V2_FACTORIES[agentType];
3759
+ return factory ? factory() : void 0;
3760
+ }
3761
+
3762
+ // src/transcript-protocol.ts
3763
+ var TRANSCRIPT_SCHEMA_VERSION = 2;
3764
+
3765
+ // src/transcript-reducer.ts
3766
+ function createEmptySnapshot(sessionId) {
3767
+ return {
3768
+ schemaVersion: TRANSCRIPT_SCHEMA_VERSION,
3769
+ sessionId,
3770
+ turnOrder: [],
3771
+ turns: {}
3772
+ };
3773
+ }
3774
+ function applyOps(state, ops) {
3775
+ const turns = { ...state.turns };
3776
+ const turnOrder = [...state.turnOrder];
3777
+ for (const op of ops) {
3778
+ switch (op.op) {
3779
+ case "upsert_turn": {
3780
+ const { turn } = op;
3781
+ const existing = turns[turn.turnId];
3782
+ if (existing) {
3783
+ turns[turn.turnId] = {
3784
+ ...existing,
3785
+ ...turn,
3786
+ blockOrder: turn.blockOrder ?? existing.blockOrder,
3787
+ blocks: turn.blocks ? { ...existing.blocks, ...turn.blocks } : existing.blocks
3788
+ };
3789
+ } else {
3790
+ turns[turn.turnId] = {
3791
+ ...turn,
3792
+ blockOrder: turn.blockOrder ?? [],
3793
+ blocks: turn.blocks ?? {}
3794
+ };
3795
+ if (!turnOrder.includes(turn.turnId)) {
3796
+ turnOrder.push(turn.turnId);
3797
+ }
3798
+ }
3799
+ break;
3800
+ }
3801
+ case "insert_block": {
3802
+ const turn = turns[op.turnId];
3803
+ if (!turn) break;
3804
+ const newBlocks = { ...turn.blocks, [op.block.blockId]: op.block };
3805
+ const newBlockOrder = [...turn.blockOrder];
3806
+ if (op.afterBlockId) {
3807
+ const idx = newBlockOrder.indexOf(op.afterBlockId);
3808
+ if (idx >= 0) {
3809
+ newBlockOrder.splice(idx + 1, 0, op.block.blockId);
3810
+ } else {
3811
+ newBlockOrder.push(op.block.blockId);
3812
+ }
3813
+ } else {
3814
+ newBlockOrder.push(op.block.blockId);
3815
+ }
3816
+ turns[op.turnId] = {
3817
+ ...turn,
3818
+ blocks: newBlocks,
3819
+ blockOrder: newBlockOrder,
3820
+ updatedAt: op.block.updatedAt
3821
+ };
3822
+ break;
3823
+ }
3824
+ case "replace_block": {
3825
+ const turn = turns[op.turnId];
3826
+ if (!turn || !turn.blocks[op.block.blockId]) break;
3827
+ turns[op.turnId] = {
3828
+ ...turn,
3829
+ blocks: { ...turn.blocks, [op.block.blockId]: op.block },
3830
+ updatedAt: op.block.updatedAt
3831
+ };
3832
+ break;
3833
+ }
3834
+ case "append_text": {
3835
+ const block = findBlock(turns, op.turnId, op.blockId);
3836
+ if (!block) break;
3837
+ if (block.kind === "text" || block.kind === "reasoning") {
3838
+ const updated = {
3839
+ ...block,
3840
+ text: block.text + op.text,
3841
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3842
+ };
3843
+ setBlock(turns, op.turnId, op.blockId, updated);
3844
+ }
3845
+ break;
3846
+ }
3847
+ case "append_tool_output": {
3848
+ const block = findBlock(turns, op.turnId, op.blockId);
3849
+ if (!block || block.kind !== "tool") {
3850
+ const found = findBlockAcrossTurns(turns, op.blockId);
3851
+ if (found && found.block.kind === "tool") {
3852
+ const updated2 = {
3853
+ ...found.block,
3854
+ outputText: (found.block.outputText ?? "") + op.text,
3855
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3856
+ };
3857
+ setBlock(turns, found.turnId, op.blockId, updated2);
3858
+ }
3859
+ break;
3860
+ }
3861
+ const updated = {
3862
+ ...block,
3863
+ outputText: (block.outputText ?? "") + op.text,
3864
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3865
+ };
3866
+ setBlock(turns, op.turnId, op.blockId, updated);
3867
+ break;
3868
+ }
3869
+ case "set_tool_state": {
3870
+ let block = findBlock(turns, op.turnId, op.blockId);
3871
+ let resolvedTurnId = op.turnId;
3872
+ if (!block || block.kind !== "tool") {
3873
+ const found = findBlockAcrossTurns(turns, op.blockId);
3874
+ if (found && found.block.kind === "tool") {
3875
+ block = found.block;
3876
+ resolvedTurnId = found.turnId;
3877
+ } else {
3878
+ break;
3879
+ }
3880
+ }
3881
+ const updated = {
3882
+ ...block,
3883
+ state: op.state,
3884
+ ...op.exitCode !== void 0 && { exitCode: op.exitCode },
3885
+ ...op.errorText !== void 0 && { errorText: op.errorText },
3886
+ ...op.isComplete !== void 0 && { isComplete: op.isComplete },
3887
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3888
+ };
3889
+ setBlock(turns, resolvedTurnId, op.blockId, updated);
3890
+ break;
3891
+ }
3892
+ case "complete_block": {
3893
+ let block = findBlock(turns, op.turnId, op.blockId);
3894
+ let resolvedTurnId = op.turnId;
3895
+ if (!block) {
3896
+ const found = findBlockAcrossTurns(turns, op.blockId);
3897
+ if (found) {
3898
+ block = found.block;
3899
+ resolvedTurnId = found.turnId;
3900
+ } else {
3901
+ break;
3902
+ }
3903
+ }
3904
+ const updated = {
3905
+ ...block,
3906
+ isComplete: true,
3907
+ updatedAt: op.updatedAt
3908
+ };
3909
+ setBlock(turns, resolvedTurnId, op.blockId, updated);
3910
+ break;
3911
+ }
3912
+ case "complete_turn": {
3913
+ const turn = turns[op.turnId];
3914
+ if (!turn) break;
3915
+ turns[op.turnId] = {
3916
+ ...turn,
3917
+ isComplete: true,
3918
+ endedAt: op.endedAt,
3919
+ updatedAt: op.updatedAt
3920
+ };
3921
+ break;
3922
+ }
3923
+ }
3924
+ }
3925
+ return {
3926
+ ...state,
3927
+ turnOrder,
3928
+ turns
3929
+ };
3930
+ }
3931
+ function findBlock(turns, turnId, blockId) {
3932
+ return turns[turnId]?.blocks[blockId];
3933
+ }
3934
+ function findBlockAcrossTurns(turns, blockId) {
3935
+ for (const [turnId, turn] of Object.entries(turns)) {
3936
+ const block = turn.blocks[blockId];
3937
+ if (block) return { turnId, block };
3938
+ }
3939
+ return void 0;
3940
+ }
3941
+ function setBlock(turns, turnId, blockId, block) {
3942
+ const turn = turns[turnId];
3943
+ if (!turn) return;
3944
+ turns[turnId] = {
3945
+ ...turn,
3946
+ blocks: { ...turn.blocks, [blockId]: block },
3947
+ updatedAt: block.updatedAt
3948
+ };
3949
+ }
3950
+ var MAX_SNAPSHOT_TURNS = 200;
3951
+ function trimSnapshot(snapshot, maxTurns = MAX_SNAPSHOT_TURNS) {
3952
+ if (snapshot.turnOrder.length <= maxTurns) return snapshot;
3953
+ const keptOrder = snapshot.turnOrder.slice(-maxTurns);
3954
+ const keptTurns = {};
3955
+ for (const id of keptOrder) {
3956
+ if (snapshot.turns[id]) {
3957
+ keptTurns[id] = snapshot.turns[id];
3958
+ }
3959
+ }
3960
+ return {
3961
+ ...snapshot,
3962
+ turnOrder: keptOrder,
3963
+ turns: keptTurns
3964
+ };
3965
+ }
3966
+
3967
+ // src/transcript-watcher.ts
3058
3968
  var ClaudeCodeParser = class {
3059
3969
  name = "claude-code";
3060
3970
  /** Maps tool_use id → tool name for matching results back to tools. */
@@ -3185,10 +4095,10 @@ function discoverViaProc(panePid) {
3185
4095
  for (const childPid of children) {
3186
4096
  const fdDir = `/proc/${childPid}/fd`;
3187
4097
  try {
3188
- const fds = fs3.readdirSync(fdDir);
4098
+ const fds = fs4.readdirSync(fdDir);
3189
4099
  for (const fd of fds) {
3190
4100
  try {
3191
- const target = fs3.readlinkSync(path3.join(fdDir, fd));
4101
+ const target = fs4.readlinkSync(path3.join(fdDir, fd));
3192
4102
  if (target.endsWith(".jsonl") && target.includes("/.claude/")) {
3193
4103
  return target;
3194
4104
  }
@@ -3205,10 +4115,10 @@ function discoverViaProc(panePid) {
3205
4115
  for (const gcPid of grandchildren) {
3206
4116
  const gcFdDir = `/proc/${gcPid}/fd`;
3207
4117
  try {
3208
- const fds = fs3.readdirSync(gcFdDir);
4118
+ const fds = fs4.readdirSync(gcFdDir);
3209
4119
  for (const fd of fds) {
3210
4120
  try {
3211
- const target = fs3.readlinkSync(path3.join(gcFdDir, fd));
4121
+ const target = fs4.readlinkSync(path3.join(gcFdDir, fd));
3212
4122
  if (target.endsWith(".jsonl") && target.includes("/.claude/")) {
3213
4123
  return target;
3214
4124
  }
@@ -3227,15 +4137,15 @@ function discoverViaProc(panePid) {
3227
4137
  }
3228
4138
  function discoverViaCwd(paneCwd) {
3229
4139
  const claudeProjectsDir = path3.join(os7.homedir(), ".claude", "projects");
3230
- if (!fs3.existsSync(claudeProjectsDir)) return null;
3231
- const resolvedCwd = fs3.realpathSync(paneCwd);
4140
+ if (!fs4.existsSync(claudeProjectsDir)) return null;
4141
+ const resolvedCwd = fs4.realpathSync(paneCwd);
3232
4142
  const expectedDirName = resolvedCwd.replace(/\//g, "-");
3233
4143
  const projectDir = path3.join(claudeProjectsDir, expectedDirName);
3234
- if (!fs3.existsSync(projectDir)) return null;
4144
+ if (!fs4.existsSync(projectDir)) return null;
3235
4145
  try {
3236
- const jsonlFiles = fs3.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => {
4146
+ const jsonlFiles = fs4.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => {
3237
4147
  const fullPath = path3.join(projectDir, f);
3238
- const stat = fs3.statSync(fullPath);
4148
+ const stat = fs4.statSync(fullPath);
3239
4149
  return { path: fullPath, mtime: stat.mtimeMs };
3240
4150
  }).sort((a, b) => b.mtime - a.mtime);
3241
4151
  return jsonlFiles.length > 0 ? jsonlFiles[0].path : null;
@@ -3245,14 +4155,14 @@ function discoverViaCwd(paneCwd) {
3245
4155
  }
3246
4156
  function discoverCodexTranscript() {
3247
4157
  const codexSessionsDir = path3.join(os7.homedir(), ".codex", "sessions");
3248
- if (!fs3.existsSync(codexSessionsDir)) return null;
4158
+ if (!fs4.existsSync(codexSessionsDir)) return null;
3249
4159
  try {
3250
- const dateDirs = fs3.readdirSync(codexSessionsDir).filter((d) => /^\d{4}-\d{2}-\d{2}/.test(d)).sort().reverse();
4160
+ const dateDirs = fs4.readdirSync(codexSessionsDir).filter((d) => /^\d{4}-\d{2}-\d{2}/.test(d)).sort().reverse();
3251
4161
  for (const dateDir of dateDirs.slice(0, 3)) {
3252
4162
  const fullDir = path3.join(codexSessionsDir, dateDir);
3253
- const jsonlFiles = fs3.readdirSync(fullDir).filter((f) => f.startsWith("rollout-") && f.endsWith(".jsonl")).map((f) => {
4163
+ const jsonlFiles = fs4.readdirSync(fullDir).filter((f) => f.startsWith("rollout-") && f.endsWith(".jsonl")).map((f) => {
3254
4164
  const fp = path3.join(fullDir, f);
3255
- const stat = fs3.statSync(fp);
4165
+ const stat = fs4.statSync(fp);
3256
4166
  return { path: fp, mtime: stat.mtimeMs };
3257
4167
  }).sort((a, b) => b.mtime - a.mtime);
3258
4168
  if (jsonlFiles.length > 0) return jsonlFiles[0].path;
@@ -3303,25 +4213,30 @@ var REDISCOVERY_INTERVAL_MS = 5e3;
3303
4213
  var TranscriptWatcher = class {
3304
4214
  filePath;
3305
4215
  parser;
4216
+ parserV2;
3306
4217
  callbacks;
3307
4218
  offset = 0;
3308
4219
  inode = 0;
3309
4220
  partialLine = "";
3310
4221
  seq = 0;
3311
4222
  turns = [];
4223
+ /** V2 normalized snapshot state. */
4224
+ snapshot;
3312
4225
  watcher = null;
3313
4226
  pollTimer = null;
3314
4227
  stopped = false;
3315
4228
  subscriberCount = 0;
3316
- constructor(filePath, parser, callbacks) {
4229
+ constructor(filePath, parser, callbacks, parserV2, sessionId) {
3317
4230
  this.filePath = filePath;
3318
4231
  this.parser = parser;
4232
+ this.parserV2 = parserV2;
3319
4233
  this.callbacks = callbacks;
4234
+ this.snapshot = createEmptySnapshot(sessionId ?? "");
3320
4235
  }
3321
4236
  /** Start watching the file. Returns false if file doesn't exist. */
3322
4237
  start() {
3323
4238
  try {
3324
- const stat = fs3.statSync(this.filePath);
4239
+ const stat = fs4.statSync(this.filePath);
3325
4240
  this.inode = stat.ino;
3326
4241
  this.offset = stat.size;
3327
4242
  } catch {
@@ -3368,6 +4283,10 @@ var TranscriptWatcher = class {
3368
4283
  get currentSeq() {
3369
4284
  return this.seq;
3370
4285
  }
4286
+ /** Get the V2 normalized snapshot. */
4287
+ getSnapshot() {
4288
+ return trimSnapshot(this.snapshot);
4289
+ }
3371
4290
  /** Read and replay the full transcript from the start of the file. */
3372
4291
  replayFromStart() {
3373
4292
  const savedOffset = this.offset;
@@ -3380,7 +4299,7 @@ var TranscriptWatcher = class {
3380
4299
  }
3381
4300
  startWatching() {
3382
4301
  try {
3383
- this.watcher = fs3.watch(this.filePath, () => {
4302
+ this.watcher = fs4.watch(this.filePath, () => {
3384
4303
  if (!this.stopped) this.readNewData();
3385
4304
  });
3386
4305
  this.watcher.on("error", () => {
@@ -3398,7 +4317,7 @@ var TranscriptWatcher = class {
3398
4317
  }
3399
4318
  checkForRotation() {
3400
4319
  try {
3401
- const stat = fs3.statSync(this.filePath);
4320
+ const stat = fs4.statSync(this.filePath);
3402
4321
  if (stat.ino !== this.inode) {
3403
4322
  this.inode = stat.ino;
3404
4323
  this.offset = 0;
@@ -3413,22 +4332,22 @@ var TranscriptWatcher = class {
3413
4332
  readNewData() {
3414
4333
  let fd;
3415
4334
  try {
3416
- fd = fs3.openSync(this.filePath, "r");
4335
+ fd = fs4.openSync(this.filePath, "r");
3417
4336
  } catch {
3418
4337
  return;
3419
4338
  }
3420
4339
  try {
3421
- const stat = fs3.fstatSync(fd);
4340
+ const stat = fs4.fstatSync(fd);
3422
4341
  if (stat.size <= this.offset) return;
3423
4342
  const readSize = Math.min(stat.size - this.offset, 256 * 1024);
3424
4343
  const buffer = Buffer.alloc(readSize);
3425
- const bytesRead = fs3.readSync(fd, buffer, 0, readSize, this.offset);
4344
+ const bytesRead = fs4.readSync(fd, buffer, 0, readSize, this.offset);
3426
4345
  if (bytesRead === 0) return;
3427
4346
  this.offset += bytesRead;
3428
4347
  const chunk = buffer.subarray(0, bytesRead).toString("utf-8");
3429
4348
  this.processChunk(chunk);
3430
4349
  } finally {
3431
- fs3.closeSync(fd);
4350
+ fs4.closeSync(fd);
3432
4351
  }
3433
4352
  }
3434
4353
  processChunk(chunk) {
@@ -3442,9 +4361,20 @@ var TranscriptWatcher = class {
3442
4361
  for (const line of lines) {
3443
4362
  const trimmed = line.trim();
3444
4363
  if (!trimmed) continue;
4364
+ if (this.parserV2 && this.callbacks.onOps) {
4365
+ const ops = this.parserV2.parseLine(trimmed);
4366
+ if (ops.length > 0) {
4367
+ this.seq++;
4368
+ this.snapshot = applyOps(this.snapshot, ops);
4369
+ this.snapshot = trimSnapshot(this.snapshot);
4370
+ this.callbacks.onOps(ops, this.seq);
4371
+ }
4372
+ }
3445
4373
  const turn = this.parser.parseLine(trimmed);
3446
4374
  if (turn) {
3447
- this.seq++;
4375
+ if (!this.parserV2 || !this.callbacks.onOps) {
4376
+ this.seq++;
4377
+ }
3448
4378
  this.turns.push({
3449
4379
  turnId: turn.turnId,
3450
4380
  role: turn.role,
@@ -3460,22 +4390,27 @@ var TranscriptWatcher = class {
3460
4390
  }
3461
4391
  }
3462
4392
  };
3463
- var PARSERS = {
3464
- "Claude Code": new ClaudeCodeParser(),
3465
- "Codex CLI": new CodexCliParser()
4393
+ var PARSER_FACTORIES = {
4394
+ "Claude Code": () => new ClaudeCodeParser(),
4395
+ "Codex CLI": () => new CodexCliParser()
3466
4396
  };
3467
4397
  function getParser(agentType) {
3468
4398
  if (!agentType) return void 0;
3469
- return PARSERS[agentType];
4399
+ const factory = PARSER_FACTORIES[agentType];
4400
+ return factory ? factory() : void 0;
3470
4401
  }
3471
4402
  var TranscriptWatcherManager = class {
3472
4403
  active = /* @__PURE__ */ new Map();
3473
4404
  rediscoveryTimers = /* @__PURE__ */ new Map();
3474
4405
  sendFn;
3475
4406
  sendSnapshotFn;
3476
- constructor(sendFn, sendSnapshotFn) {
4407
+ sendOpsFn;
4408
+ sendV2SnapshotFn;
4409
+ constructor(sendFn, sendSnapshotFn, sendOpsFn, sendV2SnapshotFn) {
3477
4410
  this.sendFn = sendFn;
3478
4411
  this.sendSnapshotFn = sendSnapshotFn;
4412
+ this.sendOpsFn = sendOpsFn;
4413
+ this.sendV2SnapshotFn = sendV2SnapshotFn;
3479
4414
  }
3480
4415
  /** Enable markdown streaming for a session. */
3481
4416
  enableMarkdown(sessionId, agentType) {
@@ -3495,14 +4430,24 @@ var TranscriptWatcherManager = class {
3495
4430
  return false;
3496
4431
  }
3497
4432
  console.log(`[rAgent] Watching transcript: ${filePath} (${parser.name})`);
3498
- const watcher = new TranscriptWatcher(filePath, parser, {
3499
- onTurn: (turn, seq) => {
3500
- this.sendFn(sessionId, turn, seq);
4433
+ const v2Parser = getParserV2(agentType);
4434
+ const watcher = new TranscriptWatcher(
4435
+ filePath,
4436
+ parser,
4437
+ {
4438
+ onTurn: (turn, seq) => {
4439
+ this.sendFn(sessionId, turn, seq);
4440
+ },
4441
+ onOps: this.sendOpsFn ? (ops, seq) => {
4442
+ this.sendOpsFn(sessionId, ops, seq);
4443
+ } : void 0,
4444
+ onError: (error) => {
4445
+ console.warn(`[rAgent] Transcript watcher error (${sessionId}): ${error}`);
4446
+ }
3501
4447
  },
3502
- onError: (error) => {
3503
- console.warn(`[rAgent] Transcript watcher error (${sessionId}): ${error}`);
3504
- }
3505
- });
4448
+ v2Parser,
4449
+ sessionId
4450
+ );
3506
4451
  if (!watcher.start()) {
3507
4452
  console.warn(`[rAgent] Failed to start watching ${filePath}`);
3508
4453
  return false;
@@ -3536,14 +4481,24 @@ var TranscriptWatcherManager = class {
3536
4481
  session.watcher.stop();
3537
4482
  const parser = getParser(agentType);
3538
4483
  if (!parser) return;
3539
- const newWatcher = new TranscriptWatcher(newPath, parser, {
3540
- onTurn: (turn, seq) => {
3541
- this.sendFn(sessionId, turn, seq);
4484
+ const newV2Parser = getParserV2(agentType);
4485
+ const newWatcher = new TranscriptWatcher(
4486
+ newPath,
4487
+ parser,
4488
+ {
4489
+ onTurn: (turn, seq) => {
4490
+ this.sendFn(sessionId, turn, seq);
4491
+ },
4492
+ onOps: this.sendOpsFn ? (ops, seq) => {
4493
+ this.sendOpsFn(sessionId, ops, seq);
4494
+ } : void 0,
4495
+ onError: (error) => {
4496
+ console.warn(`[rAgent] Transcript watcher error (${sessionId}): ${error}`);
4497
+ }
3542
4498
  },
3543
- onError: (error) => {
3544
- console.warn(`[rAgent] Transcript watcher error (${sessionId}): ${error}`);
3545
- }
3546
- });
4499
+ newV2Parser,
4500
+ sessionId
4501
+ );
3547
4502
  if (!newWatcher.start()) {
3548
4503
  console.warn(`[rAgent] Failed to start watching new transcript ${newPath}`);
3549
4504
  return;
@@ -3562,12 +4517,16 @@ var TranscriptWatcherManager = class {
3562
4517
  this.rediscoveryTimers.delete(sessionId);
3563
4518
  }
3564
4519
  }
3565
- /** Handle sync-markdown request — send replay snapshot. */
4520
+ /** Handle sync-markdown request — send replay snapshot (V1 + V2). */
3566
4521
  handleSyncRequest(sessionId, fromSeq) {
3567
4522
  const session = this.active.get(sessionId);
3568
4523
  if (!session) return;
3569
4524
  const turns = session.watcher.getReplayTurns(fromSeq);
3570
4525
  this.sendSnapshotFn(sessionId, turns, session.watcher.currentSeq);
4526
+ if (this.sendV2SnapshotFn) {
4527
+ const snapshot = session.watcher.getSnapshot();
4528
+ this.sendV2SnapshotFn(sessionId, snapshot, session.watcher.currentSeq);
4529
+ }
3571
4530
  }
3572
4531
  /** Stop all watchers. */
3573
4532
  stopAll() {
@@ -3591,7 +4550,7 @@ function pidFilePath(hostId) {
3591
4550
  }
3592
4551
  function readPidFile(filePath) {
3593
4552
  try {
3594
- const raw = fs4.readFileSync(filePath, "utf8").trim();
4553
+ const raw = fs5.readFileSync(filePath, "utf8").trim();
3595
4554
  const pid = Number.parseInt(raw, 10);
3596
4555
  return Number.isInteger(pid) && pid > 0 ? pid : null;
3597
4556
  } catch {
@@ -3616,7 +4575,7 @@ function acquirePidLock(hostId) {
3616
4575
  Stop it first with: kill ${existingPid} \u2014 or: ragent service stop`
3617
4576
  );
3618
4577
  }
3619
- fs4.writeFileSync(lockPath, `${process.pid}
4578
+ fs5.writeFileSync(lockPath, `${process.pid}
3620
4579
  `, "utf8");
3621
4580
  return lockPath;
3622
4581
  }
@@ -3624,7 +4583,7 @@ function releasePidLock(lockPath) {
3624
4583
  try {
3625
4584
  const currentPid = readPidFile(lockPath);
3626
4585
  if (currentPid === process.pid) {
3627
- fs4.unlinkSync(lockPath);
4586
+ fs5.unlinkSync(lockPath);
3628
4587
  }
3629
4588
  } catch {
3630
4589
  }
@@ -3730,6 +4689,44 @@ async function runAgent(rawOptions) {
3730
4689
  } else {
3731
4690
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
3732
4691
  }
4692
+ },
4693
+ // V2: send block-level delta ops
4694
+ (sessionId, ops, seq) => {
4695
+ if (!conn.isReady()) return;
4696
+ const payload = {
4697
+ type: "markdown",
4698
+ subtype: "delta",
4699
+ schemaVersion: 2,
4700
+ sessionId,
4701
+ seq,
4702
+ ops
4703
+ };
4704
+ if (conn.sessionKey) {
4705
+ const contentStr = JSON.stringify(payload);
4706
+ const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
4707
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
4708
+ } else {
4709
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
4710
+ }
4711
+ },
4712
+ // V2: send full snapshot
4713
+ (sessionId, snapshot, seq) => {
4714
+ if (!conn.isReady()) return;
4715
+ const payload = {
4716
+ type: "markdown",
4717
+ subtype: "snapshot",
4718
+ schemaVersion: 2,
4719
+ sessionId,
4720
+ seq,
4721
+ snapshot
4722
+ };
4723
+ if (conn.sessionKey) {
4724
+ const contentStr = JSON.stringify(payload);
4725
+ const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
4726
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
4727
+ } else {
4728
+ sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
4729
+ }
3733
4730
  }
3734
4731
  );
3735
4732
  const shell = new ShellManager(options.command, sendOutput);
@@ -3766,6 +4763,24 @@ async function runAgent(rawOptions) {
3766
4763
  await new Promise((resolve) => {
3767
4764
  const ws = new import_ws5.default(negotiated.url, "json.webpubsub.azure.v1");
3768
4765
  conn.setConnection(ws, groups, negotiated.sessionKey ?? null);
4766
+ if (negotiated.sessionKey) {
4767
+ const enforcer = new ApprovalEnforcer({
4768
+ secret: negotiated.sessionKey,
4769
+ sendRequest: (request) => {
4770
+ if (conn.isReady()) {
4771
+ sendToGroup(ws, groups.privateGroup, {
4772
+ type: "approval",
4773
+ subtype: "request",
4774
+ sessionId: request.sessionId,
4775
+ request
4776
+ });
4777
+ }
4778
+ }
4779
+ });
4780
+ dispatcher.setApprovalEnforcer(enforcer);
4781
+ } else {
4782
+ dispatcher.setApprovalEnforcer(null);
4783
+ }
3769
4784
  ws.on("open", async () => {
3770
4785
  console.log("[rAgent] Connector connected to relay.");
3771
4786
  conn.resetReconnectDelay();
@@ -3833,6 +4848,8 @@ async function runAgent(rawOptions) {
3833
4848
  await dispatcher.handleControlAction({ ...payload, action: "start-agent" });
3834
4849
  } else if (payload.type === "provision") {
3835
4850
  await dispatcher.handleControlAction({ ...payload, action: "provision" });
4851
+ } else if (payload.type === "approval") {
4852
+ dispatcher.handleApprovalMessage(payload);
3836
4853
  }
3837
4854
  }
3838
4855
  if (msg.type === "message" && msg.group === groups.registryGroup) {
@@ -4257,7 +5274,7 @@ function registerServiceCommand(program2) {
4257
5274
  }
4258
5275
 
4259
5276
  // src/commands/uninstall.ts
4260
- var fs5 = __toESM(require("fs"));
5277
+ var fs6 = __toESM(require("fs"));
4261
5278
  async function uninstallAgent(opts) {
4262
5279
  const config = loadConfig();
4263
5280
  const hostName = config.hostName || config.hostId || "this machine";
@@ -4288,8 +5305,8 @@ async function uninstallAgent(opts) {
4288
5305
  }
4289
5306
  console.log("[rAgent] Stopping and removing service...");
4290
5307
  await uninstallService().catch(() => void 0);
4291
- if (fs5.existsSync(CONFIG_DIR)) {
4292
- fs5.rmSync(CONFIG_DIR, { recursive: true, force: true });
5308
+ if (fs6.existsSync(CONFIG_DIR)) {
5309
+ fs6.rmSync(CONFIG_DIR, { recursive: true, force: true });
4293
5310
  console.log(`[rAgent] Removed config directory: ${CONFIG_DIR}`);
4294
5311
  }
4295
5312
  try {
@@ -4312,6 +5329,58 @@ function registerUninstallCommand(parent) {
4312
5329
  });
4313
5330
  }
4314
5331
 
5332
+ // src/commands/discover.ts
5333
+ async function discoverSessions() {
5334
+ const [tmux, screen, zellij] = await Promise.all([
5335
+ collectTmuxSessions(),
5336
+ collectScreenSessions(),
5337
+ collectZellijSessions()
5338
+ ]);
5339
+ const multiplexerPids = /* @__PURE__ */ new Set();
5340
+ for (const s of [...tmux, ...screen, ...zellij]) {
5341
+ if (s.pids) s.pids.forEach((p) => multiplexerPids.add(p));
5342
+ }
5343
+ const bare = await collectBareAgentProcesses(multiplexerPids);
5344
+ return [...tmux, ...screen, ...zellij, ...bare];
5345
+ }
5346
+ function formatSessionTable(sessions) {
5347
+ if (sessions.length === 0) return "No agent sessions discovered.";
5348
+ const headers = ["TYPE", "NAME", "AGENT", "CONF", "ENV", "COMMAND", "PID", "WORKING DIR"];
5349
+ const rows = sessions.map((s) => [
5350
+ s.type,
5351
+ s.name,
5352
+ s.agentType || "-",
5353
+ s.agentConfidence !== void 0 ? s.agentConfidence.toFixed(1) : "-",
5354
+ s.environment || "-",
5355
+ truncate(s.command || "-", 40),
5356
+ s.pids?.join(",") || "-",
5357
+ truncate(s.workingDir || "-", 30)
5358
+ ]);
5359
+ const widths = headers.map(
5360
+ (h, i) => Math.max(h.length, ...rows.map((r) => r[i].length))
5361
+ );
5362
+ const headerLine = headers.map((h, i) => h.padEnd(widths[i])).join(" ");
5363
+ const separator = widths.map((w) => "-".repeat(w)).join(" ");
5364
+ const dataLines = rows.map(
5365
+ (row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ")
5366
+ );
5367
+ return [headerLine, separator, ...dataLines].join("\n");
5368
+ }
5369
+ function truncate(s, max) {
5370
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
5371
+ }
5372
+ function registerDiscoverCommand(program2) {
5373
+ program2.command("discover").description("Scan for running AI agent sessions (one-shot)").option("--json", "Output as JSON").action(async (opts) => {
5374
+ const sessions = await discoverSessions();
5375
+ if (opts.json) {
5376
+ console.log(JSON.stringify(sessions, null, 2));
5377
+ } else {
5378
+ console.log(formatSessionTable(sessions));
5379
+ }
5380
+ process.exitCode = sessions.length > 0 ? 0 : 1;
5381
+ });
5382
+ }
5383
+
4315
5384
  // src/index.ts
4316
5385
  process.on("unhandledRejection", (reason) => {
4317
5386
  const message = reason instanceof Error ? reason.stack || reason.message : String(reason);
@@ -4325,6 +5394,7 @@ registerUpdateCommand(import_commander.program);
4325
5394
  registerSessionsCommand(import_commander.program);
4326
5395
  registerServiceCommand(import_commander.program);
4327
5396
  registerUninstallCommand(import_commander.program);
5397
+ registerDiscoverCommand(import_commander.program);
4328
5398
  import_commander.program.hook("preAction", async (_thisCommand, actionCommand) => {
4329
5399
  if (actionCommand.name() === "update") return;
4330
5400
  await maybeWarnUpdate();
@@ -4338,7 +5408,7 @@ function showStatus() {
4338
5408
  ragent v${CURRENT_VERSION}
4339
5409
  `);
4340
5410
  try {
4341
- const raw = fs6.readFileSync(CONFIG_FILE, "utf8");
5411
+ const raw = fs7.readFileSync(CONFIG_FILE, "utf8");
4342
5412
  const config = JSON.parse(raw);
4343
5413
  if (config.portal && config.agentToken) {
4344
5414
  console.log(` Status: Connected`);