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