ragent-cli 1.7.3 → 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.7.3",
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,9 +90,10 @@ 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
- "@types/node": "^25.3.0",
96
+ "@types/node": "^20.17.0",
93
97
  "@types/ws": "^8.5.13",
94
98
  eslint: "^10.0.2",
95
99
  tsup: "^8.4.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];
@@ -950,7 +1063,7 @@ var SECRET_PATTERNS = [
950
1063
  /eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g
951
1064
  // JWT
952
1065
  ];
953
- var redactionEnabled = false;
1066
+ var redactionEnabled = true;
954
1067
  function setRedactionEnabled(enabled) {
955
1068
  redactionEnabled = enabled;
956
1069
  }
@@ -1045,6 +1158,7 @@ var import_node_os = require("os");
1045
1158
  var import_node_string_decoder = require("string_decoder");
1046
1159
  var pty = __toESM(require("node-pty"));
1047
1160
  var STOP_DEBOUNCE_MS = 2e3;
1161
+ var MAX_CONCURRENT_STREAMS = 20;
1048
1162
  var MAX_SESSION_BUFFER_BYTES = 64 * 1024;
1049
1163
  function parsePaneTarget(sessionId) {
1050
1164
  if (!sessionId.startsWith("tmux:")) return null;
@@ -1151,6 +1265,12 @@ var SessionStreamer = class {
1151
1265
  if (this.active.has(sessionId)) {
1152
1266
  return true;
1153
1267
  }
1268
+ if (this.active.size >= MAX_CONCURRENT_STREAMS) {
1269
+ console.warn(
1270
+ `[rAgent] Stream limit reached (${MAX_CONCURRENT_STREAMS}). Rejecting: ${sessionId}`
1271
+ );
1272
+ return false;
1273
+ }
1154
1274
  if (sessionId.startsWith("tmux:")) {
1155
1275
  return this.startTmuxStream(sessionId);
1156
1276
  }
@@ -2140,7 +2260,7 @@ var import_ws4 = __toESM(require("ws"));
2140
2260
 
2141
2261
  // src/service.ts
2142
2262
  var import_child_process2 = require("child_process");
2143
- var fs2 = __toESM(require("fs"));
2263
+ var fs3 = __toESM(require("fs"));
2144
2264
  var os6 = __toESM(require("os"));
2145
2265
  var path2 = __toESM(require("path"));
2146
2266
  function assertConfiguredAgentToken() {
@@ -2154,8 +2274,8 @@ function getConfiguredServiceBackend() {
2154
2274
  if (config.serviceBackend === "systemd" || config.serviceBackend === "pidfile") {
2155
2275
  return config.serviceBackend;
2156
2276
  }
2157
- if (fs2.existsSync(SERVICE_FILE)) return "systemd";
2158
- 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";
2159
2279
  return null;
2160
2280
  }
2161
2281
  async function canUseSystemdUser() {
@@ -2198,8 +2318,8 @@ WantedBy=default.target
2198
2318
  }
2199
2319
  async function installSystemdService(opts = {}) {
2200
2320
  assertConfiguredAgentToken();
2201
- fs2.mkdirSync(SERVICE_DIR, { recursive: true });
2202
- fs2.writeFileSync(SERVICE_FILE, buildSystemdUnit(), "utf8");
2321
+ fs3.mkdirSync(SERVICE_DIR, { recursive: true });
2322
+ fs3.writeFileSync(SERVICE_FILE, buildSystemdUnit(), "utf8");
2203
2323
  await runSystemctlUser(["daemon-reload"]);
2204
2324
  if (opts.enable !== false) {
2205
2325
  await runSystemctlUser(["enable", SERVICE_NAME]);
@@ -2212,7 +2332,7 @@ async function installSystemdService(opts = {}) {
2212
2332
  }
2213
2333
  function readFallbackPid() {
2214
2334
  try {
2215
- const raw = fs2.readFileSync(FALLBACK_PID_FILE, "utf8").trim();
2335
+ const raw = fs3.readFileSync(FALLBACK_PID_FILE, "utf8").trim();
2216
2336
  const pid = Number.parseInt(raw, 10);
2217
2337
  return Number.isInteger(pid) ? pid : null;
2218
2338
  } catch {
@@ -2231,14 +2351,14 @@ function isProcessRunning(pid) {
2231
2351
  var LOG_ROTATION_MAX_BYTES = 10 * 1024 * 1024;
2232
2352
  function rotateLogIfNeeded() {
2233
2353
  try {
2234
- const stat = fs2.statSync(FALLBACK_LOG_FILE);
2354
+ const stat = fs3.statSync(FALLBACK_LOG_FILE);
2235
2355
  if (stat.size > LOG_ROTATION_MAX_BYTES) {
2236
2356
  const rotated = `${FALLBACK_LOG_FILE}.1`;
2237
2357
  try {
2238
- fs2.unlinkSync(rotated);
2358
+ fs3.unlinkSync(rotated);
2239
2359
  } catch {
2240
2360
  }
2241
- fs2.renameSync(FALLBACK_LOG_FILE, rotated);
2361
+ fs3.renameSync(FALLBACK_LOG_FILE, rotated);
2242
2362
  }
2243
2363
  } catch {
2244
2364
  }
@@ -2252,7 +2372,7 @@ async function startPidfileService() {
2252
2372
  }
2253
2373
  ensureConfigDir();
2254
2374
  rotateLogIfNeeded();
2255
- const logFd = fs2.openSync(FALLBACK_LOG_FILE, "a");
2375
+ const logFd = fs3.openSync(FALLBACK_LOG_FILE, "a");
2256
2376
  const child = (0, import_child_process2.spawn)(process.execPath, [__filename, "run"], {
2257
2377
  detached: true,
2258
2378
  stdio: ["ignore", logFd, logFd],
@@ -2260,8 +2380,8 @@ async function startPidfileService() {
2260
2380
  env: process.env
2261
2381
  });
2262
2382
  child.unref();
2263
- fs2.closeSync(logFd);
2264
- fs2.writeFileSync(FALLBACK_PID_FILE, `${child.pid}
2383
+ fs3.closeSync(logFd);
2384
+ fs3.writeFileSync(FALLBACK_PID_FILE, `${child.pid}
2265
2385
  `, "utf8");
2266
2386
  saveConfigPatch({ serviceBackend: "pidfile" });
2267
2387
  console.log(`[rAgent] Started fallback background service (pid ${child.pid})`);
@@ -2271,7 +2391,7 @@ async function stopPidfileService() {
2271
2391
  const pid = readFallbackPid();
2272
2392
  if (!pid || !isProcessRunning(pid)) {
2273
2393
  try {
2274
- fs2.unlinkSync(FALLBACK_PID_FILE);
2394
+ fs3.unlinkSync(FALLBACK_PID_FILE);
2275
2395
  } catch {
2276
2396
  }
2277
2397
  console.log("[rAgent] Service is not running.");
@@ -2286,7 +2406,7 @@ async function stopPidfileService() {
2286
2406
  process.kill(pid, "SIGKILL");
2287
2407
  }
2288
2408
  try {
2289
- fs2.unlinkSync(FALLBACK_PID_FILE);
2409
+ fs3.unlinkSync(FALLBACK_PID_FILE);
2290
2410
  } catch {
2291
2411
  }
2292
2412
  console.log(`[rAgent] Stopped fallback background service (pid ${pid})`);
@@ -2331,12 +2451,12 @@ async function stopService() {
2331
2451
  }
2332
2452
  function killStaleAgentProcesses() {
2333
2453
  try {
2334
- const entries = fs2.readdirSync(CONFIG_DIR);
2454
+ const entries = fs3.readdirSync(CONFIG_DIR);
2335
2455
  for (const entry of entries) {
2336
2456
  if (!entry.startsWith("agent-") || !entry.endsWith(".pid")) continue;
2337
2457
  const pidPath = path2.join(CONFIG_DIR, entry);
2338
2458
  try {
2339
- const raw = fs2.readFileSync(pidPath, "utf8").trim();
2459
+ const raw = fs3.readFileSync(pidPath, "utf8").trim();
2340
2460
  const pid = Number.parseInt(raw, 10);
2341
2461
  if (!Number.isInteger(pid) || pid <= 0) continue;
2342
2462
  try {
@@ -2345,7 +2465,7 @@ function killStaleAgentProcesses() {
2345
2465
  console.log(`[rAgent] Stopped stale agent process (pid ${pid})`);
2346
2466
  } catch {
2347
2467
  }
2348
- fs2.unlinkSync(pidPath);
2468
+ fs3.unlinkSync(pidPath);
2349
2469
  } catch {
2350
2470
  }
2351
2471
  }
@@ -2408,7 +2528,7 @@ async function printServiceLogs(opts) {
2408
2528
  process.stdout.write(output);
2409
2529
  return;
2410
2530
  }
2411
- if (!fs2.existsSync(FALLBACK_LOG_FILE)) {
2531
+ if (!fs3.existsSync(FALLBACK_LOG_FILE)) {
2412
2532
  console.log(`[rAgent] No log file found at ${FALLBACK_LOG_FILE}`);
2413
2533
  return;
2414
2534
  }
@@ -2421,7 +2541,7 @@ async function printServiceLogs(opts) {
2421
2541
  });
2422
2542
  return;
2423
2543
  }
2424
- const content = fs2.readFileSync(FALLBACK_LOG_FILE, "utf8");
2544
+ const content = fs3.readFileSync(FALLBACK_LOG_FILE, "utf8");
2425
2545
  const tail = content.split("\n").slice(-lines).join("\n");
2426
2546
  process.stdout.write(`${tail}${tail.endsWith("\n") ? "" : "\n"}`);
2427
2547
  }
@@ -2431,7 +2551,7 @@ async function uninstallService() {
2431
2551
  await runSystemctlUser(["stop", SERVICE_NAME]).catch(() => void 0);
2432
2552
  await runSystemctlUser(["disable", SERVICE_NAME]).catch(() => void 0);
2433
2553
  try {
2434
- fs2.unlinkSync(SERVICE_FILE);
2554
+ fs3.unlinkSync(SERVICE_FILE);
2435
2555
  } catch {
2436
2556
  }
2437
2557
  await runSystemctlUser(["daemon-reload"]).catch(() => void 0);
@@ -2455,7 +2575,7 @@ function requestStopSelfService() {
2455
2575
  }
2456
2576
  if (backend === "pidfile") {
2457
2577
  try {
2458
- fs2.unlinkSync(FALLBACK_PID_FILE);
2578
+ fs3.unlinkSync(FALLBACK_PID_FILE);
2459
2579
  } catch {
2460
2580
  }
2461
2581
  }
@@ -2674,6 +2794,7 @@ var ControlDispatcher = class {
2674
2794
  connection;
2675
2795
  transcriptWatcher;
2676
2796
  options;
2797
+ _approvalEnforcer = null;
2677
2798
  /** Set to true when a reconnect was requested (restart-agent, disconnect). */
2678
2799
  reconnectRequested = false;
2679
2800
  /** Set to false to stop the agent. */
@@ -2690,6 +2811,25 @@ var ControlDispatcher = class {
2690
2811
  updateOptions(options) {
2691
2812
  this.options = options;
2692
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
+ }
2693
2833
  /**
2694
2834
  * Verify HMAC-SHA256 signature on a control message.
2695
2835
  * Returns true if valid or no session key is available (backward compat).
@@ -2702,6 +2842,10 @@ var ControlDispatcher = class {
2702
2842
  console.warn("[rAgent] Control message missing HMAC signature.");
2703
2843
  return false;
2704
2844
  }
2845
+ if (!/^[0-9a-f]{64}$/i.test(receivedHmac)) {
2846
+ console.warn("[rAgent] Control message has malformed HMAC signature.");
2847
+ return false;
2848
+ }
2705
2849
  const { hmac: _, ...payloadWithoutHmac } = payload;
2706
2850
  const canonical = JSON.stringify(payloadWithoutHmac, Object.keys(payloadWithoutHmac).sort());
2707
2851
  const expected = crypto2.createHmac("sha256", sessionKey).update(canonical).digest("hex");
@@ -2718,7 +2862,9 @@ var ControlDispatcher = class {
2718
2862
  "stop-session",
2719
2863
  "stop-detached",
2720
2864
  "disconnect",
2721
- "kill-process"
2865
+ "kill-process",
2866
+ "start-agent",
2867
+ "provision"
2722
2868
  ]);
2723
2869
  if (dangerousActions.has(action) && this.connection.sessionKey) {
2724
2870
  if (!this.verifyMessageHmac(payload)) {
@@ -2798,6 +2944,9 @@ var ControlDispatcher = class {
2798
2944
  case "start-agent":
2799
2945
  await this.handleStartAgent(payload);
2800
2946
  return;
2947
+ case "provision":
2948
+ await this.handleProvision(payload);
2949
+ return;
2801
2950
  case "stream-session":
2802
2951
  this.handleStreamSession(sessionId);
2803
2952
  return;
@@ -2837,8 +2986,11 @@ var ControlDispatcher = class {
2837
2986
  }
2838
2987
  /** Handle provision request from dashboard. */
2839
2988
  async handleProvision(payload) {
2840
- const provReq = payload;
2841
- if (!provReq.provisionId || !provReq.manifest) return;
2989
+ const provReq = validateProvisionRequest(payload);
2990
+ if (!provReq) {
2991
+ console.warn("[rAgent] Rejecting provision request \u2014 malformed payload.");
2992
+ return;
2993
+ }
2842
2994
  console.log(`[rAgent] Provision request: ${provReq.manifest.name} (${provReq.provisionId})`);
2843
2995
  const sendProgress = (progress) => {
2844
2996
  const ws = this.connection.activeSocket;
@@ -2980,12 +3132,839 @@ var ControlDispatcher = class {
2980
3132
  }
2981
3133
  }
2982
3134
  };
3135
+ var VALID_INSTALL_METHODS = /* @__PURE__ */ new Set(["npm", "pip", "binary", "custom"]);
3136
+ var VALID_RUNTIMES = /* @__PURE__ */ new Set(["node", "python", "none"]);
3137
+ function validateProvisionRequest(payload) {
3138
+ if (typeof payload.provisionId !== "string" || !payload.provisionId) return null;
3139
+ if (typeof payload.sessionName !== "string" || !payload.sessionName) return null;
3140
+ if (typeof payload.command !== "string" || !payload.command) return null;
3141
+ const manifest = payload.manifest;
3142
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return null;
3143
+ const m = manifest;
3144
+ if (typeof m.id !== "string" || !m.id) return null;
3145
+ if (typeof m.name !== "string" || !m.name) return null;
3146
+ if (typeof m.installMethod !== "string" || !VALID_INSTALL_METHODS.has(m.installMethod)) return null;
3147
+ if (typeof m.installCommand !== "string") return null;
3148
+ if (typeof m.checkCommand !== "string") return null;
3149
+ let prerequisites;
3150
+ if (m.prerequisites !== void 0 && m.prerequisites !== null) {
3151
+ if (typeof m.prerequisites !== "object" || Array.isArray(m.prerequisites)) return null;
3152
+ const p = m.prerequisites;
3153
+ if (typeof p.runtime !== "string" || !VALID_RUNTIMES.has(p.runtime)) return null;
3154
+ prerequisites = {
3155
+ runtime: p.runtime,
3156
+ minVersion: typeof p.minVersion === "string" ? p.minVersion : void 0
3157
+ };
3158
+ }
3159
+ if (payload.workingDir !== void 0 && typeof payload.workingDir !== "string") return null;
3160
+ if (payload.envVars !== void 0 && (typeof payload.envVars !== "object" || payload.envVars === null || Array.isArray(payload.envVars))) return null;
3161
+ let validatedEnvVars;
3162
+ if (payload.envVars && typeof payload.envVars === "object") {
3163
+ const entries = Object.entries(payload.envVars);
3164
+ for (const [, v] of entries) {
3165
+ if (typeof v !== "string") return null;
3166
+ }
3167
+ validatedEnvVars = payload.envVars;
3168
+ }
3169
+ return {
3170
+ provisionId: payload.provisionId,
3171
+ sessionName: payload.sessionName,
3172
+ command: payload.command,
3173
+ workingDir: typeof payload.workingDir === "string" ? payload.workingDir : void 0,
3174
+ envVars: validatedEnvVars,
3175
+ manifest: {
3176
+ id: m.id,
3177
+ name: m.name,
3178
+ installMethod: m.installMethod,
3179
+ installCommand: m.installCommand,
3180
+ checkCommand: m.checkCommand,
3181
+ prerequisites
3182
+ }
3183
+ };
3184
+ }
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
+ };
2983
3468
 
2984
3469
  // src/transcript-watcher.ts
2985
- var fs3 = __toESM(require("fs"));
3470
+ var fs4 = __toESM(require("fs"));
2986
3471
  var path3 = __toESM(require("path"));
2987
3472
  var os7 = __toESM(require("os"));
2988
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
2989
3968
  var ClaudeCodeParser = class {
2990
3969
  name = "claude-code";
2991
3970
  /** Maps tool_use id → tool name for matching results back to tools. */
@@ -3116,10 +4095,10 @@ function discoverViaProc(panePid) {
3116
4095
  for (const childPid of children) {
3117
4096
  const fdDir = `/proc/${childPid}/fd`;
3118
4097
  try {
3119
- const fds = fs3.readdirSync(fdDir);
4098
+ const fds = fs4.readdirSync(fdDir);
3120
4099
  for (const fd of fds) {
3121
4100
  try {
3122
- const target = fs3.readlinkSync(path3.join(fdDir, fd));
4101
+ const target = fs4.readlinkSync(path3.join(fdDir, fd));
3123
4102
  if (target.endsWith(".jsonl") && target.includes("/.claude/")) {
3124
4103
  return target;
3125
4104
  }
@@ -3136,10 +4115,10 @@ function discoverViaProc(panePid) {
3136
4115
  for (const gcPid of grandchildren) {
3137
4116
  const gcFdDir = `/proc/${gcPid}/fd`;
3138
4117
  try {
3139
- const fds = fs3.readdirSync(gcFdDir);
4118
+ const fds = fs4.readdirSync(gcFdDir);
3140
4119
  for (const fd of fds) {
3141
4120
  try {
3142
- const target = fs3.readlinkSync(path3.join(gcFdDir, fd));
4121
+ const target = fs4.readlinkSync(path3.join(gcFdDir, fd));
3143
4122
  if (target.endsWith(".jsonl") && target.includes("/.claude/")) {
3144
4123
  return target;
3145
4124
  }
@@ -3158,15 +4137,15 @@ function discoverViaProc(panePid) {
3158
4137
  }
3159
4138
  function discoverViaCwd(paneCwd) {
3160
4139
  const claudeProjectsDir = path3.join(os7.homedir(), ".claude", "projects");
3161
- if (!fs3.existsSync(claudeProjectsDir)) return null;
3162
- const resolvedCwd = fs3.realpathSync(paneCwd);
4140
+ if (!fs4.existsSync(claudeProjectsDir)) return null;
4141
+ const resolvedCwd = fs4.realpathSync(paneCwd);
3163
4142
  const expectedDirName = resolvedCwd.replace(/\//g, "-");
3164
4143
  const projectDir = path3.join(claudeProjectsDir, expectedDirName);
3165
- if (!fs3.existsSync(projectDir)) return null;
4144
+ if (!fs4.existsSync(projectDir)) return null;
3166
4145
  try {
3167
- 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) => {
3168
4147
  const fullPath = path3.join(projectDir, f);
3169
- const stat = fs3.statSync(fullPath);
4148
+ const stat = fs4.statSync(fullPath);
3170
4149
  return { path: fullPath, mtime: stat.mtimeMs };
3171
4150
  }).sort((a, b) => b.mtime - a.mtime);
3172
4151
  return jsonlFiles.length > 0 ? jsonlFiles[0].path : null;
@@ -3176,14 +4155,14 @@ function discoverViaCwd(paneCwd) {
3176
4155
  }
3177
4156
  function discoverCodexTranscript() {
3178
4157
  const codexSessionsDir = path3.join(os7.homedir(), ".codex", "sessions");
3179
- if (!fs3.existsSync(codexSessionsDir)) return null;
4158
+ if (!fs4.existsSync(codexSessionsDir)) return null;
3180
4159
  try {
3181
- 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();
3182
4161
  for (const dateDir of dateDirs.slice(0, 3)) {
3183
4162
  const fullDir = path3.join(codexSessionsDir, dateDir);
3184
- 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) => {
3185
4164
  const fp = path3.join(fullDir, f);
3186
- const stat = fs3.statSync(fp);
4165
+ const stat = fs4.statSync(fp);
3187
4166
  return { path: fp, mtime: stat.mtimeMs };
3188
4167
  }).sort((a, b) => b.mtime - a.mtime);
3189
4168
  if (jsonlFiles.length > 0) return jsonlFiles[0].path;
@@ -3234,25 +4213,30 @@ var REDISCOVERY_INTERVAL_MS = 5e3;
3234
4213
  var TranscriptWatcher = class {
3235
4214
  filePath;
3236
4215
  parser;
4216
+ parserV2;
3237
4217
  callbacks;
3238
4218
  offset = 0;
3239
4219
  inode = 0;
3240
4220
  partialLine = "";
3241
4221
  seq = 0;
3242
4222
  turns = [];
4223
+ /** V2 normalized snapshot state. */
4224
+ snapshot;
3243
4225
  watcher = null;
3244
4226
  pollTimer = null;
3245
4227
  stopped = false;
3246
4228
  subscriberCount = 0;
3247
- constructor(filePath, parser, callbacks) {
4229
+ constructor(filePath, parser, callbacks, parserV2, sessionId) {
3248
4230
  this.filePath = filePath;
3249
4231
  this.parser = parser;
4232
+ this.parserV2 = parserV2;
3250
4233
  this.callbacks = callbacks;
4234
+ this.snapshot = createEmptySnapshot(sessionId ?? "");
3251
4235
  }
3252
4236
  /** Start watching the file. Returns false if file doesn't exist. */
3253
4237
  start() {
3254
4238
  try {
3255
- const stat = fs3.statSync(this.filePath);
4239
+ const stat = fs4.statSync(this.filePath);
3256
4240
  this.inode = stat.ino;
3257
4241
  this.offset = stat.size;
3258
4242
  } catch {
@@ -3299,6 +4283,10 @@ var TranscriptWatcher = class {
3299
4283
  get currentSeq() {
3300
4284
  return this.seq;
3301
4285
  }
4286
+ /** Get the V2 normalized snapshot. */
4287
+ getSnapshot() {
4288
+ return trimSnapshot(this.snapshot);
4289
+ }
3302
4290
  /** Read and replay the full transcript from the start of the file. */
3303
4291
  replayFromStart() {
3304
4292
  const savedOffset = this.offset;
@@ -3311,7 +4299,7 @@ var TranscriptWatcher = class {
3311
4299
  }
3312
4300
  startWatching() {
3313
4301
  try {
3314
- this.watcher = fs3.watch(this.filePath, () => {
4302
+ this.watcher = fs4.watch(this.filePath, () => {
3315
4303
  if (!this.stopped) this.readNewData();
3316
4304
  });
3317
4305
  this.watcher.on("error", () => {
@@ -3329,7 +4317,7 @@ var TranscriptWatcher = class {
3329
4317
  }
3330
4318
  checkForRotation() {
3331
4319
  try {
3332
- const stat = fs3.statSync(this.filePath);
4320
+ const stat = fs4.statSync(this.filePath);
3333
4321
  if (stat.ino !== this.inode) {
3334
4322
  this.inode = stat.ino;
3335
4323
  this.offset = 0;
@@ -3344,22 +4332,22 @@ var TranscriptWatcher = class {
3344
4332
  readNewData() {
3345
4333
  let fd;
3346
4334
  try {
3347
- fd = fs3.openSync(this.filePath, "r");
4335
+ fd = fs4.openSync(this.filePath, "r");
3348
4336
  } catch {
3349
4337
  return;
3350
4338
  }
3351
4339
  try {
3352
- const stat = fs3.fstatSync(fd);
4340
+ const stat = fs4.fstatSync(fd);
3353
4341
  if (stat.size <= this.offset) return;
3354
4342
  const readSize = Math.min(stat.size - this.offset, 256 * 1024);
3355
4343
  const buffer = Buffer.alloc(readSize);
3356
- const bytesRead = fs3.readSync(fd, buffer, 0, readSize, this.offset);
4344
+ const bytesRead = fs4.readSync(fd, buffer, 0, readSize, this.offset);
3357
4345
  if (bytesRead === 0) return;
3358
4346
  this.offset += bytesRead;
3359
4347
  const chunk = buffer.subarray(0, bytesRead).toString("utf-8");
3360
4348
  this.processChunk(chunk);
3361
4349
  } finally {
3362
- fs3.closeSync(fd);
4350
+ fs4.closeSync(fd);
3363
4351
  }
3364
4352
  }
3365
4353
  processChunk(chunk) {
@@ -3373,9 +4361,20 @@ var TranscriptWatcher = class {
3373
4361
  for (const line of lines) {
3374
4362
  const trimmed = line.trim();
3375
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
+ }
3376
4373
  const turn = this.parser.parseLine(trimmed);
3377
4374
  if (turn) {
3378
- this.seq++;
4375
+ if (!this.parserV2 || !this.callbacks.onOps) {
4376
+ this.seq++;
4377
+ }
3379
4378
  this.turns.push({
3380
4379
  turnId: turn.turnId,
3381
4380
  role: turn.role,
@@ -3391,22 +4390,27 @@ var TranscriptWatcher = class {
3391
4390
  }
3392
4391
  }
3393
4392
  };
3394
- var PARSERS = {
3395
- "Claude Code": new ClaudeCodeParser(),
3396
- "Codex CLI": new CodexCliParser()
4393
+ var PARSER_FACTORIES = {
4394
+ "Claude Code": () => new ClaudeCodeParser(),
4395
+ "Codex CLI": () => new CodexCliParser()
3397
4396
  };
3398
4397
  function getParser(agentType) {
3399
4398
  if (!agentType) return void 0;
3400
- return PARSERS[agentType];
4399
+ const factory = PARSER_FACTORIES[agentType];
4400
+ return factory ? factory() : void 0;
3401
4401
  }
3402
4402
  var TranscriptWatcherManager = class {
3403
4403
  active = /* @__PURE__ */ new Map();
3404
4404
  rediscoveryTimers = /* @__PURE__ */ new Map();
3405
4405
  sendFn;
3406
4406
  sendSnapshotFn;
3407
- constructor(sendFn, sendSnapshotFn) {
4407
+ sendOpsFn;
4408
+ sendV2SnapshotFn;
4409
+ constructor(sendFn, sendSnapshotFn, sendOpsFn, sendV2SnapshotFn) {
3408
4410
  this.sendFn = sendFn;
3409
4411
  this.sendSnapshotFn = sendSnapshotFn;
4412
+ this.sendOpsFn = sendOpsFn;
4413
+ this.sendV2SnapshotFn = sendV2SnapshotFn;
3410
4414
  }
3411
4415
  /** Enable markdown streaming for a session. */
3412
4416
  enableMarkdown(sessionId, agentType) {
@@ -3426,14 +4430,24 @@ var TranscriptWatcherManager = class {
3426
4430
  return false;
3427
4431
  }
3428
4432
  console.log(`[rAgent] Watching transcript: ${filePath} (${parser.name})`);
3429
- const watcher = new TranscriptWatcher(filePath, parser, {
3430
- onTurn: (turn, seq) => {
3431
- 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
+ }
3432
4447
  },
3433
- onError: (error) => {
3434
- console.warn(`[rAgent] Transcript watcher error (${sessionId}): ${error}`);
3435
- }
3436
- });
4448
+ v2Parser,
4449
+ sessionId
4450
+ );
3437
4451
  if (!watcher.start()) {
3438
4452
  console.warn(`[rAgent] Failed to start watching ${filePath}`);
3439
4453
  return false;
@@ -3467,14 +4481,24 @@ var TranscriptWatcherManager = class {
3467
4481
  session.watcher.stop();
3468
4482
  const parser = getParser(agentType);
3469
4483
  if (!parser) return;
3470
- const newWatcher = new TranscriptWatcher(newPath, parser, {
3471
- onTurn: (turn, seq) => {
3472
- 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
+ }
3473
4498
  },
3474
- onError: (error) => {
3475
- console.warn(`[rAgent] Transcript watcher error (${sessionId}): ${error}`);
3476
- }
3477
- });
4499
+ newV2Parser,
4500
+ sessionId
4501
+ );
3478
4502
  if (!newWatcher.start()) {
3479
4503
  console.warn(`[rAgent] Failed to start watching new transcript ${newPath}`);
3480
4504
  return;
@@ -3493,12 +4517,16 @@ var TranscriptWatcherManager = class {
3493
4517
  this.rediscoveryTimers.delete(sessionId);
3494
4518
  }
3495
4519
  }
3496
- /** Handle sync-markdown request — send replay snapshot. */
4520
+ /** Handle sync-markdown request — send replay snapshot (V1 + V2). */
3497
4521
  handleSyncRequest(sessionId, fromSeq) {
3498
4522
  const session = this.active.get(sessionId);
3499
4523
  if (!session) return;
3500
4524
  const turns = session.watcher.getReplayTurns(fromSeq);
3501
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
+ }
3502
4530
  }
3503
4531
  /** Stop all watchers. */
3504
4532
  stopAll() {
@@ -3522,7 +4550,7 @@ function pidFilePath(hostId) {
3522
4550
  }
3523
4551
  function readPidFile(filePath) {
3524
4552
  try {
3525
- const raw = fs4.readFileSync(filePath, "utf8").trim();
4553
+ const raw = fs5.readFileSync(filePath, "utf8").trim();
3526
4554
  const pid = Number.parseInt(raw, 10);
3527
4555
  return Number.isInteger(pid) && pid > 0 ? pid : null;
3528
4556
  } catch {
@@ -3547,7 +4575,7 @@ function acquirePidLock(hostId) {
3547
4575
  Stop it first with: kill ${existingPid} \u2014 or: ragent service stop`
3548
4576
  );
3549
4577
  }
3550
- fs4.writeFileSync(lockPath, `${process.pid}
4578
+ fs5.writeFileSync(lockPath, `${process.pid}
3551
4579
  `, "utf8");
3552
4580
  return lockPath;
3553
4581
  }
@@ -3555,7 +4583,7 @@ function releasePidLock(lockPath) {
3555
4583
  try {
3556
4584
  const currentPid = readPidFile(lockPath);
3557
4585
  if (currentPid === process.pid) {
3558
- fs4.unlinkSync(lockPath);
4586
+ fs5.unlinkSync(lockPath);
3559
4587
  }
3560
4588
  } catch {
3561
4589
  }
@@ -3576,9 +4604,9 @@ async function runAgent(rawOptions) {
3576
4604
  }
3577
4605
  const lockPath = acquirePidLock(options.hostId);
3578
4606
  const config = loadConfig();
3579
- if (config.redaction?.enabled) {
3580
- setRedactionEnabled(true);
3581
- console.log("[rAgent] Secret redaction enabled.");
4607
+ if (config.redaction?.enabled === false || rawOptions.redact === false) {
4608
+ setRedactionEnabled(false);
4609
+ console.log("[rAgent] Secret redaction disabled.");
3582
4610
  }
3583
4611
  console.log(`[rAgent] Connector started for ${options.hostName} (${options.hostId})`);
3584
4612
  console.log(`[rAgent] Portal: ${options.portal}`);
@@ -3661,6 +4689,44 @@ async function runAgent(rawOptions) {
3661
4689
  } else {
3662
4690
  sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
3663
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
+ }
3664
4730
  }
3665
4731
  );
3666
4732
  const shell = new ShellManager(options.command, sendOutput);
@@ -3697,6 +4763,24 @@ async function runAgent(rawOptions) {
3697
4763
  await new Promise((resolve) => {
3698
4764
  const ws = new import_ws5.default(negotiated.url, "json.webpubsub.azure.v1");
3699
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
+ }
3700
4784
  ws.on("open", async () => {
3701
4785
  console.log("[rAgent] Connector connected to relay.");
3702
4786
  conn.resetReconnectDelay();
@@ -3763,7 +4847,9 @@ async function runAgent(rawOptions) {
3763
4847
  } else if (payload.type === "start-agent") {
3764
4848
  await dispatcher.handleControlAction({ ...payload, action: "start-agent" });
3765
4849
  } else if (payload.type === "provision") {
3766
- await dispatcher.handleProvision(payload);
4850
+ await dispatcher.handleControlAction({ ...payload, action: "provision" });
4851
+ } else if (payload.type === "approval") {
4852
+ dispatcher.handleApprovalMessage(payload);
3767
4853
  }
3768
4854
  }
3769
4855
  if (msg.type === "message" && msg.group === groups.registryGroup) {
@@ -3985,7 +5071,7 @@ function registerConnectCommand(program2) {
3985
5071
 
3986
5072
  // src/commands/run.ts
3987
5073
  function registerRunCommand(program2) {
3988
- 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").action(async (opts) => {
5074
+ 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) => {
3989
5075
  try {
3990
5076
  printCommandArt("Run Connector", "streaming terminal session to portal");
3991
5077
  await runAgent(opts);
@@ -4188,7 +5274,7 @@ function registerServiceCommand(program2) {
4188
5274
  }
4189
5275
 
4190
5276
  // src/commands/uninstall.ts
4191
- var fs5 = __toESM(require("fs"));
5277
+ var fs6 = __toESM(require("fs"));
4192
5278
  async function uninstallAgent(opts) {
4193
5279
  const config = loadConfig();
4194
5280
  const hostName = config.hostName || config.hostId || "this machine";
@@ -4219,8 +5305,8 @@ async function uninstallAgent(opts) {
4219
5305
  }
4220
5306
  console.log("[rAgent] Stopping and removing service...");
4221
5307
  await uninstallService().catch(() => void 0);
4222
- if (fs5.existsSync(CONFIG_DIR)) {
4223
- fs5.rmSync(CONFIG_DIR, { recursive: true, force: true });
5308
+ if (fs6.existsSync(CONFIG_DIR)) {
5309
+ fs6.rmSync(CONFIG_DIR, { recursive: true, force: true });
4224
5310
  console.log(`[rAgent] Removed config directory: ${CONFIG_DIR}`);
4225
5311
  }
4226
5312
  try {
@@ -4243,6 +5329,58 @@ function registerUninstallCommand(parent) {
4243
5329
  });
4244
5330
  }
4245
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
+
4246
5384
  // src/index.ts
4247
5385
  process.on("unhandledRejection", (reason) => {
4248
5386
  const message = reason instanceof Error ? reason.stack || reason.message : String(reason);
@@ -4256,6 +5394,7 @@ registerUpdateCommand(import_commander.program);
4256
5394
  registerSessionsCommand(import_commander.program);
4257
5395
  registerServiceCommand(import_commander.program);
4258
5396
  registerUninstallCommand(import_commander.program);
5397
+ registerDiscoverCommand(import_commander.program);
4259
5398
  import_commander.program.hook("preAction", async (_thisCommand, actionCommand) => {
4260
5399
  if (actionCommand.name() === "update") return;
4261
5400
  await maybeWarnUpdate();
@@ -4269,7 +5408,7 @@ function showStatus() {
4269
5408
  ragent v${CURRENT_VERSION}
4270
5409
  `);
4271
5410
  try {
4272
- const raw = fs6.readFileSync(CONFIG_FILE, "utf8");
5411
+ const raw = fs7.readFileSync(CONFIG_FILE, "utf8");
4273
5412
  const config = JSON.parse(raw);
4274
5413
  if (config.portal && config.agentToken) {
4275
5414
  console.log(` Status: Connected`);