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/SECURITY.md +75 -0
- package/dist/index.js +1238 -99
- package/dist/sbom.json +1149 -0
- package/package.json +7 -3
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.
|
|
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": "^
|
|
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
|
|
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
|
|
238
|
+
const now2 = Date.now();
|
|
235
239
|
let latestVersion = null;
|
|
236
|
-
if (!force && lastChecked > 0 &&
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
543
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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 (
|
|
2158
|
-
if (
|
|
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
|
-
|
|
2202
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
2358
|
+
fs3.unlinkSync(rotated);
|
|
2239
2359
|
} catch {
|
|
2240
2360
|
}
|
|
2241
|
-
|
|
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 =
|
|
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
|
-
|
|
2264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
4098
|
+
const fds = fs4.readdirSync(fdDir);
|
|
3120
4099
|
for (const fd of fds) {
|
|
3121
4100
|
try {
|
|
3122
|
-
const target =
|
|
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 =
|
|
4118
|
+
const fds = fs4.readdirSync(gcFdDir);
|
|
3140
4119
|
for (const fd of fds) {
|
|
3141
4120
|
try {
|
|
3142
|
-
const target =
|
|
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 (!
|
|
3162
|
-
const resolvedCwd =
|
|
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 (!
|
|
4144
|
+
if (!fs4.existsSync(projectDir)) return null;
|
|
3166
4145
|
try {
|
|
3167
|
-
const jsonlFiles =
|
|
4146
|
+
const jsonlFiles = fs4.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => {
|
|
3168
4147
|
const fullPath = path3.join(projectDir, f);
|
|
3169
|
-
const stat =
|
|
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 (!
|
|
4158
|
+
if (!fs4.existsSync(codexSessionsDir)) return null;
|
|
3180
4159
|
try {
|
|
3181
|
-
const dateDirs =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
4335
|
+
fd = fs4.openSync(this.filePath, "r");
|
|
3348
4336
|
} catch {
|
|
3349
4337
|
return;
|
|
3350
4338
|
}
|
|
3351
4339
|
try {
|
|
3352
|
-
const stat =
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3430
|
-
|
|
3431
|
-
|
|
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
|
-
|
|
3434
|
-
|
|
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
|
|
3471
|
-
|
|
3472
|
-
|
|
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
|
-
|
|
3475
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
3581
|
-
console.log("[rAgent] Secret redaction
|
|
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.
|
|
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
|
|
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 (
|
|
4223
|
-
|
|
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 =
|
|
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`);
|