agentflow-dashboard 0.8.4 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-LZXEKWRO.js +1220 -0
- package/dist/{chunk-YLQ5MVCW.js → chunk-NEERJBUV.js} +1683 -282
- package/dist/cli.cjs +6105 -303
- package/dist/cli.js +2 -1
- package/dist/client/assets/index-Bjpw_cGn.js +10 -0
- package/dist/client/assets/index-DgJkzB6k.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/dist-H4QMTTY3.js +3223 -0
- package/dist/index.cjs +6105 -303
- package/dist/index.js +2 -1
- package/dist/ops-intel-PL6GGIKL.js +31 -0
- package/dist/server.cjs +6105 -303
- package/dist/server.js +2 -1
- package/package.json +6 -2
- package/dist/client/assets/index-BQBa4cES.css +0 -1
- package/dist/client/assets/index-DA9m90ZC.js +0 -50
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__require
|
|
3
|
+
} from "./chunk-3RG5ZIWI.js";
|
|
4
|
+
|
|
1
5
|
// src/server.ts
|
|
2
6
|
import { execFileSync, execSync } from "child_process";
|
|
3
|
-
import * as
|
|
7
|
+
import * as fs4 from "fs";
|
|
4
8
|
import { createServer } from "http";
|
|
5
|
-
import * as
|
|
9
|
+
import * as path4 from "path";
|
|
6
10
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
11
|
import {
|
|
8
12
|
auditProcesses,
|
|
@@ -12,7 +16,8 @@ import {
|
|
|
12
16
|
discoverProcess,
|
|
13
17
|
findVariants,
|
|
14
18
|
getBottlenecks,
|
|
15
|
-
loadGraph as loadGraph2
|
|
19
|
+
loadGraph as loadGraph2,
|
|
20
|
+
toReceipt
|
|
16
21
|
} from "agentflow-core";
|
|
17
22
|
import chokidar2 from "chokidar";
|
|
18
23
|
import express from "express";
|
|
@@ -89,6 +94,70 @@ function getAgentDetection(config) {
|
|
|
89
94
|
function getProcessPreference(config) {
|
|
90
95
|
return config.processPreference ?? null;
|
|
91
96
|
}
|
|
97
|
+
function getExternalCommands(config) {
|
|
98
|
+
return config.externalCommands ?? {};
|
|
99
|
+
}
|
|
100
|
+
function getValidatedExternalCommands(config) {
|
|
101
|
+
var _a, _b, _c, _d;
|
|
102
|
+
const externalCommands = getExternalCommands(config);
|
|
103
|
+
const errors = [];
|
|
104
|
+
const validatedCommands = {};
|
|
105
|
+
if (!externalCommands.commands) {
|
|
106
|
+
return { commands: {}, errors: [] };
|
|
107
|
+
}
|
|
108
|
+
for (const [commandId, command] of Object.entries(externalCommands.commands)) {
|
|
109
|
+
try {
|
|
110
|
+
if (!commandId.match(/^[a-z][a-z0-9-_]*$/i)) {
|
|
111
|
+
errors.push(`Invalid command ID "${commandId}": must start with letter and contain only letters, numbers, hyphens, and underscores`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!((_a = command.name) == null ? void 0 : _a.trim())) {
|
|
115
|
+
errors.push(`Command "${commandId}": name is required`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!((_b = command.command) == null ? void 0 : _b.trim())) {
|
|
119
|
+
errors.push(`Command "${commandId}": command is required`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const validatedCommand = {
|
|
123
|
+
name: command.name.trim(),
|
|
124
|
+
command: command.command.trim(),
|
|
125
|
+
args: command.args ?? [],
|
|
126
|
+
cwd: expandTilde(command.cwd ?? externalCommands.globalCwd ?? process.cwd()),
|
|
127
|
+
env: { ...externalCommands.globalEnv, ...command.env },
|
|
128
|
+
timeout: command.timeout ?? externalCommands.globalTimeout ?? 6e4,
|
|
129
|
+
description: ((_c = command.description) == null ? void 0 : _c.trim()) ?? "",
|
|
130
|
+
category: ((_d = command.category) == null ? void 0 : _d.trim()) ?? "general",
|
|
131
|
+
allowConcurrent: command.allowConcurrent ?? false
|
|
132
|
+
};
|
|
133
|
+
if (validatedCommand.timeout <= 0 || validatedCommand.timeout > 6e5) {
|
|
134
|
+
errors.push(`Command "${commandId}": timeout must be between 1ms and 600000ms (10 minutes)`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (validatedCommand.cwd.startsWith("/")) {
|
|
138
|
+
try {
|
|
139
|
+
const fs5 = __require("fs");
|
|
140
|
+
const stats = fs5.statSync(validatedCommand.cwd);
|
|
141
|
+
if (!stats.isDirectory()) {
|
|
142
|
+
errors.push(`Command "${commandId}": cwd "${validatedCommand.cwd}" is not a directory`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
errors.push(`Command "${commandId}": cwd "${validatedCommand.cwd}" does not exist or is not accessible`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
validatedCommands[commandId] = validatedCommand;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
errors.push(`Command "${commandId}": validation error - ${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { commands: validatedCommands, errors };
|
|
156
|
+
}
|
|
157
|
+
function getExternalCommand(config, commandId) {
|
|
158
|
+
const { commands } = getValidatedExternalCommands(config);
|
|
159
|
+
return commands[commandId] ?? null;
|
|
160
|
+
}
|
|
92
161
|
|
|
93
162
|
// src/adapters/agentflow.ts
|
|
94
163
|
var SKIP_FILES = /* @__PURE__ */ new Set([
|
|
@@ -167,6 +236,7 @@ var OpenClawAdapter = class {
|
|
|
167
236
|
return filePath.includes("/cron/runs/") || filePath.includes("\\cron\\runs\\");
|
|
168
237
|
}
|
|
169
238
|
parse(filePath) {
|
|
239
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
170
240
|
const traces = [];
|
|
171
241
|
try {
|
|
172
242
|
const content = readFileSync2(filePath, "utf-8");
|
|
@@ -186,34 +256,100 @@ var OpenClawAdapter = class {
|
|
|
186
256
|
const jobName = (job == null ? void 0 : job.name) ?? jobId;
|
|
187
257
|
const startTime = entry.runAtMs ?? entry.ts;
|
|
188
258
|
const duration = entry.durationMs ?? 0;
|
|
259
|
+
const runStatus = entry.status === "ok" ? "completed" : entry.status === "error" ? "failed" : "unknown";
|
|
260
|
+
const summary = entry.summary ?? "";
|
|
261
|
+
const nodes = {};
|
|
262
|
+
const rootChildren = [];
|
|
263
|
+
let stepIdx = 0;
|
|
264
|
+
let m;
|
|
265
|
+
const tableRe = /\|\s*\*?\*?(\d+)\.\s*\*?\*?([^|]+)\*?\*?\s*\|\s*([^|]+)\s*\|\s*([^|]*)\|/g;
|
|
266
|
+
while ((m = tableRe.exec(summary)) !== null) {
|
|
267
|
+
stepIdx++;
|
|
268
|
+
const nodeId = `step-${stepIdx}`;
|
|
269
|
+
rootChildren.push(nodeId);
|
|
270
|
+
nodes[nodeId] = {
|
|
271
|
+
id: nodeId,
|
|
272
|
+
type: "tool",
|
|
273
|
+
name: ((_a = m[2]) == null ? void 0 : _a.trim().replace(/\*+/g, "")) || `Step ${stepIdx}`,
|
|
274
|
+
status: ((_b = m[3]) == null ? void 0 : _b.includes("\u274C")) || ((_c = m[3]) == null ? void 0 : _c.toLowerCase().includes("fail")) ? "failed" : "completed",
|
|
275
|
+
startTime: startTime + (stepIdx - 1) * Math.floor(duration / Math.max(1, stepIdx + 1)),
|
|
276
|
+
endTime: startTime + stepIdx * Math.floor(duration / Math.max(1, stepIdx + 1)),
|
|
277
|
+
parentId: "root",
|
|
278
|
+
children: [],
|
|
279
|
+
metadata: { detail: ((_d = m[4]) == null ? void 0 : _d.trim()) || "" }
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (stepIdx === 0) {
|
|
283
|
+
const listRe = /^\s*(\d+)\.\s*\*?\*?([^*\n]+)\*?\*?\s*(?:[-\u2014]\s*(.+))?$/gm;
|
|
284
|
+
while ((m = listRe.exec(summary)) !== null) {
|
|
285
|
+
if ((_e = m[2]) == null ? void 0 : _e.trim().startsWith("#")) continue;
|
|
286
|
+
stepIdx++;
|
|
287
|
+
const nodeId = `step-${stepIdx}`;
|
|
288
|
+
const detail = ((_f = m[3]) == null ? void 0 : _f.trim()) || "";
|
|
289
|
+
rootChildren.push(nodeId);
|
|
290
|
+
nodes[nodeId] = {
|
|
291
|
+
id: nodeId,
|
|
292
|
+
type: "tool",
|
|
293
|
+
name: ((_g = m[2]) == null ? void 0 : _g.trim().replace(/\*+/g, "")) || `Step ${stepIdx}`,
|
|
294
|
+
status: detail.toLowerCase().includes("fail") ? "failed" : "completed",
|
|
295
|
+
startTime: startTime + (stepIdx - 1) * Math.floor(duration / Math.max(1, stepIdx + 1)),
|
|
296
|
+
endTime: startTime + stepIdx * Math.floor(duration / Math.max(1, stepIdx + 1)),
|
|
297
|
+
parentId: "root",
|
|
298
|
+
children: [],
|
|
299
|
+
metadata: { detail }
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (stepIdx === 0 && summary.length > 50) {
|
|
304
|
+
const sectionRe = /(?:^|\n)\s*(?:#{1,3}|(?:\*\*[^*\n]{3,50}\*\*))\s*(.+)/g;
|
|
305
|
+
while ((m = sectionRe.exec(summary)) !== null) {
|
|
306
|
+
const heading = ((_h = m[1]) == null ? void 0 : _h.trim().replace(/\*+/g, "").replace(/^#+\s*/, "")) || "";
|
|
307
|
+
if (!heading || heading.length < 3 || heading.startsWith("|") || heading.startsWith("---"))
|
|
308
|
+
continue;
|
|
309
|
+
stepIdx++;
|
|
310
|
+
const nodeId = `section-${stepIdx}`;
|
|
311
|
+
rootChildren.push(nodeId);
|
|
312
|
+
nodes[nodeId] = {
|
|
313
|
+
id: nodeId,
|
|
314
|
+
type: "custom",
|
|
315
|
+
name: heading.slice(0, 60),
|
|
316
|
+
status: "completed",
|
|
317
|
+
startTime: startTime + (stepIdx - 1) * Math.floor(duration / Math.max(1, stepIdx + 1)),
|
|
318
|
+
endTime: startTime + stepIdx * Math.floor(duration / Math.max(1, stepIdx + 1)),
|
|
319
|
+
parentId: "root",
|
|
320
|
+
children: [],
|
|
321
|
+
metadata: {}
|
|
322
|
+
};
|
|
323
|
+
if (stepIdx >= 10) break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
nodes["root"] = {
|
|
327
|
+
id: "root",
|
|
328
|
+
type: "cron-job",
|
|
329
|
+
name: jobName,
|
|
330
|
+
status: runStatus,
|
|
331
|
+
startTime,
|
|
332
|
+
endTime: startTime + duration,
|
|
333
|
+
parentId: null,
|
|
334
|
+
children: rootChildren,
|
|
335
|
+
metadata: {
|
|
336
|
+
jobId,
|
|
337
|
+
summary: entry.summary,
|
|
338
|
+
error: entry.error,
|
|
339
|
+
delivered: entry.delivered,
|
|
340
|
+
deliveryStatus: entry.deliveryStatus
|
|
341
|
+
}
|
|
342
|
+
};
|
|
189
343
|
const trace = {
|
|
190
344
|
id: entry.sessionId ?? `${jobId}-${entry.ts}`,
|
|
191
345
|
agentId: `openclaw:${jobId}`,
|
|
192
346
|
name: jobName,
|
|
193
|
-
status:
|
|
347
|
+
status: runStatus,
|
|
194
348
|
startTime,
|
|
195
349
|
endTime: startTime + duration,
|
|
196
350
|
trigger: "cron",
|
|
197
351
|
source: "openclaw",
|
|
198
|
-
nodes
|
|
199
|
-
root: {
|
|
200
|
-
id: "root",
|
|
201
|
-
type: "cron-job",
|
|
202
|
-
name: jobName,
|
|
203
|
-
status: entry.status === "ok" ? "completed" : entry.status === "error" ? "failed" : "unknown",
|
|
204
|
-
startTime,
|
|
205
|
-
endTime: startTime + duration,
|
|
206
|
-
parentId: null,
|
|
207
|
-
children: [],
|
|
208
|
-
metadata: {
|
|
209
|
-
jobId,
|
|
210
|
-
summary: entry.summary,
|
|
211
|
-
error: entry.error,
|
|
212
|
-
delivered: entry.delivered,
|
|
213
|
-
deliveryStatus: entry.deliveryStatus
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
},
|
|
352
|
+
nodes,
|
|
217
353
|
metadata: {
|
|
218
354
|
model: entry.model,
|
|
219
355
|
provider: entry.provider,
|
|
@@ -441,8 +577,9 @@ function deduplicateAgents(agents) {
|
|
|
441
577
|
const mergedIds = /* @__PURE__ */ new Set();
|
|
442
578
|
const mergedAgents = [];
|
|
443
579
|
for (const [_key, group] of suffixGroups) {
|
|
444
|
-
const suffix = extractSuffix((_a = group[0]) == null ? void 0 : _a.localId);
|
|
445
580
|
if (group.length < 2) continue;
|
|
581
|
+
const suffix = extractSuffix(((_a = group[0]) == null ? void 0 : _a.localId) ?? "");
|
|
582
|
+
if (!suffix) continue;
|
|
446
583
|
const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
|
|
447
584
|
if (prefixes.size < 2) continue;
|
|
448
585
|
const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
|
|
@@ -519,13 +656,470 @@ function groupAgents(agents) {
|
|
|
519
656
|
return { groups };
|
|
520
657
|
}
|
|
521
658
|
|
|
659
|
+
// src/command-executor.ts
|
|
660
|
+
import * as fs from "fs";
|
|
661
|
+
import * as path from "path";
|
|
662
|
+
import { spawn } from "child_process";
|
|
663
|
+
var CommandExecutor = class {
|
|
664
|
+
constructor(config, options = {}) {
|
|
665
|
+
this.config = config;
|
|
666
|
+
var _a;
|
|
667
|
+
this.maxConcurrentExecutions = options.maxConcurrentExecutions ?? ((_a = config.externalCommands) == null ? void 0 : _a.maxConcurrentExecutions) ?? 5;
|
|
668
|
+
}
|
|
669
|
+
executions = /* @__PURE__ */ new Map();
|
|
670
|
+
runningProcesses = /* @__PURE__ */ new Map();
|
|
671
|
+
executionCounter = 0;
|
|
672
|
+
maxConcurrentExecutions;
|
|
673
|
+
/**
|
|
674
|
+
* Execute an external command with security validation
|
|
675
|
+
*/
|
|
676
|
+
async executeCommand(request) {
|
|
677
|
+
var _a, _b;
|
|
678
|
+
const executionId = this.generateExecutionId();
|
|
679
|
+
try {
|
|
680
|
+
const validation = this.validateExecutionRequest(request);
|
|
681
|
+
if (validation.length > 0) {
|
|
682
|
+
return this.createFailedResult(executionId, request.commandId, validation[0].message);
|
|
683
|
+
}
|
|
684
|
+
const command = getExternalCommand(this.config, request.commandId);
|
|
685
|
+
if (!command) {
|
|
686
|
+
return this.createFailedResult(executionId, request.commandId, `Command "${request.commandId}" not found in configuration`);
|
|
687
|
+
}
|
|
688
|
+
if (!this.canStartExecution(command)) {
|
|
689
|
+
return this.createFailedResult(
|
|
690
|
+
executionId,
|
|
691
|
+
request.commandId,
|
|
692
|
+
`Cannot start command: ${command.allowConcurrent ? "concurrent execution limit reached" : "command already running"}`
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
const result = {
|
|
696
|
+
executionId,
|
|
697
|
+
started: false,
|
|
698
|
+
command,
|
|
699
|
+
startedAt: Date.now(),
|
|
700
|
+
stdout: "",
|
|
701
|
+
stderr: "",
|
|
702
|
+
status: "running"
|
|
703
|
+
};
|
|
704
|
+
this.executions.set(executionId, result);
|
|
705
|
+
const sanitizedArgs = this.sanitizeArguments([
|
|
706
|
+
...command.args,
|
|
707
|
+
...request.additionalArgs ?? []
|
|
708
|
+
]);
|
|
709
|
+
const executionTimeout = request.timeout ?? command.timeout;
|
|
710
|
+
const sanitizedEnv = this.sanitizeEnvironment(command.env);
|
|
711
|
+
const childProcess = spawn(command.command, sanitizedArgs, {
|
|
712
|
+
cwd: command.cwd,
|
|
713
|
+
env: { ...process.env, ...sanitizedEnv },
|
|
714
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
715
|
+
// stdin ignored, capture stdout/stderr
|
|
716
|
+
detached: false
|
|
717
|
+
// Keep process attached for proper cleanup
|
|
718
|
+
});
|
|
719
|
+
result.started = true;
|
|
720
|
+
result.pid = childProcess.pid;
|
|
721
|
+
this.runningProcesses.set(executionId, childProcess);
|
|
722
|
+
const timeoutHandle = setTimeout(() => {
|
|
723
|
+
this.killExecution(executionId, "timeout");
|
|
724
|
+
}, executionTimeout);
|
|
725
|
+
(_a = childProcess.stdout) == null ? void 0 : _a.on("data", (data) => {
|
|
726
|
+
result.stdout += data.toString();
|
|
727
|
+
});
|
|
728
|
+
(_b = childProcess.stderr) == null ? void 0 : _b.on("data", (data) => {
|
|
729
|
+
result.stderr += data.toString();
|
|
730
|
+
});
|
|
731
|
+
childProcess.on("close", (code, signal) => {
|
|
732
|
+
clearTimeout(timeoutHandle);
|
|
733
|
+
this.runningProcesses.delete(executionId);
|
|
734
|
+
result.completedAt = Date.now();
|
|
735
|
+
result.duration = result.completedAt - result.startedAt;
|
|
736
|
+
result.exitCode = code ?? void 0;
|
|
737
|
+
if (signal) {
|
|
738
|
+
result.status = result.status === "running" ? "killed" : result.status;
|
|
739
|
+
result.error = `Process killed by signal: ${signal}`;
|
|
740
|
+
} else if (code === 0) {
|
|
741
|
+
result.status = "completed";
|
|
742
|
+
} else {
|
|
743
|
+
result.status = "failed";
|
|
744
|
+
result.error = `Process exited with code: ${code}`;
|
|
745
|
+
}
|
|
746
|
+
this.logExecution(result, request.context);
|
|
747
|
+
});
|
|
748
|
+
childProcess.on("error", (error) => {
|
|
749
|
+
clearTimeout(timeoutHandle);
|
|
750
|
+
this.runningProcesses.delete(executionId);
|
|
751
|
+
result.completedAt = Date.now();
|
|
752
|
+
result.duration = result.completedAt - result.startedAt;
|
|
753
|
+
result.status = "failed";
|
|
754
|
+
result.error = `Process error: ${error.message}`;
|
|
755
|
+
this.logExecution(result, request.context);
|
|
756
|
+
});
|
|
757
|
+
return result;
|
|
758
|
+
} catch (error) {
|
|
759
|
+
return this.createFailedResult(
|
|
760
|
+
executionId,
|
|
761
|
+
request.commandId,
|
|
762
|
+
`Execution setup failed: ${error.message}`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Get execution status by ID
|
|
768
|
+
*/
|
|
769
|
+
getExecution(executionId) {
|
|
770
|
+
return this.executions.get(executionId) ?? null;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Get all executions (recent first)
|
|
774
|
+
*/
|
|
775
|
+
getAllExecutions(limit = 100) {
|
|
776
|
+
return Array.from(this.executions.values()).sort((a, b) => b.startedAt - a.startedAt).slice(0, limit);
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Kill a running execution
|
|
780
|
+
*/
|
|
781
|
+
killExecution(executionId, reason = "manual") {
|
|
782
|
+
const execution = this.executions.get(executionId);
|
|
783
|
+
const process2 = this.runningProcesses.get(executionId);
|
|
784
|
+
if (!execution || !process2) {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
process2.kill("SIGTERM");
|
|
789
|
+
setTimeout(() => {
|
|
790
|
+
if (this.runningProcesses.has(executionId)) {
|
|
791
|
+
process2.kill("SIGKILL");
|
|
792
|
+
}
|
|
793
|
+
}, 5e3);
|
|
794
|
+
if (reason === "timeout") {
|
|
795
|
+
execution.status = "timeout";
|
|
796
|
+
execution.error = "Command execution timed out";
|
|
797
|
+
} else {
|
|
798
|
+
execution.status = "killed";
|
|
799
|
+
execution.error = `Command killed: ${reason}`;
|
|
800
|
+
}
|
|
801
|
+
return true;
|
|
802
|
+
} catch (error) {
|
|
803
|
+
execution.error = `Failed to kill process: ${error.message}`;
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Get currently running executions
|
|
809
|
+
*/
|
|
810
|
+
getRunningExecutions() {
|
|
811
|
+
return Array.from(this.executions.values()).filter((exec) => exec.status === "running");
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Clean up old execution records
|
|
815
|
+
*/
|
|
816
|
+
cleanupExecutions(maxAge = 24 * 60 * 60 * 1e3) {
|
|
817
|
+
const cutoff = Date.now() - maxAge;
|
|
818
|
+
let cleaned = 0;
|
|
819
|
+
for (const [id, execution] of this.executions.entries()) {
|
|
820
|
+
if (execution.status !== "running" && execution.startedAt < cutoff) {
|
|
821
|
+
this.executions.delete(id);
|
|
822
|
+
cleaned++;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return cleaned;
|
|
826
|
+
}
|
|
827
|
+
validateExecutionRequest(request) {
|
|
828
|
+
var _a;
|
|
829
|
+
const errors = [];
|
|
830
|
+
if (!((_a = request.commandId) == null ? void 0 : _a.trim())) {
|
|
831
|
+
errors.push({
|
|
832
|
+
type: "validation_error",
|
|
833
|
+
message: "Command ID is required"
|
|
834
|
+
});
|
|
835
|
+
return errors;
|
|
836
|
+
}
|
|
837
|
+
const { commands, errors: configErrors } = getValidatedExternalCommands(this.config);
|
|
838
|
+
if (configErrors.length > 0) {
|
|
839
|
+
errors.push({
|
|
840
|
+
type: "config_error",
|
|
841
|
+
message: `Configuration errors: ${configErrors.join(", ")}`
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
if (!commands[request.commandId]) {
|
|
845
|
+
errors.push({
|
|
846
|
+
type: "validation_error",
|
|
847
|
+
message: `Command "${request.commandId}" not found in configuration`,
|
|
848
|
+
commandId: request.commandId
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
if (request.additionalArgs) {
|
|
852
|
+
for (const arg of request.additionalArgs) {
|
|
853
|
+
if (this.containsUnsafeContent(arg)) {
|
|
854
|
+
errors.push({
|
|
855
|
+
type: "security_violation",
|
|
856
|
+
message: `Unsafe content detected in additional arguments`,
|
|
857
|
+
commandId: request.commandId
|
|
858
|
+
});
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (request.timeout !== void 0 && (request.timeout <= 0 || request.timeout > 6e5)) {
|
|
864
|
+
errors.push({
|
|
865
|
+
type: "validation_error",
|
|
866
|
+
message: "Timeout must be between 1ms and 600000ms (10 minutes)",
|
|
867
|
+
commandId: request.commandId
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
return errors;
|
|
871
|
+
}
|
|
872
|
+
containsUnsafeContent(content) {
|
|
873
|
+
const dangerousPatterns = [
|
|
874
|
+
/[;&|`$(){}[\]]/,
|
|
875
|
+
// Shell metacharacters
|
|
876
|
+
/\.\./,
|
|
877
|
+
// Directory traversal
|
|
878
|
+
/\/dev\/|\/proc\/|\/sys\//,
|
|
879
|
+
// System paths
|
|
880
|
+
/rm\s+-rf|rm\s+-f/i,
|
|
881
|
+
// Dangerous rm commands
|
|
882
|
+
/chmod|chown|sudo/i,
|
|
883
|
+
// Privilege escalation
|
|
884
|
+
/curl|wget|nc|telnet/i
|
|
885
|
+
// Network commands
|
|
886
|
+
];
|
|
887
|
+
return dangerousPatterns.some((pattern) => pattern.test(content));
|
|
888
|
+
}
|
|
889
|
+
sanitizeArguments(args) {
|
|
890
|
+
return args.map((arg) => {
|
|
891
|
+
let sanitized = arg.replace(/[\x00-\x1f\x7f]/g, "");
|
|
892
|
+
sanitized = sanitized.trim();
|
|
893
|
+
if (sanitized.length > 1e3) {
|
|
894
|
+
sanitized = sanitized.substring(0, 1e3);
|
|
895
|
+
}
|
|
896
|
+
return sanitized;
|
|
897
|
+
}).filter((arg) => arg.length > 0);
|
|
898
|
+
}
|
|
899
|
+
sanitizeEnvironment(env) {
|
|
900
|
+
const sanitized = {};
|
|
901
|
+
if (!env) return sanitized;
|
|
902
|
+
for (const [key, value] of Object.entries(env)) {
|
|
903
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
const sanitizedValue = value.replace(/[\x00-\x1f\x7f]/g, "").trim();
|
|
907
|
+
if (sanitizedValue.length > 0 && sanitizedValue.length <= 4096) {
|
|
908
|
+
sanitized[key] = sanitizedValue;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return sanitized;
|
|
912
|
+
}
|
|
913
|
+
canStartExecution(command) {
|
|
914
|
+
const running = this.getRunningExecutions();
|
|
915
|
+
if (running.length >= this.maxConcurrentExecutions) {
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
if (!command.allowConcurrent) {
|
|
919
|
+
const commandRunning = running.some(
|
|
920
|
+
(exec) => exec.command.name === command.name
|
|
921
|
+
);
|
|
922
|
+
if (commandRunning) {
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
928
|
+
generateExecutionId() {
|
|
929
|
+
return `exec_${Date.now()}_${++this.executionCounter}`;
|
|
930
|
+
}
|
|
931
|
+
createFailedResult(executionId, commandId, error) {
|
|
932
|
+
const result = {
|
|
933
|
+
executionId,
|
|
934
|
+
started: false,
|
|
935
|
+
command: {
|
|
936
|
+
name: commandId,
|
|
937
|
+
command: "unknown",
|
|
938
|
+
timeout: 0
|
|
939
|
+
},
|
|
940
|
+
startedAt: Date.now(),
|
|
941
|
+
completedAt: Date.now(),
|
|
942
|
+
duration: 0,
|
|
943
|
+
stdout: "",
|
|
944
|
+
stderr: "",
|
|
945
|
+
status: "failed",
|
|
946
|
+
error
|
|
947
|
+
};
|
|
948
|
+
this.executions.set(executionId, result);
|
|
949
|
+
return result;
|
|
950
|
+
}
|
|
951
|
+
logExecution(result, context) {
|
|
952
|
+
var _a;
|
|
953
|
+
const logEntry = {
|
|
954
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
955
|
+
executionId: result.executionId,
|
|
956
|
+
commandId: result.command.name,
|
|
957
|
+
commandLine: `${result.command.command} ${((_a = result.command.args) == null ? void 0 : _a.join(" ")) ?? ""}`.trim(),
|
|
958
|
+
status: result.status,
|
|
959
|
+
duration: result.duration,
|
|
960
|
+
exitCode: result.exitCode,
|
|
961
|
+
pid: result.pid,
|
|
962
|
+
cwd: result.command.cwd,
|
|
963
|
+
timeout: result.command.timeout,
|
|
964
|
+
context,
|
|
965
|
+
hasOutput: result.stdout.length > 0,
|
|
966
|
+
hasError: result.stderr.length > 0,
|
|
967
|
+
outputSize: result.stdout.length,
|
|
968
|
+
errorSize: result.stderr.length,
|
|
969
|
+
error: result.error,
|
|
970
|
+
userAgent: (context == null ? void 0 : context.userId) ? `user:${context.userId}` : "system",
|
|
971
|
+
sessionId: context == null ? void 0 : context.sessionId,
|
|
972
|
+
requestId: context == null ? void 0 : context.requestId
|
|
973
|
+
};
|
|
974
|
+
console.log(`[CommandExecution] ${JSON.stringify(logEntry)}`);
|
|
975
|
+
this.writeAuditLog(logEntry, result);
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Write detailed audit log to file for compliance and debugging
|
|
979
|
+
*/
|
|
980
|
+
writeAuditLog(logEntry, result) {
|
|
981
|
+
var _a, _b, _c, _d;
|
|
982
|
+
try {
|
|
983
|
+
const auditDir = path.join(process.cwd(), ".agentflow", "audit");
|
|
984
|
+
const auditFile = path.join(auditDir, `command-executions-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.jsonl`);
|
|
985
|
+
if (!fs.existsSync(auditDir)) {
|
|
986
|
+
fs.mkdirSync(auditDir, { recursive: true });
|
|
987
|
+
}
|
|
988
|
+
const auditEntry = {
|
|
989
|
+
...logEntry,
|
|
990
|
+
// Additional audit fields
|
|
991
|
+
startedAt: new Date(result.startedAt).toISOString(),
|
|
992
|
+
completedAt: result.completedAt ? new Date(result.completedAt).toISOString() : null,
|
|
993
|
+
commandArgs: result.command.args,
|
|
994
|
+
allowConcurrent: result.command.allowConcurrent,
|
|
995
|
+
category: result.command.category,
|
|
996
|
+
description: result.command.description,
|
|
997
|
+
// Include first/last lines of output for audit trail
|
|
998
|
+
outputPreview: result.stdout ? {
|
|
999
|
+
firstLine: ((_a = result.stdout.split("\n")[0]) == null ? void 0 : _a.slice(0, 200)) || "",
|
|
1000
|
+
lastLine: ((_b = result.stdout.split("\n").slice(-1)[0]) == null ? void 0 : _b.slice(0, 200)) || "",
|
|
1001
|
+
totalLines: result.stdout.split("\n").length
|
|
1002
|
+
} : null,
|
|
1003
|
+
errorPreview: result.stderr ? {
|
|
1004
|
+
firstLine: ((_c = result.stderr.split("\n")[0]) == null ? void 0 : _c.slice(0, 200)) || "",
|
|
1005
|
+
lastLine: ((_d = result.stderr.split("\n").slice(-1)[0]) == null ? void 0 : _d.slice(0, 200)) || "",
|
|
1006
|
+
totalLines: result.stderr.split("\n").length
|
|
1007
|
+
} : null
|
|
1008
|
+
};
|
|
1009
|
+
fs.appendFileSync(auditFile, JSON.stringify(auditEntry) + "\n", "utf-8");
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
console.warn(`[CommandExecutor] Failed to write audit log: ${error.message}`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Get audit trail for a specific execution
|
|
1016
|
+
*/
|
|
1017
|
+
getAuditTrail(executionId) {
|
|
1018
|
+
try {
|
|
1019
|
+
const auditDir = path.join(process.cwd(), ".agentflow", "audit");
|
|
1020
|
+
const auditEntries = [];
|
|
1021
|
+
if (!fs.existsSync(auditDir)) {
|
|
1022
|
+
return auditEntries;
|
|
1023
|
+
}
|
|
1024
|
+
const files = fs.readdirSync(auditDir).filter((f) => f.startsWith("command-executions-") && f.endsWith(".jsonl")).sort().slice(-7);
|
|
1025
|
+
for (const file of files) {
|
|
1026
|
+
const filePath = path.join(auditDir, file);
|
|
1027
|
+
try {
|
|
1028
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1029
|
+
const lines = content.trim().split("\n").filter((line) => line.trim());
|
|
1030
|
+
for (const line of lines) {
|
|
1031
|
+
try {
|
|
1032
|
+
const entry = JSON.parse(line);
|
|
1033
|
+
if (entry.executionId === executionId) {
|
|
1034
|
+
auditEntries.push(entry);
|
|
1035
|
+
}
|
|
1036
|
+
} catch {
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
} catch {
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return auditEntries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
console.warn(`[CommandExecutor] Failed to get audit trail: ${error.message}`);
|
|
1045
|
+
return [];
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Get audit statistics
|
|
1050
|
+
*/
|
|
1051
|
+
getAuditStats(days = 7) {
|
|
1052
|
+
try {
|
|
1053
|
+
const auditDir = path.join(process.cwd(), ".agentflow", "audit");
|
|
1054
|
+
const stats = {
|
|
1055
|
+
totalExecutions: 0,
|
|
1056
|
+
successfulExecutions: 0,
|
|
1057
|
+
failedExecutions: 0,
|
|
1058
|
+
averageDuration: 0,
|
|
1059
|
+
commandFrequency: {},
|
|
1060
|
+
userActivity: {}
|
|
1061
|
+
};
|
|
1062
|
+
if (!fs.existsSync(auditDir)) {
|
|
1063
|
+
return stats;
|
|
1064
|
+
}
|
|
1065
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
1066
|
+
let totalDuration = 0;
|
|
1067
|
+
const files = fs.readdirSync(auditDir).filter((f) => f.startsWith("command-executions-") && f.endsWith(".jsonl")).sort().slice(-days);
|
|
1068
|
+
for (const file of files) {
|
|
1069
|
+
const filePath = path.join(auditDir, file);
|
|
1070
|
+
try {
|
|
1071
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1072
|
+
const lines = content.trim().split("\n").filter((line) => line.trim());
|
|
1073
|
+
for (const line of lines) {
|
|
1074
|
+
try {
|
|
1075
|
+
const entry = JSON.parse(line);
|
|
1076
|
+
const entryTime = new Date(entry.timestamp).getTime();
|
|
1077
|
+
if (entryTime < cutoff) continue;
|
|
1078
|
+
stats.totalExecutions++;
|
|
1079
|
+
if (entry.status === "completed") {
|
|
1080
|
+
stats.successfulExecutions++;
|
|
1081
|
+
} else if (["failed", "timeout", "killed"].includes(entry.status)) {
|
|
1082
|
+
stats.failedExecutions++;
|
|
1083
|
+
}
|
|
1084
|
+
if (entry.duration) {
|
|
1085
|
+
totalDuration += entry.duration;
|
|
1086
|
+
}
|
|
1087
|
+
const cmd = entry.commandId || "unknown";
|
|
1088
|
+
stats.commandFrequency[cmd] = (stats.commandFrequency[cmd] || 0) + 1;
|
|
1089
|
+
const user = entry.userAgent || "unknown";
|
|
1090
|
+
stats.userActivity[user] = (stats.userActivity[user] || 0) + 1;
|
|
1091
|
+
} catch {
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
} catch {
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
stats.averageDuration = stats.totalExecutions > 0 ? totalDuration / stats.totalExecutions : 0;
|
|
1098
|
+
return stats;
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
console.warn(`[CommandExecutor] Failed to get audit stats: ${error.message}`);
|
|
1101
|
+
return {
|
|
1102
|
+
totalExecutions: 0,
|
|
1103
|
+
successfulExecutions: 0,
|
|
1104
|
+
failedExecutions: 0,
|
|
1105
|
+
averageDuration: 0,
|
|
1106
|
+
commandFrequency: {},
|
|
1107
|
+
userActivity: {}
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
function createCommandExecutor(config, options) {
|
|
1113
|
+
return new CommandExecutor(config, options);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
522
1116
|
// src/stats.ts
|
|
523
1117
|
import { getFailures, getHungNodes, getStats } from "agentflow-core";
|
|
524
1118
|
var AgentStats = class {
|
|
525
1119
|
agentMetrics = /* @__PURE__ */ new Map();
|
|
526
1120
|
processedTraces = /* @__PURE__ */ new Set();
|
|
527
1121
|
processTrace(trace) {
|
|
528
|
-
const traceKey = `${trace.filename || trace.
|
|
1122
|
+
const traceKey = `${trace.agentId}#${trace.filename || trace.id}-${trace.startTime}`;
|
|
529
1123
|
if (this.processedTraces.has(traceKey)) {
|
|
530
1124
|
return;
|
|
531
1125
|
}
|
|
@@ -696,8 +1290,8 @@ var AgentStats = class {
|
|
|
696
1290
|
|
|
697
1291
|
// src/watcher.ts
|
|
698
1292
|
import { EventEmitter } from "events";
|
|
699
|
-
import * as
|
|
700
|
-
import * as
|
|
1293
|
+
import * as fs2 from "fs";
|
|
1294
|
+
import * as path2 from "path";
|
|
701
1295
|
import { loadGraph } from "agentflow-core";
|
|
702
1296
|
import chokidar from "chokidar";
|
|
703
1297
|
|
|
@@ -745,13 +1339,19 @@ function extractKeyValuePairs(line) {
|
|
|
745
1339
|
const kvRegex = /(\w+)=('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\S+)/g;
|
|
746
1340
|
let match;
|
|
747
1341
|
while ((match = kvRegex.exec(clean)) !== null) {
|
|
748
|
-
|
|
749
|
-
|
|
1342
|
+
const key = match[1];
|
|
1343
|
+
const value = match[2];
|
|
1344
|
+
if (!key || !value) continue;
|
|
1345
|
+
if (key === "Z" || key === "m") continue;
|
|
1346
|
+
pairs[key] = parseValue(value);
|
|
750
1347
|
}
|
|
751
1348
|
return pairs;
|
|
752
1349
|
}
|
|
753
1350
|
function detectComponent(action, kvPairs) {
|
|
754
|
-
if (action.includes("."))
|
|
1351
|
+
if (action.includes(".")) {
|
|
1352
|
+
const parts = action.split(".");
|
|
1353
|
+
return parts[0] || action;
|
|
1354
|
+
}
|
|
755
1355
|
if (kvPairs.component) return String(kvPairs.component);
|
|
756
1356
|
if (kvPairs.service) return String(kvPairs.service);
|
|
757
1357
|
if (kvPairs.module) return String(kvPairs.module);
|
|
@@ -789,7 +1389,9 @@ function detectActivityPattern(line) {
|
|
|
789
1389
|
const pairs = {};
|
|
790
1390
|
for (const m of kvMatches) {
|
|
791
1391
|
const [key, value] = m.split("=", 2);
|
|
792
|
-
|
|
1392
|
+
if (key && value !== void 0) {
|
|
1393
|
+
pairs[key] = parseValue(value);
|
|
1394
|
+
}
|
|
793
1395
|
}
|
|
794
1396
|
timestamp = parseTimestamp(pairs.timestamp || pairs.time) || Date.now();
|
|
795
1397
|
level = String(pairs.level || "info");
|
|
@@ -801,7 +1403,7 @@ function detectActivityPattern(line) {
|
|
|
801
1403
|
const logMatch = line.match(
|
|
802
1404
|
/^(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)\s+(\w+)?\s*:?\s*(.+)/
|
|
803
1405
|
);
|
|
804
|
-
if (logMatch) {
|
|
1406
|
+
if (logMatch && logMatch[1]) {
|
|
805
1407
|
timestamp = new Date(logMatch[1]).getTime();
|
|
806
1408
|
level = logMatch[2] || "info";
|
|
807
1409
|
action = logMatch[3] || "";
|
|
@@ -839,11 +1441,38 @@ function getUniversalNodeStatus(activity) {
|
|
|
839
1441
|
if (op.match(/complete|finish|end|done/i)) return "completed";
|
|
840
1442
|
return "completed";
|
|
841
1443
|
}
|
|
842
|
-
function openClawSessionIdToAgent(sessionId) {
|
|
1444
|
+
function openClawSessionIdToAgent(sessionId, lookupMap) {
|
|
1445
|
+
if (lookupMap == null ? void 0 : lookupMap.has(sessionId)) {
|
|
1446
|
+
return lookupMap.get(sessionId);
|
|
1447
|
+
}
|
|
843
1448
|
const firstSegment = sessionId.split("-")[0];
|
|
844
1449
|
if (firstSegment) return firstSegment;
|
|
845
1450
|
return "openclaw";
|
|
846
1451
|
}
|
|
1452
|
+
function parseOpenClawSessionKey(key) {
|
|
1453
|
+
const parts = key.split(":");
|
|
1454
|
+
if (parts.length < 3 || parts[0] !== "agent") return null;
|
|
1455
|
+
const agentName = parts[1];
|
|
1456
|
+
const kind = parts[2];
|
|
1457
|
+
if (parts.length === 3 && kind === agentName) {
|
|
1458
|
+
return `openclaw:${agentName}`;
|
|
1459
|
+
}
|
|
1460
|
+
if (kind === "cron" && parts.length >= 4) {
|
|
1461
|
+
const runIdx = parts.indexOf("run", 4);
|
|
1462
|
+
const jobParts = runIdx > 0 ? parts.slice(3, runIdx) : parts.slice(3);
|
|
1463
|
+
const jobId = jobParts.join("-");
|
|
1464
|
+
return jobId ? `openclaw:${jobId}` : null;
|
|
1465
|
+
}
|
|
1466
|
+
if (parts.length >= 5) {
|
|
1467
|
+
const target = parts[4];
|
|
1468
|
+
return `openclaw:${kind}:${target}`;
|
|
1469
|
+
}
|
|
1470
|
+
if (parts.length >= 4) {
|
|
1471
|
+
const target = parts[3];
|
|
1472
|
+
return `openclaw:${kind}:${target}`;
|
|
1473
|
+
}
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
847
1476
|
|
|
848
1477
|
// src/watcher.ts
|
|
849
1478
|
var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
@@ -854,19 +1483,21 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
854
1483
|
allWatchDirs;
|
|
855
1484
|
maxAgeMs;
|
|
856
1485
|
userConfig;
|
|
1486
|
+
/** Maps OpenClaw session UUIDs to resolved agentIds (populated from sessions.json) */
|
|
1487
|
+
sessionAgentMap = /* @__PURE__ */ new Map();
|
|
857
1488
|
constructor(tracesDirOrOptions) {
|
|
858
1489
|
super();
|
|
859
1490
|
const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
|
|
860
1491
|
const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
|
|
861
1492
|
const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
|
|
862
1493
|
if (typeof tracesDirOrOptions === "string") {
|
|
863
|
-
this.tracesDir =
|
|
1494
|
+
this.tracesDir = path2.resolve(tracesDirOrOptions);
|
|
864
1495
|
this.dataDirs = [];
|
|
865
1496
|
this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
|
|
866
1497
|
this.userConfig = {};
|
|
867
1498
|
} else {
|
|
868
|
-
this.tracesDir =
|
|
869
|
-
this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) =>
|
|
1499
|
+
this.tracesDir = path2.resolve(tracesDirOrOptions.tracesDir);
|
|
1500
|
+
this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path2.resolve(d));
|
|
870
1501
|
this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
|
|
871
1502
|
this.userConfig = tracesDirOrOptions.userConfig ?? {};
|
|
872
1503
|
}
|
|
@@ -876,7 +1507,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
876
1507
|
]);
|
|
877
1508
|
this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
|
|
878
1509
|
this.allWatchDirs = [
|
|
879
|
-
...new Set([this.tracesDir, ...this.dataDirs].map((d) =>
|
|
1510
|
+
...new Set([this.tracesDir, ...this.dataDirs].map((d) => path2.resolve(d)))
|
|
880
1511
|
];
|
|
881
1512
|
this.ensureTracesDir();
|
|
882
1513
|
this.loadExistingFiles();
|
|
@@ -889,7 +1520,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
889
1520
|
const cutoff = Date.now() - this.maxAgeMs;
|
|
890
1521
|
const _archived = 0;
|
|
891
1522
|
for (const dir of this.allWatchDirs) {
|
|
892
|
-
if (!
|
|
1523
|
+
if (!fs2.existsSync(dir)) continue;
|
|
893
1524
|
try {
|
|
894
1525
|
this.archiveDirectory(dir, cutoff, 0);
|
|
895
1526
|
} catch (error) {
|
|
@@ -899,28 +1530,28 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
899
1530
|
}
|
|
900
1531
|
archiveDirectory(dir, cutoff, depth) {
|
|
901
1532
|
if (depth > 10) return 0;
|
|
902
|
-
if (
|
|
1533
|
+
if (path2.basename(dir) === "archive") return 0;
|
|
903
1534
|
let archived = 0;
|
|
904
1535
|
try {
|
|
905
|
-
const entries =
|
|
1536
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
906
1537
|
for (const entry of entries) {
|
|
907
1538
|
if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name))
|
|
908
1539
|
continue;
|
|
909
|
-
const fullPath =
|
|
1540
|
+
const fullPath = path2.join(dir, entry.name);
|
|
910
1541
|
if (entry.isDirectory()) {
|
|
911
1542
|
archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
|
|
912
1543
|
continue;
|
|
913
1544
|
}
|
|
914
1545
|
if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
|
|
915
1546
|
try {
|
|
916
|
-
const stats =
|
|
1547
|
+
const stats = fs2.statSync(fullPath);
|
|
917
1548
|
if (stats.mtimeMs >= cutoff) continue;
|
|
918
1549
|
const mtime = new Date(stats.mtimeMs);
|
|
919
1550
|
const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
|
|
920
|
-
const archiveDir =
|
|
921
|
-
|
|
922
|
-
const dest =
|
|
923
|
-
|
|
1551
|
+
const archiveDir = path2.join(this.tracesDir, "archive", yearMonth);
|
|
1552
|
+
fs2.mkdirSync(archiveDir, { recursive: true });
|
|
1553
|
+
const dest = path2.join(archiveDir, entry.name);
|
|
1554
|
+
fs2.renameSync(fullPath, dest);
|
|
924
1555
|
const key = this.traceKey(fullPath);
|
|
925
1556
|
this.traces.delete(key);
|
|
926
1557
|
archived++;
|
|
@@ -932,16 +1563,19 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
932
1563
|
return archived;
|
|
933
1564
|
}
|
|
934
1565
|
ensureTracesDir() {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1566
|
+
try {
|
|
1567
|
+
if (!fs2.existsSync(this.tracesDir)) {
|
|
1568
|
+
fs2.mkdirSync(this.tracesDir, { recursive: true });
|
|
1569
|
+
console.log(`Created traces directory: ${this.tracesDir}`);
|
|
1570
|
+
}
|
|
1571
|
+
} catch {
|
|
938
1572
|
}
|
|
939
1573
|
}
|
|
940
1574
|
loadExistingFiles() {
|
|
941
1575
|
let totalFiles = 0;
|
|
942
1576
|
let totalDirectories = 0;
|
|
943
1577
|
for (const dir of this.allWatchDirs) {
|
|
944
|
-
if (!
|
|
1578
|
+
if (!fs2.existsSync(dir)) continue;
|
|
945
1579
|
try {
|
|
946
1580
|
totalDirectories++;
|
|
947
1581
|
const loadedFiles = this.scanDirectoryRecursive(dir);
|
|
@@ -959,16 +1593,16 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
959
1593
|
if (depth > 10) return 0;
|
|
960
1594
|
let fileCount = 0;
|
|
961
1595
|
try {
|
|
962
|
-
const entries =
|
|
1596
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
963
1597
|
for (const entry of entries) {
|
|
964
1598
|
if (entry.name.startsWith(".")) continue;
|
|
965
1599
|
if (entry.name === "archive") continue;
|
|
966
1600
|
if (this.userSkipDirs.has(entry.name)) continue;
|
|
967
|
-
const fullPath =
|
|
1601
|
+
const fullPath = path2.join(dir, entry.name);
|
|
968
1602
|
if (entry.isFile()) {
|
|
969
1603
|
if (this.isSupportedFile(entry.name)) {
|
|
970
1604
|
try {
|
|
971
|
-
const mtime =
|
|
1605
|
+
const mtime = fs2.statSync(fullPath).mtimeMs;
|
|
972
1606
|
if (Date.now() - mtime > this.maxAgeMs) continue;
|
|
973
1607
|
} catch {
|
|
974
1608
|
continue;
|
|
@@ -1020,7 +1654,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1020
1654
|
];
|
|
1021
1655
|
/** Load a file using the adapter registry, falling back to built-in parsing. */
|
|
1022
1656
|
loadFile(filePath) {
|
|
1023
|
-
const filename =
|
|
1657
|
+
const filename = path2.basename(filePath);
|
|
1024
1658
|
if (this.skipFiles.has(filename)) return false;
|
|
1025
1659
|
if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
|
|
1026
1660
|
const adapter = findAdapter(filePath);
|
|
@@ -1073,9 +1707,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1073
1707
|
metadata: { ...trace.metadata, adapterSource: adapterName },
|
|
1074
1708
|
sessionEvents: trace.sessionEvents ?? [],
|
|
1075
1709
|
sourceType: "session",
|
|
1076
|
-
filename:
|
|
1710
|
+
filename: path2.basename(filePath),
|
|
1077
1711
|
lastModified: Date.now(),
|
|
1078
|
-
sourceDir:
|
|
1712
|
+
sourceDir: path2.dirname(filePath)
|
|
1079
1713
|
};
|
|
1080
1714
|
const key = `${adapterName}:${trace.id}`;
|
|
1081
1715
|
this.traces.set(key, watched);
|
|
@@ -1088,9 +1722,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1088
1722
|
}
|
|
1089
1723
|
loadLogFile(filePath) {
|
|
1090
1724
|
try {
|
|
1091
|
-
const content =
|
|
1092
|
-
const filename =
|
|
1093
|
-
const stats =
|
|
1725
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
1726
|
+
const filename = path2.basename(filePath);
|
|
1727
|
+
const stats = fs2.statSync(filePath);
|
|
1094
1728
|
if (filename.startsWith("openclaw-") || filePath.includes("openclaw")) {
|
|
1095
1729
|
const result = this.loadOpenClawLogFile(content, filename, filePath, stats);
|
|
1096
1730
|
if (result) return true;
|
|
@@ -1108,8 +1742,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1108
1742
|
trace.filename = filename;
|
|
1109
1743
|
trace.lastModified = stats.mtime.getTime();
|
|
1110
1744
|
trace.sourceType = trace.sourceType || "trace";
|
|
1111
|
-
trace.sourceDir =
|
|
1112
|
-
const
|
|
1745
|
+
trace.sourceDir = path2.dirname(filePath);
|
|
1746
|
+
const traceAgentId = trace.agentId;
|
|
1747
|
+
const key = traces.length === 1 ? this.traceKey(filePath, traceAgentId) : `${this.traceKey(filePath, traceAgentId)}-${i}`;
|
|
1113
1748
|
this.traces.set(key, trace);
|
|
1114
1749
|
}
|
|
1115
1750
|
return traces.length > 0;
|
|
@@ -1170,7 +1805,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1170
1805
|
trace.sourceType = "session";
|
|
1171
1806
|
}
|
|
1172
1807
|
if (traces.length === 0) {
|
|
1173
|
-
const stats =
|
|
1808
|
+
const stats = fs2.statSync(filePath);
|
|
1174
1809
|
traces.push({
|
|
1175
1810
|
id: "",
|
|
1176
1811
|
rootNodeId: "root",
|
|
@@ -1210,10 +1845,10 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1210
1845
|
const pathAgent = this.extractAgentFromPath(filePath);
|
|
1211
1846
|
const detection = getAgentDetection(this.userConfig);
|
|
1212
1847
|
if (detection.filePatterns) {
|
|
1213
|
-
const
|
|
1848
|
+
const basename4 = path2.basename(filePath, path2.extname(filePath));
|
|
1214
1849
|
for (const [pattern, template] of Object.entries(detection.filePatterns)) {
|
|
1215
1850
|
const re = new RegExp(`^(${pattern})$`);
|
|
1216
|
-
const match =
|
|
1851
|
+
const match = basename4.match(re);
|
|
1217
1852
|
if (match) {
|
|
1218
1853
|
const resolved = template.replace("${match}", match[1]);
|
|
1219
1854
|
return this.normaliseAgentId(resolved);
|
|
@@ -1223,8 +1858,8 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1223
1858
|
return this.normaliseAgentId(pathAgent);
|
|
1224
1859
|
}
|
|
1225
1860
|
extractAgentFromPath(filePath) {
|
|
1226
|
-
const filename =
|
|
1227
|
-
const pathParts = filePath.split(
|
|
1861
|
+
const filename = path2.basename(filePath, path2.extname(filePath));
|
|
1862
|
+
const pathParts = filePath.split(path2.sep);
|
|
1228
1863
|
const detection = getAgentDetection(this.userConfig);
|
|
1229
1864
|
let pathPrefix = "";
|
|
1230
1865
|
if (detection.pathPatterns) {
|
|
@@ -1295,7 +1930,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1295
1930
|
if ((inner == null ? void 0 : inner.payloads) && ((_a = inner == null ? void 0 : inner.meta) == null ? void 0 : _a.agentMeta)) {
|
|
1296
1931
|
const agentMeta = inner.meta.agentMeta;
|
|
1297
1932
|
const sessionId = agentMeta.sessionId || "unknown";
|
|
1298
|
-
const agentName = openClawSessionIdToAgent(sessionId);
|
|
1933
|
+
const agentName = openClawSessionIdToAgent(sessionId, this.sessionAgentMap);
|
|
1299
1934
|
const timestamp = parsed.time ? new Date(parsed.time).getTime() : ((_b = parsed._meta) == null ? void 0 : _b.date) ? new Date(parsed._meta.date).getTime() : stats.mtime.getTime();
|
|
1300
1935
|
const texts = (inner.payloads || []).map((p) => p.text || "").filter(Boolean);
|
|
1301
1936
|
if (!sessions.has(sessionId)) {
|
|
@@ -1319,7 +1954,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1319
1954
|
if (parsed.payloads && ((_d = parsed.meta) == null ? void 0 : _d.agentMeta)) {
|
|
1320
1955
|
const agentMeta = parsed.meta.agentMeta;
|
|
1321
1956
|
const sessionId = agentMeta.sessionId || "unknown";
|
|
1322
|
-
const agentName = openClawSessionIdToAgent(sessionId);
|
|
1957
|
+
const agentName = openClawSessionIdToAgent(sessionId, this.sessionAgentMap);
|
|
1323
1958
|
const timestamp = parsed.time ? new Date(parsed.time).getTime() : ((_e = parsed._meta) == null ? void 0 : _e.date) ? new Date(parsed._meta.date).getTime() : stats.mtime.getTime();
|
|
1324
1959
|
const texts = (parsed.payloads || []).map((p) => p.text || "").filter(Boolean);
|
|
1325
1960
|
if (!sessions.has(sessionId)) {
|
|
@@ -1427,7 +2062,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1427
2062
|
filename,
|
|
1428
2063
|
lastModified: stats.mtime.getTime(),
|
|
1429
2064
|
sourceType: "session",
|
|
1430
|
-
sourceDir:
|
|
2065
|
+
sourceDir: path2.dirname(filePath),
|
|
1431
2066
|
sessionEvents,
|
|
1432
2067
|
tokenUsage: {
|
|
1433
2068
|
input: totalInput,
|
|
@@ -1442,7 +2077,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1442
2077
|
source: "openclaw-log"
|
|
1443
2078
|
}
|
|
1444
2079
|
};
|
|
1445
|
-
const key = sessions.size === 1 ? this.traceKey(filePath) : `${this.traceKey(filePath)}-${traceIndex}`;
|
|
2080
|
+
const key = sessions.size === 1 ? this.traceKey(filePath, agentId) : `${this.traceKey(filePath, agentId)}-${traceIndex}`;
|
|
1446
2081
|
this.traces.set(key, trace);
|
|
1447
2082
|
traceIndex++;
|
|
1448
2083
|
}
|
|
@@ -1450,23 +2085,26 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1450
2085
|
}
|
|
1451
2086
|
loadTraceFile(filePath) {
|
|
1452
2087
|
try {
|
|
1453
|
-
const content =
|
|
1454
|
-
const filename =
|
|
2088
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
2089
|
+
const filename = path2.basename(filePath);
|
|
1455
2090
|
if (filename === "sessions.json") {
|
|
1456
2091
|
return this.loadSessionsIndex(filePath, content);
|
|
1457
2092
|
}
|
|
1458
2093
|
const graph = loadGraph(content);
|
|
1459
|
-
const stats =
|
|
2094
|
+
const stats = fs2.statSync(filePath);
|
|
1460
2095
|
graph.filename = filename;
|
|
1461
2096
|
graph.lastModified = stats.mtime.getTime();
|
|
1462
2097
|
graph.sourceType = "trace";
|
|
1463
|
-
graph.sourceDir =
|
|
2098
|
+
graph.sourceDir = path2.dirname(filePath);
|
|
2099
|
+
if (!graph.agentId || graph.agentId === "unknown") {
|
|
2100
|
+
graph.agentId = this.extractAgentFromPath(filePath);
|
|
2101
|
+
}
|
|
1464
2102
|
if (graph.nodes instanceof Map) {
|
|
1465
2103
|
for (const node of graph.nodes.values()) {
|
|
1466
2104
|
if (!node.children) node.children = [];
|
|
1467
2105
|
}
|
|
1468
2106
|
}
|
|
1469
|
-
this.traces.set(this.traceKey(filePath), graph);
|
|
2107
|
+
this.traces.set(this.traceKey(filePath, graph.agentId), graph);
|
|
1470
2108
|
return true;
|
|
1471
2109
|
} catch {
|
|
1472
2110
|
return false;
|
|
@@ -1474,11 +2112,12 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1474
2112
|
}
|
|
1475
2113
|
/** Parse sessions.json index to discover agents and their sessions. */
|
|
1476
2114
|
loadSessionsIndex(filePath, content) {
|
|
2115
|
+
var _a;
|
|
1477
2116
|
try {
|
|
1478
2117
|
const data = JSON.parse(content);
|
|
1479
2118
|
if (typeof data !== "object" || data === null) return false;
|
|
1480
|
-
const stats =
|
|
1481
|
-
const pathParts = filePath.split(
|
|
2119
|
+
const stats = fs2.statSync(filePath);
|
|
2120
|
+
const pathParts = filePath.split(path2.sep);
|
|
1482
2121
|
const agentsIndex = pathParts.lastIndexOf("agents");
|
|
1483
2122
|
if (agentsIndex === -1 || agentsIndex + 1 >= pathParts.length) return false;
|
|
1484
2123
|
const agentName = pathParts[agentsIndex + 1];
|
|
@@ -1489,11 +2128,19 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1489
2128
|
const session = sessionData;
|
|
1490
2129
|
const sessionId = session.sessionId;
|
|
1491
2130
|
if (!sessionId) continue;
|
|
2131
|
+
const resolvedAgentId = parseOpenClawSessionKey(sessionKey) ?? agentId;
|
|
2132
|
+
this.sessionAgentMap.set(String(sessionId), resolvedAgentId);
|
|
1492
2133
|
const existingKey = Array.from(this.traces.keys()).find((k) => {
|
|
1493
2134
|
const t = this.traces.get(k);
|
|
1494
2135
|
return (t == null ? void 0 : t.id) === sessionId || (t == null ? void 0 : t.traceId) === sessionId;
|
|
1495
2136
|
});
|
|
1496
|
-
if (existingKey)
|
|
2137
|
+
if (existingKey) {
|
|
2138
|
+
const existing = this.traces.get(existingKey);
|
|
2139
|
+
if (existing && (existing.agentId === agentId || ((_a = existing.agentId) == null ? void 0 : _a.startsWith("openclaw-main")))) {
|
|
2140
|
+
existing.agentId = resolvedAgentId;
|
|
2141
|
+
}
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
1497
2144
|
const updatedAt = session.updatedAt || stats.mtime.getTime();
|
|
1498
2145
|
const label = session.label || sessionKey.split(":").pop() || sessionId;
|
|
1499
2146
|
const chatType = session.chatType || (sessionKey.includes("cron") ? "cron" : "direct");
|
|
@@ -1522,7 +2169,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1522
2169
|
edges: [],
|
|
1523
2170
|
events: [],
|
|
1524
2171
|
startTime: updatedAt,
|
|
1525
|
-
agentId,
|
|
2172
|
+
agentId: resolvedAgentId,
|
|
1526
2173
|
trigger,
|
|
1527
2174
|
name: label,
|
|
1528
2175
|
traceId: sessionId,
|
|
@@ -1530,7 +2177,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1530
2177
|
filename: `${agentName}-${sessionId.slice(0, 8)}.index`,
|
|
1531
2178
|
lastModified: updatedAt,
|
|
1532
2179
|
sourceType: "session",
|
|
1533
|
-
sourceDir:
|
|
2180
|
+
sourceDir: path2.dirname(filePath),
|
|
1534
2181
|
sessionEvents: [],
|
|
1535
2182
|
tokenUsage: { input: 0, output: 0, total: 0, cost: 0 },
|
|
1536
2183
|
metadata: {
|
|
@@ -1540,7 +2187,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1540
2187
|
agentName
|
|
1541
2188
|
}
|
|
1542
2189
|
};
|
|
1543
|
-
const key = `${this.traceKey(filePath)}-${sessionId.slice(0, 12)}`;
|
|
2190
|
+
const key = `${this.traceKey(filePath, agentId)}-${sessionId.slice(0, 12)}`;
|
|
1544
2191
|
this.traces.set(key, trace);
|
|
1545
2192
|
loaded++;
|
|
1546
2193
|
}
|
|
@@ -1553,7 +2200,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1553
2200
|
loadSessionFile(filePath) {
|
|
1554
2201
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
1555
2202
|
try {
|
|
1556
|
-
const content =
|
|
2203
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
1557
2204
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
1558
2205
|
if (lines.length === 0) return false;
|
|
1559
2206
|
const rawEvents = [];
|
|
@@ -1569,13 +2216,13 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1569
2216
|
return this.loadCronRunFile(rawEvents, filePath);
|
|
1570
2217
|
}
|
|
1571
2218
|
const sessionEvent = rawEvents.find((e) => e.type === "session");
|
|
1572
|
-
const sessionId = (sessionEvent == null ? void 0 : sessionEvent.id) ||
|
|
2219
|
+
const sessionId = (sessionEvent == null ? void 0 : sessionEvent.id) || path2.basename(filePath, ".jsonl");
|
|
1573
2220
|
const sessionTimestamp = (sessionEvent == null ? void 0 : sessionEvent.timestamp) || ((_a = rawEvents[0]) == null ? void 0 : _a.timestamp);
|
|
1574
2221
|
const startTime = sessionTimestamp ? new Date(sessionTimestamp).getTime() : 0;
|
|
1575
2222
|
if (!startTime) return false;
|
|
1576
|
-
const parentDir =
|
|
1577
|
-
const grandParentDir =
|
|
1578
|
-
const greatGrandParentDir =
|
|
2223
|
+
const parentDir = path2.basename(path2.dirname(filePath));
|
|
2224
|
+
const grandParentDir = path2.basename(path2.dirname(path2.dirname(filePath)));
|
|
2225
|
+
const greatGrandParentDir = path2.basename(path2.dirname(path2.dirname(path2.dirname(filePath))));
|
|
1579
2226
|
let agentName;
|
|
1580
2227
|
if (parentDir === "sessions" && greatGrandParentDir === "agents") {
|
|
1581
2228
|
agentName = grandParentDir;
|
|
@@ -1594,6 +2241,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1594
2241
|
}
|
|
1595
2242
|
}
|
|
1596
2243
|
}
|
|
2244
|
+
if (this.sessionAgentMap.has(sessionId)) {
|
|
2245
|
+
agentId = this.sessionAgentMap.get(sessionId);
|
|
2246
|
+
}
|
|
1597
2247
|
const modelEvent = rawEvents.find((e) => e.type === "model_change");
|
|
1598
2248
|
const provider = (modelEvent == null ? void 0 : modelEvent.provider) || "";
|
|
1599
2249
|
const modelId = (modelEvent == null ? void 0 : modelEvent.modelId) || "";
|
|
@@ -1797,7 +2447,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1797
2447
|
if (role === "toolResult") {
|
|
1798
2448
|
const toolCallId = ((_j = contentBlocks[0]) == null ? void 0 : _j.toolCallId) || evt.parentId;
|
|
1799
2449
|
const resultContent = contentBlocks.map((b) => b.text || b.content || "").join("\n");
|
|
1800
|
-
const hasError = contentBlocks.some(
|
|
2450
|
+
const hasError = contentBlocks.some(
|
|
2451
|
+
(b) => b.isError || b.error
|
|
2452
|
+
);
|
|
1801
2453
|
const errorText = hasError ? resultContent : void 0;
|
|
1802
2454
|
sessionEvents.push({
|
|
1803
2455
|
type: "tool_result",
|
|
@@ -1825,7 +2477,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1825
2477
|
}
|
|
1826
2478
|
}
|
|
1827
2479
|
}
|
|
1828
|
-
const fileStat =
|
|
2480
|
+
const fileStat = fs2.statSync(filePath);
|
|
1829
2481
|
const fileAge = Date.now() - fileStat.mtime.getTime();
|
|
1830
2482
|
const lastEvt = rawEvents[rawEvents.length - 1];
|
|
1831
2483
|
const hasToolError = sessionEvents.some((e) => e.type === "tool_result" && e.toolError);
|
|
@@ -1873,7 +2525,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1873
2525
|
"gen_ai.request.model": modelId
|
|
1874
2526
|
}
|
|
1875
2527
|
});
|
|
1876
|
-
const filename =
|
|
2528
|
+
const filename = path2.basename(filePath);
|
|
1877
2529
|
const trace = {
|
|
1878
2530
|
id: sessionId,
|
|
1879
2531
|
nodes,
|
|
@@ -1889,7 +2541,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1889
2541
|
filename,
|
|
1890
2542
|
lastModified: fileStat.mtime.getTime(),
|
|
1891
2543
|
sourceType: "session",
|
|
1892
|
-
sourceDir:
|
|
2544
|
+
sourceDir: path2.dirname(filePath),
|
|
1893
2545
|
sessionEvents,
|
|
1894
2546
|
tokenUsage,
|
|
1895
2547
|
metadata: {
|
|
@@ -1903,93 +2555,180 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1903
2555
|
sessionVersion: sessionEvent == null ? void 0 : sessionEvent.version
|
|
1904
2556
|
}
|
|
1905
2557
|
};
|
|
1906
|
-
this.traces.set(this.traceKey(filePath), trace);
|
|
2558
|
+
this.traces.set(this.traceKey(filePath, agentId), trace);
|
|
1907
2559
|
return true;
|
|
1908
2560
|
} catch {
|
|
1909
2561
|
return false;
|
|
1910
2562
|
}
|
|
1911
2563
|
}
|
|
1912
|
-
/** Parse cron run JSONL files
|
|
2564
|
+
/** Parse cron run JSONL files — creates one trace per execution run. */
|
|
1913
2565
|
loadCronRunFile(rawEvents, filePath) {
|
|
1914
|
-
var _a, _b, _c;
|
|
2566
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1915
2567
|
try {
|
|
1916
|
-
const filename =
|
|
1917
|
-
const
|
|
1918
|
-
const fileStat =
|
|
1919
|
-
const
|
|
1920
|
-
let
|
|
2568
|
+
const filename = path2.basename(filePath);
|
|
2569
|
+
const fileJobId = String(((_a = rawEvents[0]) == null ? void 0 : _a.jobId) || path2.basename(filePath, ".jsonl"));
|
|
2570
|
+
const fileStat = fs2.statSync(filePath);
|
|
2571
|
+
const agentId = `openclaw:${fileJobId}`;
|
|
2572
|
+
let loaded = 0;
|
|
1921
2573
|
for (const evt of rawEvents) {
|
|
1922
2574
|
const ts = evt.ts || Date.now();
|
|
1923
|
-
const
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2575
|
+
const runAtMs = evt.runAtMs || ts;
|
|
2576
|
+
const durationMs = evt.durationMs || 0;
|
|
2577
|
+
const status = evt.status === "ok" ? "completed" : "failed";
|
|
2578
|
+
const sessionId = String(evt.sessionId || `${fileJobId}-${ts}`);
|
|
2579
|
+
const summary = String(evt.summary || evt.error || "");
|
|
2580
|
+
const model = String(evt.model || "");
|
|
2581
|
+
const usage = evt.usage || {};
|
|
2582
|
+
const rootId = `cron-${sessionId.slice(0, 12)}`;
|
|
2583
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
2584
|
+
const children = [];
|
|
2585
|
+
nodes.set(rootId, {
|
|
2586
|
+
id: rootId,
|
|
2587
|
+
type: "agent",
|
|
2588
|
+
name: `${fileJobId} run`,
|
|
2589
|
+
startTime: runAtMs,
|
|
2590
|
+
endTime: runAtMs + durationMs,
|
|
2591
|
+
status,
|
|
2592
|
+
parentId: void 0,
|
|
2593
|
+
children,
|
|
2594
|
+
metadata: {
|
|
2595
|
+
jobId: fileJobId,
|
|
2596
|
+
model,
|
|
2597
|
+
sessionId,
|
|
2598
|
+
durationMs
|
|
2599
|
+
}
|
|
1932
2600
|
});
|
|
2601
|
+
let stepIdx = 0;
|
|
2602
|
+
const tablePattern = /\|\s*\*?\*?(\d+)\.\s*\*?\*?([^|]+)\*?\*?\s*\|\s*([^|]+)\s*\|\s*([^|]*)\|/g;
|
|
2603
|
+
let m;
|
|
2604
|
+
while ((m = tablePattern.exec(summary)) !== null) {
|
|
2605
|
+
stepIdx++;
|
|
2606
|
+
const stepName = ((_b = m[2]) == null ? void 0 : _b.trim().replace(/\*+/g, "")) || `Step ${stepIdx}`;
|
|
2607
|
+
const stepDetail = ((_c = m[4]) == null ? void 0 : _c.trim()) || "";
|
|
2608
|
+
const nodeId = `step-${stepIdx}`;
|
|
2609
|
+
children.push(nodeId);
|
|
2610
|
+
nodes.set(nodeId, {
|
|
2611
|
+
id: nodeId,
|
|
2612
|
+
type: "tool",
|
|
2613
|
+
name: stepName,
|
|
2614
|
+
startTime: runAtMs + (stepIdx - 1) * Math.floor(durationMs / Math.max(1, stepIdx + 1)),
|
|
2615
|
+
endTime: runAtMs + stepIdx * Math.floor(durationMs / Math.max(1, stepIdx + 1)),
|
|
2616
|
+
status: ((_d = m[3]) == null ? void 0 : _d.includes("\u274C")) || ((_e = m[3]) == null ? void 0 : _e.includes("Fail")) ? "failed" : "completed",
|
|
2617
|
+
parentId: rootId,
|
|
2618
|
+
children: [],
|
|
2619
|
+
metadata: { detail: stepDetail }
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
if (stepIdx === 0) {
|
|
2623
|
+
const listPattern = /^\s*(\d+)\.\s*\*?\*?([^*\n]+)\*?\*?\s*(?:[-—]\s*(.+))?$/gm;
|
|
2624
|
+
while ((m = listPattern.exec(summary)) !== null) {
|
|
2625
|
+
if ((_f = m[2]) == null ? void 0 : _f.trim().startsWith("#")) continue;
|
|
2626
|
+
stepIdx++;
|
|
2627
|
+
const stepName = ((_g = m[2]) == null ? void 0 : _g.trim().replace(/\*+/g, "")) || `Step ${stepIdx}`;
|
|
2628
|
+
const stepDetail = ((_h = m[3]) == null ? void 0 : _h.trim()) || "";
|
|
2629
|
+
const nodeId = `step-${stepIdx}`;
|
|
2630
|
+
children.push(nodeId);
|
|
2631
|
+
nodes.set(nodeId, {
|
|
2632
|
+
id: nodeId,
|
|
2633
|
+
type: "tool",
|
|
2634
|
+
name: stepName,
|
|
2635
|
+
startTime: runAtMs + (stepIdx - 1) * Math.floor(durationMs / Math.max(1, stepIdx + 1)),
|
|
2636
|
+
endTime: runAtMs + stepIdx * Math.floor(durationMs / Math.max(1, stepIdx + 1)),
|
|
2637
|
+
status: stepDetail.toLowerCase().includes("fail") ? "failed" : "completed",
|
|
2638
|
+
parentId: rootId,
|
|
2639
|
+
children: [],
|
|
2640
|
+
metadata: { detail: stepDetail }
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
if (stepIdx === 0 && summary) {
|
|
2645
|
+
const sumId = "summary-0";
|
|
2646
|
+
children.push(sumId);
|
|
2647
|
+
nodes.set(sumId, {
|
|
2648
|
+
id: sumId,
|
|
2649
|
+
type: "tool",
|
|
2650
|
+
name: status === "completed" ? "Execution" : evt.error ? String(evt.error) : "Execution",
|
|
2651
|
+
startTime: runAtMs,
|
|
2652
|
+
endTime: runAtMs + durationMs,
|
|
2653
|
+
status,
|
|
2654
|
+
parentId: rootId,
|
|
2655
|
+
children: [],
|
|
2656
|
+
metadata: { summary: summary.slice(0, 500) }
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
const sessionEvents = [
|
|
2660
|
+
{
|
|
2661
|
+
type: "system",
|
|
2662
|
+
timestamp: runAtMs,
|
|
2663
|
+
name: `${fileJobId} started`,
|
|
2664
|
+
content: `Model: ${model}`,
|
|
2665
|
+
id: `start-${ts}`
|
|
2666
|
+
}
|
|
2667
|
+
];
|
|
2668
|
+
if (summary) {
|
|
2669
|
+
sessionEvents.push({
|
|
2670
|
+
type: "assistant",
|
|
2671
|
+
timestamp: runAtMs + durationMs,
|
|
2672
|
+
name: status === "completed" ? "Completed" : "Failed",
|
|
2673
|
+
content: summary,
|
|
2674
|
+
id: `result-${ts}`
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
const trace = {
|
|
2678
|
+
id: sessionId,
|
|
2679
|
+
nodes,
|
|
2680
|
+
edges: [],
|
|
2681
|
+
events: [],
|
|
2682
|
+
startTime: runAtMs,
|
|
2683
|
+
agentId,
|
|
2684
|
+
trigger: "cron",
|
|
2685
|
+
name: `${fileJobId} ${new Date(runAtMs).toISOString().slice(0, 16)}`,
|
|
2686
|
+
traceId: sessionId,
|
|
2687
|
+
spanId: sessionId,
|
|
2688
|
+
filename,
|
|
2689
|
+
lastModified: fileStat.mtime.getTime(),
|
|
2690
|
+
sourceType: "session",
|
|
2691
|
+
sourceDir: path2.dirname(filePath),
|
|
2692
|
+
sessionEvents,
|
|
2693
|
+
tokenUsage: {
|
|
2694
|
+
input: usage.promptTokens || usage.input || 0,
|
|
2695
|
+
output: usage.completionTokens || usage.output || 0,
|
|
2696
|
+
total: usage.totalTokens || usage.total || 0,
|
|
2697
|
+
cost: 0
|
|
2698
|
+
},
|
|
2699
|
+
metadata: { jobId: fileJobId, model, source: "cron-run", sessionId }
|
|
2700
|
+
};
|
|
2701
|
+
const key = `${this.traceKey(filePath, agentId)}-${sessionId.slice(0, 12)}`;
|
|
2702
|
+
this.traces.set(key, trace);
|
|
2703
|
+
loaded++;
|
|
1933
2704
|
}
|
|
1934
|
-
|
|
1935
|
-
const lastTs = ((_c = rawEvents[rawEvents.length - 1]) == null ? void 0 : _c.ts) || fileStat.mtime.getTime();
|
|
1936
|
-
const rootId = `cron-${jobId.slice(0, 12)}`;
|
|
1937
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
1938
|
-
nodes.set(rootId, {
|
|
1939
|
-
id: rootId,
|
|
1940
|
-
type: "agent",
|
|
1941
|
-
name: jobId,
|
|
1942
|
-
startTime: firstTs,
|
|
1943
|
-
endTime: lastTs,
|
|
1944
|
-
status: lastStatus,
|
|
1945
|
-
parentId: void 0,
|
|
1946
|
-
children: [],
|
|
1947
|
-
metadata: { jobId, runs: rawEvents.length }
|
|
1948
|
-
});
|
|
1949
|
-
const trace = {
|
|
1950
|
-
id: jobId,
|
|
1951
|
-
nodes,
|
|
1952
|
-
edges: [],
|
|
1953
|
-
events: [],
|
|
1954
|
-
startTime: firstTs,
|
|
1955
|
-
agentId: "openclaw-cron",
|
|
1956
|
-
trigger: "cron",
|
|
1957
|
-
name: jobId,
|
|
1958
|
-
traceId: jobId,
|
|
1959
|
-
spanId: jobId,
|
|
1960
|
-
filename,
|
|
1961
|
-
lastModified: fileStat.mtime.getTime(),
|
|
1962
|
-
sourceType: "session",
|
|
1963
|
-
sourceDir: path.dirname(filePath),
|
|
1964
|
-
sessionEvents,
|
|
1965
|
-
tokenUsage: { input: 0, output: 0, total: 0, cost: 0 },
|
|
1966
|
-
metadata: { jobId, source: "cron-run" }
|
|
1967
|
-
};
|
|
1968
|
-
this.traces.set(this.traceKey(filePath), trace);
|
|
1969
|
-
return true;
|
|
2705
|
+
return loaded > 0;
|
|
1970
2706
|
} catch {
|
|
1971
2707
|
return false;
|
|
1972
2708
|
}
|
|
1973
2709
|
}
|
|
1974
|
-
/** Unique key for a file across directories. */
|
|
1975
|
-
traceKey(filePath) {
|
|
2710
|
+
/** Unique key for a file across directories. Includes agentId to prevent collisions between agents. */
|
|
2711
|
+
traceKey(filePath, agentId) {
|
|
2712
|
+
let fileKey;
|
|
1976
2713
|
for (const dir of this.allWatchDirs) {
|
|
1977
2714
|
if (filePath.startsWith(dir)) {
|
|
1978
|
-
const dirParts = dir.split(
|
|
2715
|
+
const dirParts = dir.split(path2.sep).filter(Boolean);
|
|
1979
2716
|
const dirSuffix = dirParts.slice(-2).join("/");
|
|
1980
|
-
|
|
2717
|
+
fileKey = `${path2.relative(dir, filePath).replace(/\\/g, "/")}@${dirSuffix}`;
|
|
2718
|
+
return agentId ? `${fileKey}#${agentId}` : fileKey;
|
|
1981
2719
|
}
|
|
1982
2720
|
}
|
|
1983
|
-
|
|
2721
|
+
fileKey = filePath;
|
|
2722
|
+
return agentId ? `${fileKey}#${agentId}` : fileKey;
|
|
1984
2723
|
}
|
|
1985
2724
|
startWatching() {
|
|
1986
2725
|
for (const dir of this.allWatchDirs) {
|
|
1987
|
-
if (!
|
|
2726
|
+
if (!fs2.existsSync(dir)) continue;
|
|
1988
2727
|
const patterns = [
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2728
|
+
path2.join(dir, "**/*.json"),
|
|
2729
|
+
path2.join(dir, "**/*.jsonl"),
|
|
2730
|
+
path2.join(dir, "**/*.log"),
|
|
2731
|
+
path2.join(dir, "**/*.trace")
|
|
1993
2732
|
];
|
|
1994
2733
|
const watcher = chokidar.watch(patterns, {
|
|
1995
2734
|
ignored: [
|
|
@@ -2015,9 +2754,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
2015
2754
|
// Allow deep nesting for OpenClaw agents/*/sessions/
|
|
2016
2755
|
});
|
|
2017
2756
|
watcher.on("add", (filePath) => {
|
|
2018
|
-
if (this.isSupportedFile(
|
|
2019
|
-
const relativePath =
|
|
2020
|
-
console.log(`New file: ${relativePath} (in ${
|
|
2757
|
+
if (this.isSupportedFile(path2.basename(filePath))) {
|
|
2758
|
+
const relativePath = path2.relative(dir, filePath);
|
|
2759
|
+
console.log(`New file: ${relativePath} (in ${path2.basename(dir)})`);
|
|
2021
2760
|
if (this.loadFile(filePath)) {
|
|
2022
2761
|
const key = this.traceKey(filePath);
|
|
2023
2762
|
const trace = this.traces.get(key);
|
|
@@ -2028,7 +2767,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
2028
2767
|
}
|
|
2029
2768
|
});
|
|
2030
2769
|
watcher.on("change", (filePath) => {
|
|
2031
|
-
if (this.isSupportedFile(
|
|
2770
|
+
if (this.isSupportedFile(path2.basename(filePath))) {
|
|
2032
2771
|
if (this.loadFile(filePath)) {
|
|
2033
2772
|
const key = this.traceKey(filePath);
|
|
2034
2773
|
const trace = this.traces.get(key);
|
|
@@ -2039,7 +2778,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
2039
2778
|
}
|
|
2040
2779
|
});
|
|
2041
2780
|
watcher.on("unlink", (filePath) => {
|
|
2042
|
-
if (this.isSupportedFile(
|
|
2781
|
+
if (this.isSupportedFile(path2.basename(filePath))) {
|
|
2043
2782
|
const key = this.traceKey(filePath);
|
|
2044
2783
|
this.traces.delete(key);
|
|
2045
2784
|
this.emit("trace-removed", key);
|
|
@@ -2063,6 +2802,10 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
2063
2802
|
let candidates = [];
|
|
2064
2803
|
const exact = this.traces.get(filename);
|
|
2065
2804
|
if (exact) candidates.push(exact);
|
|
2805
|
+
if (agentId && !filename.includes("#")) {
|
|
2806
|
+
const agentKeyed = this.traces.get(`${filename}#${agentId}`);
|
|
2807
|
+
if (agentKeyed) candidates.push(agentKeyed);
|
|
2808
|
+
}
|
|
2066
2809
|
if (filename.includes("::")) {
|
|
2067
2810
|
const [fname, startTimeStr] = filename.split("::");
|
|
2068
2811
|
const startTime = Number(startTimeStr);
|
|
@@ -2083,19 +2826,21 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
2083
2826
|
candidates.push(trace);
|
|
2084
2827
|
}
|
|
2085
2828
|
}
|
|
2829
|
+
candidates = [...new Set(candidates)];
|
|
2086
2830
|
if (candidates.length === 0) return void 0;
|
|
2087
2831
|
if (candidates.length === 1) return candidates[0];
|
|
2088
2832
|
if (agentId) {
|
|
2089
2833
|
const agentMatches = candidates.filter((c) => c.agentId === agentId);
|
|
2090
|
-
if (agentMatches.length
|
|
2091
|
-
|
|
2092
|
-
|
|
2834
|
+
if (agentMatches.length === 1) return agentMatches[0];
|
|
2835
|
+
if (agentMatches.length > 1) candidates = agentMatches;
|
|
2836
|
+
if (agentMatches.length === 0) return void 0;
|
|
2093
2837
|
}
|
|
2094
|
-
if (candidates.length === 1) return candidates[0];
|
|
2095
2838
|
let best = candidates[0];
|
|
2839
|
+
if (!best) return void 0;
|
|
2096
2840
|
let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
|
|
2097
2841
|
for (let i = 1; i < candidates.length; i++) {
|
|
2098
2842
|
const c = candidates[i];
|
|
2843
|
+
if (!c) continue;
|
|
2099
2844
|
const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
|
|
2100
2845
|
if (nc > bestNodeCount) {
|
|
2101
2846
|
best = c;
|
|
@@ -2146,13 +2891,13 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
2146
2891
|
};
|
|
2147
2892
|
|
|
2148
2893
|
// src/cli.ts
|
|
2149
|
-
import * as
|
|
2894
|
+
import * as fs3 from "fs";
|
|
2150
2895
|
import * as os from "os";
|
|
2151
|
-
import * as
|
|
2896
|
+
import * as path3 from "path";
|
|
2152
2897
|
import { fileURLToPath } from "url";
|
|
2153
|
-
var __cliDirname =
|
|
2898
|
+
var __cliDirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
2154
2899
|
var VERSION = JSON.parse(
|
|
2155
|
-
|
|
2900
|
+
fs3.readFileSync(path3.resolve(__cliDirname, "../package.json"), "utf-8")
|
|
2156
2901
|
).version;
|
|
2157
2902
|
function getLanAddress() {
|
|
2158
2903
|
const interfaces = os.networkInterfaces();
|
|
@@ -2257,9 +3002,9 @@ async function startDashboard() {
|
|
|
2257
3002
|
if (!config.somaVault && process.env.SOMA_VAULT) {
|
|
2258
3003
|
config.somaVault = process.env.SOMA_VAULT;
|
|
2259
3004
|
}
|
|
2260
|
-
const tracesPath =
|
|
2261
|
-
if (!
|
|
2262
|
-
|
|
3005
|
+
const tracesPath = path3.resolve(config.tracesDir);
|
|
3006
|
+
if (!fs3.existsSync(tracesPath)) {
|
|
3007
|
+
fs3.mkdirSync(tracesPath, { recursive: true });
|
|
2263
3008
|
}
|
|
2264
3009
|
config.tracesDir = tracesPath;
|
|
2265
3010
|
console.log("\nStarting AgentFlow Dashboard...\n");
|
|
@@ -2331,13 +3076,77 @@ Examples:
|
|
|
2331
3076
|
|
|
2332
3077
|
// src/server.ts
|
|
2333
3078
|
var __filename = fileURLToPath2(import.meta.url);
|
|
2334
|
-
var __dirname =
|
|
3079
|
+
var __dirname = path4.dirname(__filename);
|
|
3080
|
+
function safePath(segment) {
|
|
3081
|
+
return path4.basename(segment.replace(/\.\./g, ""));
|
|
3082
|
+
}
|
|
3083
|
+
function parseVaultFrontmatter(content) {
|
|
3084
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
3085
|
+
if (!fmMatch) return null;
|
|
3086
|
+
const fm = {};
|
|
3087
|
+
const lines = fmMatch[1].split("\n");
|
|
3088
|
+
let currentKey = "";
|
|
3089
|
+
let collectingList = null;
|
|
3090
|
+
for (const line of lines) {
|
|
3091
|
+
if (line.match(/^\s*-\s/) && currentKey) {
|
|
3092
|
+
if (!collectingList) collectingList = [];
|
|
3093
|
+
const val2 = line.replace(/^\s*-\s*/, "").trim().replace(/^["']|["']$/g, "");
|
|
3094
|
+
collectingList.push(val2);
|
|
3095
|
+
continue;
|
|
3096
|
+
}
|
|
3097
|
+
if (collectingList && currentKey) {
|
|
3098
|
+
fm[currentKey] = collectingList;
|
|
3099
|
+
collectingList = null;
|
|
3100
|
+
}
|
|
3101
|
+
const colonIdx = line.indexOf(":");
|
|
3102
|
+
if (colonIdx === -1 || line.startsWith(" ")) continue;
|
|
3103
|
+
currentKey = line.slice(0, colonIdx).trim();
|
|
3104
|
+
let val = line.slice(colonIdx + 1).trim();
|
|
3105
|
+
if (val === "") continue;
|
|
3106
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
3107
|
+
try {
|
|
3108
|
+
fm[currentKey] = JSON.parse(val);
|
|
3109
|
+
} catch {
|
|
3110
|
+
fm[currentKey] = val;
|
|
3111
|
+
}
|
|
3112
|
+
currentKey = "";
|
|
3113
|
+
continue;
|
|
3114
|
+
}
|
|
3115
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
3116
|
+
val = val.slice(1, -1);
|
|
3117
|
+
}
|
|
3118
|
+
if (val === "true") fm[currentKey] = true;
|
|
3119
|
+
else if (val === "false") fm[currentKey] = false;
|
|
3120
|
+
else if (/^\d+(\.\d+)?$/.test(val)) fm[currentKey] = Number(val);
|
|
3121
|
+
else fm[currentKey] = val;
|
|
3122
|
+
currentKey = "";
|
|
3123
|
+
}
|
|
3124
|
+
if (collectingList && currentKey) {
|
|
3125
|
+
fm[currentKey] = collectingList;
|
|
3126
|
+
}
|
|
3127
|
+
for (const key of ["tags", "related", "evidence", "evidence_links", "sourceIds"]) {
|
|
3128
|
+
if (fm[key] && !Array.isArray(fm[key])) {
|
|
3129
|
+
const str = String(fm[key]);
|
|
3130
|
+
if (str.startsWith("[")) {
|
|
3131
|
+
try {
|
|
3132
|
+
fm[key] = JSON.parse(str);
|
|
3133
|
+
} catch {
|
|
3134
|
+
fm[key] = [str];
|
|
3135
|
+
}
|
|
3136
|
+
} else {
|
|
3137
|
+
fm[key] = [str];
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
if (!fm[key]) fm[key] = [];
|
|
3141
|
+
}
|
|
3142
|
+
return fm;
|
|
3143
|
+
}
|
|
2335
3144
|
function serializeTrace(trace) {
|
|
2336
3145
|
if (!trace) return trace;
|
|
2337
3146
|
const obj = { ...trace };
|
|
2338
|
-
if (
|
|
3147
|
+
if (trace.nodes instanceof Map) {
|
|
2339
3148
|
const nodesObj = {};
|
|
2340
|
-
for (const [key, value] of
|
|
3149
|
+
for (const [key, value] of trace.nodes) {
|
|
2341
3150
|
nodesObj[key] = value;
|
|
2342
3151
|
}
|
|
2343
3152
|
obj.nodes = nodesObj;
|
|
@@ -2351,11 +3160,11 @@ var DashboardServer = class {
|
|
|
2351
3160
|
this.userConfig = userCfg;
|
|
2352
3161
|
this.configPath = cfgPath;
|
|
2353
3162
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
2354
|
-
const dashConfigPath =
|
|
3163
|
+
const dashConfigPath = path4.join(home, ".agentflow/dashboard-config.json");
|
|
2355
3164
|
if (!config.dataDirs) config.dataDirs = [];
|
|
2356
3165
|
try {
|
|
2357
|
-
if (
|
|
2358
|
-
const saved = JSON.parse(
|
|
3166
|
+
if (fs4.existsSync(dashConfigPath)) {
|
|
3167
|
+
const saved = JSON.parse(fs4.readFileSync(dashConfigPath, "utf-8"));
|
|
2359
3168
|
const extraDirs = saved.extraDirs ?? [];
|
|
2360
3169
|
for (const d of extraDirs) {
|
|
2361
3170
|
if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
|
|
@@ -2364,7 +3173,7 @@ var DashboardServer = class {
|
|
|
2364
3173
|
} catch {
|
|
2365
3174
|
}
|
|
2366
3175
|
for (const p of getDiscoveryPaths(this.userConfig)) {
|
|
2367
|
-
if (
|
|
3176
|
+
if (fs4.existsSync(p) && !config.dataDirs.includes(p)) {
|
|
2368
3177
|
config.dataDirs.push(p);
|
|
2369
3178
|
}
|
|
2370
3179
|
}
|
|
@@ -2375,8 +3184,9 @@ var DashboardServer = class {
|
|
|
2375
3184
|
});
|
|
2376
3185
|
this.stats = new AgentStats();
|
|
2377
3186
|
this.knowledgeStore = createKnowledgeStore({
|
|
2378
|
-
baseDir:
|
|
3187
|
+
baseDir: path4.join(config.tracesDir, "..", ".agentflow", "knowledge")
|
|
2379
3188
|
});
|
|
3189
|
+
this.commandExecutor = createCommandExecutor(this.userConfig);
|
|
2380
3190
|
this.setupExpress();
|
|
2381
3191
|
this.setupWebSocket();
|
|
2382
3192
|
this.setupTraceWatcher();
|
|
@@ -2407,6 +3217,7 @@ var DashboardServer = class {
|
|
|
2407
3217
|
ts: 0
|
|
2408
3218
|
};
|
|
2409
3219
|
knowledgeStore;
|
|
3220
|
+
commandExecutor;
|
|
2410
3221
|
userConfig;
|
|
2411
3222
|
configPath;
|
|
2412
3223
|
setupExpress() {
|
|
@@ -2431,11 +3242,11 @@ var DashboardServer = class {
|
|
|
2431
3242
|
next();
|
|
2432
3243
|
});
|
|
2433
3244
|
}
|
|
2434
|
-
const pkgDir =
|
|
2435
|
-
const clientDir =
|
|
2436
|
-
const clientIndex =
|
|
2437
|
-
const srcDir =
|
|
2438
|
-
const needsBuild = !
|
|
3245
|
+
const pkgDir = path4.join(__dirname, "..");
|
|
3246
|
+
const clientDir = path4.join(pkgDir, "dist/client");
|
|
3247
|
+
const clientIndex = path4.join(clientDir, "index.html");
|
|
3248
|
+
const srcDir = path4.join(pkgDir, "src/client");
|
|
3249
|
+
const needsBuild = !fs4.existsSync(clientIndex) || fs4.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
|
|
2439
3250
|
if (needsBuild) {
|
|
2440
3251
|
try {
|
|
2441
3252
|
console.log("Building dashboard client...");
|
|
@@ -2444,7 +3255,7 @@ var DashboardServer = class {
|
|
|
2444
3255
|
console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
|
|
2445
3256
|
}
|
|
2446
3257
|
}
|
|
2447
|
-
if (
|
|
3258
|
+
if (fs4.existsSync(clientDir)) {
|
|
2448
3259
|
this.app.use(express.static(clientDir));
|
|
2449
3260
|
}
|
|
2450
3261
|
this.app.get("/api/traces", (req, res) => {
|
|
@@ -2491,6 +3302,20 @@ var DashboardServer = class {
|
|
|
2491
3302
|
res.status(500).json({ error: "Failed to load trace events" });
|
|
2492
3303
|
}
|
|
2493
3304
|
});
|
|
3305
|
+
this.app.get("/api/traces/:filename/receipt", (req, res) => {
|
|
3306
|
+
try {
|
|
3307
|
+
const trace = this.watcher.getTrace(req.params.filename);
|
|
3308
|
+
if (!trace) {
|
|
3309
|
+
return res.status(404).json({ error: "Trace not found" });
|
|
3310
|
+
}
|
|
3311
|
+
const serialized = serializeTrace(trace);
|
|
3312
|
+
const graph = loadGraph2(serialized);
|
|
3313
|
+
const receipt = toReceipt(graph);
|
|
3314
|
+
res.json(receipt);
|
|
3315
|
+
} catch (_error) {
|
|
3316
|
+
res.status(500).json({ error: "Failed to generate receipt" });
|
|
3317
|
+
}
|
|
3318
|
+
});
|
|
2494
3319
|
this.app.get("/api/agents", (req, res) => {
|
|
2495
3320
|
try {
|
|
2496
3321
|
const raw = this.stats.getAgentsList();
|
|
@@ -2613,19 +3438,37 @@ var DashboardServer = class {
|
|
|
2613
3438
|
res.status(500).json({ error: "Failed to build process graph" });
|
|
2614
3439
|
}
|
|
2615
3440
|
});
|
|
2616
|
-
this.app.get("/api/agents/:agentId/variants", (req, res) => {
|
|
3441
|
+
this.app.get("/api/agents/:agentId/variants", async (req, res) => {
|
|
2617
3442
|
try {
|
|
2618
3443
|
const agentId = req.params.agentId;
|
|
3444
|
+
const byModel = req.query.by === "model";
|
|
2619
3445
|
const graphs = this.getGraphTraces(agentId);
|
|
2620
3446
|
if (graphs.length === 0) {
|
|
2621
|
-
return res.json({ agentId, totalTraces: 0, variants: [] });
|
|
3447
|
+
return res.json({ agentId, totalTraces: 0, variants: [], modelVariants: [] });
|
|
2622
3448
|
}
|
|
2623
3449
|
const variants = findVariants(graphs).map((v) => ({
|
|
2624
3450
|
pathSignature: v.pathSignature,
|
|
2625
3451
|
count: v.count,
|
|
2626
3452
|
percentage: v.percentage
|
|
2627
3453
|
}));
|
|
2628
|
-
|
|
3454
|
+
let modelVariants = [];
|
|
3455
|
+
if (byModel) {
|
|
3456
|
+
try {
|
|
3457
|
+
const { findVariantsWithModel } = await import(
|
|
3458
|
+
/* webpackIgnore: true */
|
|
3459
|
+
"./ops-intel-PL6GGIKL.js"
|
|
3460
|
+
);
|
|
3461
|
+
modelVariants = findVariantsWithModel(graphs, { includeModel: true }).map(
|
|
3462
|
+
(v) => ({
|
|
3463
|
+
pathSignature: v.pathSignature,
|
|
3464
|
+
count: v.count,
|
|
3465
|
+
percentage: v.percentage
|
|
3466
|
+
})
|
|
3467
|
+
);
|
|
3468
|
+
} catch {
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
res.json({ agentId, totalTraces: graphs.length, variants, modelVariants });
|
|
2629
3472
|
} catch (error) {
|
|
2630
3473
|
console.error("Variants error:", error);
|
|
2631
3474
|
res.status(500).json({ error: "Failed to compute variants" });
|
|
@@ -2650,6 +3493,147 @@ var DashboardServer = class {
|
|
|
2650
3493
|
res.status(500).json({ error: "Failed to compute bottlenecks" });
|
|
2651
3494
|
}
|
|
2652
3495
|
});
|
|
3496
|
+
this.app.get("/api/agents/:agentId/health-briefing", async (req, res) => {
|
|
3497
|
+
const somaVault = this.config.somaVault;
|
|
3498
|
+
if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
|
|
3499
|
+
try {
|
|
3500
|
+
const agentId = req.params.agentId;
|
|
3501
|
+
const agentFile = path4.join(
|
|
3502
|
+
somaVault,
|
|
3503
|
+
"agent",
|
|
3504
|
+
`${safePath(agentId.replace(/:/g, "-"))}.md`
|
|
3505
|
+
);
|
|
3506
|
+
let agentData = {};
|
|
3507
|
+
if (fs4.existsSync(agentFile)) {
|
|
3508
|
+
agentData = parseVaultFrontmatter(fs4.readFileSync(agentFile, "utf-8")) ?? {};
|
|
3509
|
+
}
|
|
3510
|
+
const totalExecutions = Number(agentData.totalExecutions ?? 0);
|
|
3511
|
+
const failureRate = Number(agentData.failureRate ?? 0);
|
|
3512
|
+
const failureCount = Number(agentData.failureCount ?? 0);
|
|
3513
|
+
const status = failureRate > 0.5 ? "critical" : failureRate > 0.1 ? "degraded" : "healthy";
|
|
3514
|
+
const knowledgeTypes = ["decision", "insight", "constraint", "contradiction", "policy"];
|
|
3515
|
+
const intelligence = [];
|
|
3516
|
+
for (const kt of knowledgeTypes) {
|
|
3517
|
+
const dir = path4.join(somaVault, kt);
|
|
3518
|
+
if (!fs4.existsSync(dir)) continue;
|
|
3519
|
+
for (const f of fs4.readdirSync(dir)) {
|
|
3520
|
+
if (!f.endsWith(".md")) continue;
|
|
3521
|
+
try {
|
|
3522
|
+
const content = fs4.readFileSync(path4.join(dir, f), "utf-8");
|
|
3523
|
+
if (!content.includes(agentId) && !content.includes(agentId.replace(/:/g, "-")))
|
|
3524
|
+
continue;
|
|
3525
|
+
const parsed = parseVaultFrontmatter(content);
|
|
3526
|
+
if (!parsed) continue;
|
|
3527
|
+
intelligence.push({
|
|
3528
|
+
type: String(parsed.type ?? kt),
|
|
3529
|
+
name: String(parsed.name ?? f.replace(".md", "")),
|
|
3530
|
+
claim: String(parsed.claim ?? "").slice(0, 150),
|
|
3531
|
+
confidence: parsed.confidence
|
|
3532
|
+
});
|
|
3533
|
+
} catch {
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
const agentDir = path4.join(somaVault, "agent");
|
|
3538
|
+
const peers = [];
|
|
3539
|
+
if (fs4.existsSync(agentDir)) {
|
|
3540
|
+
for (const f of fs4.readdirSync(agentDir)) {
|
|
3541
|
+
if (!f.endsWith(".md")) continue;
|
|
3542
|
+
const p = parseVaultFrontmatter(fs4.readFileSync(path4.join(agentDir, f), "utf-8"));
|
|
3543
|
+
if (!p) continue;
|
|
3544
|
+
const runs = Number(p.totalExecutions ?? 0);
|
|
3545
|
+
if (runs > 0) {
|
|
3546
|
+
peers.push({
|
|
3547
|
+
name: String(p.name ?? p.agentId ?? f.replace(".md", "")),
|
|
3548
|
+
successRate: 1 - Number(p.failureRate ?? 0),
|
|
3549
|
+
runs
|
|
3550
|
+
});
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
peers.sort((a, b) => b.successRate - a.successRate);
|
|
3555
|
+
let drift = null;
|
|
3556
|
+
try {
|
|
3557
|
+
const historyPath = path4.join(somaVault, "..", "conformance-history.json");
|
|
3558
|
+
if (fs4.existsSync(historyPath)) {
|
|
3559
|
+
const history = JSON.parse(fs4.readFileSync(historyPath, "utf-8"));
|
|
3560
|
+
const agentHistory = history.filter((e) => e.agentId === agentId);
|
|
3561
|
+
if (agentHistory.length >= 10) {
|
|
3562
|
+
try {
|
|
3563
|
+
const { detectDrift: dd } = await import(
|
|
3564
|
+
/* webpackIgnore: true */
|
|
3565
|
+
"./ops-intel-PL6GGIKL.js"
|
|
3566
|
+
);
|
|
3567
|
+
drift = dd(agentHistory);
|
|
3568
|
+
} catch {
|
|
3569
|
+
drift = { status: "stable", dataPoints: agentHistory.length };
|
|
3570
|
+
}
|
|
3571
|
+
} else {
|
|
3572
|
+
drift = { status: "insufficient_data", dataPoints: agentHistory.length };
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
} catch {
|
|
3576
|
+
}
|
|
3577
|
+
res.json({
|
|
3578
|
+
agentId,
|
|
3579
|
+
status,
|
|
3580
|
+
totalExecutions,
|
|
3581
|
+
failureRate,
|
|
3582
|
+
failureCount,
|
|
3583
|
+
intelligence: {
|
|
3584
|
+
total: intelligence.length,
|
|
3585
|
+
byType: Object.fromEntries(
|
|
3586
|
+
knowledgeTypes.map((t) => [t, intelligence.filter((i) => i.type === t)])
|
|
3587
|
+
)
|
|
3588
|
+
},
|
|
3589
|
+
peers,
|
|
3590
|
+
drift
|
|
3591
|
+
});
|
|
3592
|
+
} catch (error) {
|
|
3593
|
+
console.error("Health briefing error:", error);
|
|
3594
|
+
res.status(500).json({ error: "Failed to generate briefing" });
|
|
3595
|
+
}
|
|
3596
|
+
});
|
|
3597
|
+
this.app.get("/api/traces/:filename/decisions", (req, res) => {
|
|
3598
|
+
try {
|
|
3599
|
+
const trace = this.watcher.getTrace(req.params.filename);
|
|
3600
|
+
if (!trace) return res.status(404).json({ error: "Trace not found" });
|
|
3601
|
+
const serialized = serializeTrace(trace);
|
|
3602
|
+
const sessionEvents = trace.sessionEvents;
|
|
3603
|
+
let decisions = [];
|
|
3604
|
+
if (sessionEvents && sessionEvents.length > 0) {
|
|
3605
|
+
try {
|
|
3606
|
+
import("./ops-intel-PL6GGIKL.js").then(({ extractDecisionsFromSession, computePatternSignature }) => {
|
|
3607
|
+
decisions = extractDecisionsFromSession(sessionEvents);
|
|
3608
|
+
res.json({
|
|
3609
|
+
decisions,
|
|
3610
|
+
pattern: computePatternSignature(
|
|
3611
|
+
decisions
|
|
3612
|
+
)
|
|
3613
|
+
});
|
|
3614
|
+
}).catch(() => {
|
|
3615
|
+
res.json({ decisions: [], pattern: "" });
|
|
3616
|
+
});
|
|
3617
|
+
return;
|
|
3618
|
+
} catch {
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
const graph = loadGraph2(serialized);
|
|
3622
|
+
const nodes = [...graph.nodes.values()];
|
|
3623
|
+
const toolNodes = nodes.filter((n) => n.type === "tool" || n.type === "action").sort((a, b) => (a.startTime ?? 0) - (b.startTime ?? 0));
|
|
3624
|
+
decisions = toolNodes.map((n, i) => ({
|
|
3625
|
+
action: n.name,
|
|
3626
|
+
tool: n.name,
|
|
3627
|
+
outcome: n.status === "failed" ? "failed" : "ok",
|
|
3628
|
+
durationMs: n.endTime != null ? n.endTime - n.startTime : void 0,
|
|
3629
|
+
index: i
|
|
3630
|
+
}));
|
|
3631
|
+
const pattern = decisions.map((d) => d.action).join("\u2192");
|
|
3632
|
+
res.json({ decisions, pattern });
|
|
3633
|
+
} catch {
|
|
3634
|
+
res.status(500).json({ error: "Failed to extract decisions" });
|
|
3635
|
+
}
|
|
3636
|
+
});
|
|
2653
3637
|
this.app.get("/api/agents/:agentId/profile", (req, res) => {
|
|
2654
3638
|
try {
|
|
2655
3639
|
const profile = this.knowledgeStore.getAgentProfile(req.params.agentId);
|
|
@@ -2681,8 +3665,8 @@ var DashboardServer = class {
|
|
|
2681
3665
|
const nodeArr = Object.values(nodes);
|
|
2682
3666
|
const sorted = nodeArr.filter((n) => n.name && typeof n.startTime === "number" && n.startTime > 0).sort((a, b) => (a.startTime ?? 0) - (b.startTime ?? 0));
|
|
2683
3667
|
for (let i = 0; i < sorted.length - 1; i++) {
|
|
2684
|
-
const from = (_a = sorted[i]) == null ? void 0 : _a.name;
|
|
2685
|
-
const to = (_b = sorted[i + 1]) == null ? void 0 : _b.name;
|
|
3668
|
+
const from = ((_a = sorted[i]) == null ? void 0 : _a.name) ?? "";
|
|
3669
|
+
const to = ((_b = sorted[i + 1]) == null ? void 0 : _b.name) ?? "";
|
|
2686
3670
|
const key = `${from}|||${to}`;
|
|
2687
3671
|
transMap.set(key, (transMap.get(key) ?? 0) + 1);
|
|
2688
3672
|
}
|
|
@@ -2705,7 +3689,7 @@ var DashboardServer = class {
|
|
|
2705
3689
|
const model = {
|
|
2706
3690
|
transitions: [...transMap.entries()].map(([key, count]) => {
|
|
2707
3691
|
const [from, to] = key.split("|||");
|
|
2708
|
-
return { from, to, count };
|
|
3692
|
+
return { from: from ?? "", to: to ?? "", count };
|
|
2709
3693
|
}),
|
|
2710
3694
|
nodeTypes: Object.fromEntries(nodeTypeMap)
|
|
2711
3695
|
};
|
|
@@ -2758,11 +3742,11 @@ var DashboardServer = class {
|
|
|
2758
3742
|
return res.json({ tier: "teaser", somaVault: false, governanceAvailable: false });
|
|
2759
3743
|
}
|
|
2760
3744
|
try {
|
|
2761
|
-
const reportPath =
|
|
2762
|
-
if (!
|
|
3745
|
+
const reportPath = path4.join(somaVault, "..", "soma-report.json");
|
|
3746
|
+
if (!fs4.existsSync(reportPath)) {
|
|
2763
3747
|
return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
|
|
2764
3748
|
}
|
|
2765
|
-
const report = JSON.parse(
|
|
3749
|
+
const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
|
|
2766
3750
|
const hasGovernance = report.governance && typeof report.governance.pending === "number";
|
|
2767
3751
|
return res.json({
|
|
2768
3752
|
tier: hasGovernance ? "pro" : "free",
|
|
@@ -2779,15 +3763,15 @@ var DashboardServer = class {
|
|
|
2779
3763
|
return res.json({ available: false, teaser: true });
|
|
2780
3764
|
}
|
|
2781
3765
|
try {
|
|
2782
|
-
const reportPath =
|
|
2783
|
-
if (!
|
|
3766
|
+
const reportPath = path4.join(somaVault, "..", "soma-report.json");
|
|
3767
|
+
if (!fs4.existsSync(reportPath)) {
|
|
2784
3768
|
return res.json({
|
|
2785
3769
|
available: false,
|
|
2786
3770
|
teaser: false,
|
|
2787
3771
|
message: "No report file yet. Run soma watch."
|
|
2788
3772
|
});
|
|
2789
3773
|
}
|
|
2790
|
-
const report = JSON.parse(
|
|
3774
|
+
const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
|
|
2791
3775
|
res.json(report);
|
|
2792
3776
|
} catch (error) {
|
|
2793
3777
|
console.error("Soma report error:", error);
|
|
@@ -2800,11 +3784,11 @@ var DashboardServer = class {
|
|
|
2800
3784
|
return res.json({ available: false });
|
|
2801
3785
|
}
|
|
2802
3786
|
try {
|
|
2803
|
-
const reportPath =
|
|
2804
|
-
if (!
|
|
3787
|
+
const reportPath = path4.join(somaVault, "..", "soma-report.json");
|
|
3788
|
+
if (!fs4.existsSync(reportPath)) {
|
|
2805
3789
|
return res.json({ available: false, message: "No report file. Run soma report." });
|
|
2806
3790
|
}
|
|
2807
|
-
const report = JSON.parse(
|
|
3791
|
+
const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
|
|
2808
3792
|
res.json({
|
|
2809
3793
|
available: true,
|
|
2810
3794
|
layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
|
|
@@ -2812,7 +3796,9 @@ var DashboardServer = class {
|
|
|
2812
3796
|
insights: (report.insights ?? []).filter(
|
|
2813
3797
|
(i) => i.layer === "emerging" && i.proposal_status === "pending"
|
|
2814
3798
|
),
|
|
2815
|
-
canon: (report.insights ?? []).filter(
|
|
3799
|
+
canon: (report.insights ?? []).filter(
|
|
3800
|
+
(i) => i.layer === "canon"
|
|
3801
|
+
),
|
|
2816
3802
|
generatedAt: report.generatedAt
|
|
2817
3803
|
});
|
|
2818
3804
|
} catch (error) {
|
|
@@ -2839,7 +3825,9 @@ var DashboardServer = class {
|
|
|
2839
3825
|
);
|
|
2840
3826
|
res.json({ success: true, message: result.trim() });
|
|
2841
3827
|
} catch (error) {
|
|
2842
|
-
res.status(400).json({
|
|
3828
|
+
res.status(400).json({
|
|
3829
|
+
error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message
|
|
3830
|
+
});
|
|
2843
3831
|
}
|
|
2844
3832
|
});
|
|
2845
3833
|
this.app.post("/api/soma/governance/reject", (req, res) => {
|
|
@@ -2870,7 +3858,9 @@ var DashboardServer = class {
|
|
|
2870
3858
|
);
|
|
2871
3859
|
res.json({ success: true, message: result.trim() });
|
|
2872
3860
|
} catch (error) {
|
|
2873
|
-
res.status(400).json({
|
|
3861
|
+
res.status(400).json({
|
|
3862
|
+
error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message
|
|
3863
|
+
});
|
|
2874
3864
|
}
|
|
2875
3865
|
});
|
|
2876
3866
|
this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
|
|
@@ -2889,16 +3879,18 @@ var DashboardServer = class {
|
|
|
2889
3879
|
);
|
|
2890
3880
|
res.json({ available: true, output: result.trim() });
|
|
2891
3881
|
} catch (error) {
|
|
2892
|
-
res.status(404).json({
|
|
3882
|
+
res.status(404).json({
|
|
3883
|
+
error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message
|
|
3884
|
+
});
|
|
2893
3885
|
}
|
|
2894
3886
|
});
|
|
2895
3887
|
this.app.get("/api/soma/policies", (_req, res) => {
|
|
2896
3888
|
const somaVault = this.config.somaVault;
|
|
2897
3889
|
if (!somaVault) return res.json({ policies: [] });
|
|
2898
3890
|
try {
|
|
2899
|
-
const reportPath =
|
|
2900
|
-
if (!
|
|
2901
|
-
const report = JSON.parse(
|
|
3891
|
+
const reportPath = path4.join(somaVault, "..", "soma-report.json");
|
|
3892
|
+
if (!fs4.existsSync(reportPath)) return res.json({ policies: [] });
|
|
3893
|
+
const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
|
|
2902
3894
|
res.json({ policies: report.policies ?? [] });
|
|
2903
3895
|
} catch {
|
|
2904
3896
|
res.json({ policies: [] });
|
|
@@ -2929,7 +3921,9 @@ var DashboardServer = class {
|
|
|
2929
3921
|
const result = execFileSync("npx", args, { encoding: "utf-8", timeout: 1e4 });
|
|
2930
3922
|
res.json({ success: true, message: result.trim() });
|
|
2931
3923
|
} catch (error) {
|
|
2932
|
-
res.status(400).json({
|
|
3924
|
+
res.status(400).json({
|
|
3925
|
+
error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message
|
|
3926
|
+
});
|
|
2933
3927
|
}
|
|
2934
3928
|
});
|
|
2935
3929
|
this.app.delete("/api/soma/policies/:name", (req, res) => {
|
|
@@ -2949,28 +3943,46 @@ var DashboardServer = class {
|
|
|
2949
3943
|
);
|
|
2950
3944
|
res.json({ success: true, message: result.trim() });
|
|
2951
3945
|
} catch (error) {
|
|
2952
|
-
res.status(400).json({
|
|
3946
|
+
res.status(400).json({
|
|
3947
|
+
error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message
|
|
3948
|
+
});
|
|
2953
3949
|
}
|
|
2954
3950
|
});
|
|
2955
3951
|
this.app.get("/api/soma/vault/entities", (req, res) => {
|
|
2956
3952
|
const somaVault = this.config.somaVault;
|
|
2957
3953
|
if (!somaVault) return res.json({ entities: [], total: 0 });
|
|
2958
3954
|
try {
|
|
2959
|
-
const
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
...i,
|
|
2968
|
-
type: i.type || "insight",
|
|
2969
|
-
id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}`
|
|
2970
|
-
};
|
|
2971
|
-
}),
|
|
2972
|
-
...(report.policies ?? []).map((p) => ({ ...p, type: "policy", id: p.name }))
|
|
3955
|
+
const entityTypes = [
|
|
3956
|
+
"agent",
|
|
3957
|
+
"decision",
|
|
3958
|
+
"insight",
|
|
3959
|
+
"constraint",
|
|
3960
|
+
"contradiction",
|
|
3961
|
+
"policy",
|
|
3962
|
+
"archetype"
|
|
2973
3963
|
];
|
|
3964
|
+
let entities = [];
|
|
3965
|
+
for (const entityType of entityTypes) {
|
|
3966
|
+
const dir = path4.join(somaVault, entityType);
|
|
3967
|
+
if (!fs4.existsSync(dir)) continue;
|
|
3968
|
+
for (const file of fs4.readdirSync(dir)) {
|
|
3969
|
+
if (!file.endsWith(".md")) continue;
|
|
3970
|
+
try {
|
|
3971
|
+
const content = fs4.readFileSync(path4.join(dir, file), "utf-8");
|
|
3972
|
+
const parsed = parseVaultFrontmatter(content);
|
|
3973
|
+
if (!parsed) continue;
|
|
3974
|
+
const body = content.slice(content.indexOf("---", 4) + 3).trim().slice(0, 500);
|
|
3975
|
+
entities.push({
|
|
3976
|
+
...parsed,
|
|
3977
|
+
type: parsed.type || entityType,
|
|
3978
|
+
id: parsed.id || file.replace(".md", ""),
|
|
3979
|
+
name: parsed.name || file.replace(".md", ""),
|
|
3980
|
+
body
|
|
3981
|
+
});
|
|
3982
|
+
} catch {
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
2974
3986
|
const {
|
|
2975
3987
|
type,
|
|
2976
3988
|
layer,
|
|
@@ -3000,35 +4012,414 @@ var DashboardServer = class {
|
|
|
3000
4012
|
const somaVault = this.config.somaVault;
|
|
3001
4013
|
if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
|
|
3002
4014
|
try {
|
|
3003
|
-
const
|
|
3004
|
-
const
|
|
3005
|
-
const
|
|
3006
|
-
|
|
4015
|
+
const type = safePath(req.params.type);
|
|
4016
|
+
const id = safePath(req.params.id);
|
|
4017
|
+
const filePath = path4.join(somaVault, type, `${id}.md`);
|
|
4018
|
+
if (!path4.resolve(filePath).startsWith(path4.resolve(somaVault))) {
|
|
4019
|
+
return res.status(400).json({ error: "Invalid path" });
|
|
4020
|
+
}
|
|
4021
|
+
if (!fs4.existsSync(filePath)) return res.status(404).json({ error: "Entity not found" });
|
|
4022
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
4023
|
+
const fm = parseVaultFrontmatter(content);
|
|
4024
|
+
if (!fm) return res.status(404).json({ error: "Entity not found" });
|
|
4025
|
+
const body = content.slice(content.indexOf("---", 4) + 3).trim();
|
|
4026
|
+
const agentKnowledge = [];
|
|
3007
4027
|
if (type === "agent") {
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
(
|
|
3014
|
-
|
|
3015
|
-
|
|
4028
|
+
const agentName = fm.name || fm.agentId || id;
|
|
4029
|
+
const knowledgeTypes = ["decision", "insight", "constraint", "contradiction", "policy"];
|
|
4030
|
+
for (const kt of knowledgeTypes) {
|
|
4031
|
+
const ktDir = path4.join(somaVault, kt);
|
|
4032
|
+
if (!fs4.existsSync(ktDir)) continue;
|
|
4033
|
+
for (const f of fs4.readdirSync(ktDir)) {
|
|
4034
|
+
if (!f.endsWith(".md")) continue;
|
|
4035
|
+
try {
|
|
4036
|
+
const c = fs4.readFileSync(path4.join(ktDir, f), "utf-8");
|
|
4037
|
+
if (!c.includes(String(agentName))) continue;
|
|
4038
|
+
const parsed = parseVaultFrontmatter(c);
|
|
4039
|
+
if (!parsed) continue;
|
|
4040
|
+
agentKnowledge.push({
|
|
4041
|
+
type: parsed.type || kt,
|
|
4042
|
+
id: parsed.id || f.replace(".md", ""),
|
|
4043
|
+
name: parsed.name || f.replace(".md", ""),
|
|
4044
|
+
claim: parsed.claim || "",
|
|
4045
|
+
confidence: parsed.confidence || "",
|
|
4046
|
+
layer: parsed.layer || ""
|
|
4047
|
+
});
|
|
4048
|
+
} catch {
|
|
4049
|
+
}
|
|
3016
4050
|
}
|
|
3017
|
-
|
|
4051
|
+
}
|
|
3018
4052
|
}
|
|
3019
|
-
if (!entity) return res.status(404).json({ error: "Entity not found" });
|
|
3020
4053
|
res.json({
|
|
3021
|
-
...
|
|
4054
|
+
...fm,
|
|
3022
4055
|
type,
|
|
3023
4056
|
id,
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
4057
|
+
name: fm.name || id,
|
|
4058
|
+
body: type === "agent" && !body ? `Agent with ${fm.totalExecutions ?? 0} executions, ${((1 - Number(fm.failureRate || 0)) * 100).toFixed(1)}% success rate.` : body,
|
|
4059
|
+
knowledge: agentKnowledge
|
|
3027
4060
|
});
|
|
3028
4061
|
} catch {
|
|
3029
4062
|
res.status(404).json({ error: "Entity not found" });
|
|
3030
4063
|
}
|
|
3031
4064
|
});
|
|
4065
|
+
this.app.get("/api/aicp/preflight", async (req, res) => {
|
|
4066
|
+
const agentId = req.query.agentId;
|
|
4067
|
+
if (!agentId) {
|
|
4068
|
+
return res.status(400).json({ error: "agentId query parameter required" });
|
|
4069
|
+
}
|
|
4070
|
+
const somaVault = this.config.somaVault;
|
|
4071
|
+
if (!somaVault) {
|
|
4072
|
+
return res.json({
|
|
4073
|
+
proceed: true,
|
|
4074
|
+
warnings: [],
|
|
4075
|
+
recommendations: [],
|
|
4076
|
+
available: false,
|
|
4077
|
+
_meta: { durationMs: 0 }
|
|
4078
|
+
});
|
|
4079
|
+
}
|
|
4080
|
+
try {
|
|
4081
|
+
const { evaluatePreflight } = await import(
|
|
4082
|
+
/* webpackIgnore: true */
|
|
4083
|
+
"./dist-H4QMTTY3.js"
|
|
4084
|
+
);
|
|
4085
|
+
const { createVault } = await import(
|
|
4086
|
+
/* webpackIgnore: true */
|
|
4087
|
+
"./dist-H4QMTTY3.js"
|
|
4088
|
+
);
|
|
4089
|
+
const vault = createVault({ baseDir: somaVault });
|
|
4090
|
+
const result = evaluatePreflight(vault, safePath(agentId));
|
|
4091
|
+
res.json(result);
|
|
4092
|
+
} catch {
|
|
4093
|
+
res.json({
|
|
4094
|
+
proceed: true,
|
|
4095
|
+
warnings: [],
|
|
4096
|
+
recommendations: [],
|
|
4097
|
+
available: false,
|
|
4098
|
+
_meta: { durationMs: 0 }
|
|
4099
|
+
});
|
|
4100
|
+
}
|
|
4101
|
+
});
|
|
4102
|
+
this.app.get("/api/soma/efficiency", async (_req, res) => {
|
|
4103
|
+
try {
|
|
4104
|
+
const allTraces = this.watcher.getAllTraces().map(serializeTrace);
|
|
4105
|
+
const graphs = [];
|
|
4106
|
+
for (const t of allTraces) {
|
|
4107
|
+
try {
|
|
4108
|
+
if (t.sourceType === "session" || t.sourceType === "log") continue;
|
|
4109
|
+
if (!t.rootNodeId && !t.rootId) continue;
|
|
4110
|
+
const nodes = t.nodes;
|
|
4111
|
+
if (!nodes || typeof nodes === "object" && Object.keys(nodes).length === 0) continue;
|
|
4112
|
+
graphs.push(loadGraph2(t));
|
|
4113
|
+
} catch {
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
try {
|
|
4117
|
+
const { getEfficiency } = await import(
|
|
4118
|
+
/* webpackIgnore: true */
|
|
4119
|
+
"./ops-intel-PL6GGIKL.js"
|
|
4120
|
+
);
|
|
4121
|
+
const report2 = getEfficiency(graphs);
|
|
4122
|
+
return res.json(report2);
|
|
4123
|
+
} catch {
|
|
4124
|
+
}
|
|
4125
|
+
const somaVault = this.config.somaVault;
|
|
4126
|
+
if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
|
|
4127
|
+
const reportPath = path4.join(somaVault, "..", "soma-report.json");
|
|
4128
|
+
if (!fs4.existsSync(reportPath)) {
|
|
4129
|
+
return res.status(404).json({ error: "No SOMA report found" });
|
|
4130
|
+
}
|
|
4131
|
+
const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
|
|
4132
|
+
const agents = report.agents ?? [];
|
|
4133
|
+
const runs = agents.map((a) => ({
|
|
4134
|
+
graphId: a.name,
|
|
4135
|
+
agentId: a.name,
|
|
4136
|
+
totalTokenCost: a.totalTokenCost ?? 0,
|
|
4137
|
+
completedNodes: a.totalRuns ?? 0,
|
|
4138
|
+
costPerNode: (a.totalTokenCost ?? 0) / Math.max(1, a.totalRuns ?? 1)
|
|
4139
|
+
}));
|
|
4140
|
+
const costs = runs.map((r) => r.costPerNode).filter((c) => c > 0).sort((a, b) => a - b);
|
|
4141
|
+
const mean = costs.length > 0 ? costs.reduce((a, b) => a + b, 0) / costs.length : 0;
|
|
4142
|
+
const median = costs.length > 0 ? costs[Math.floor(costs.length / 2)] : 0;
|
|
4143
|
+
const p95 = costs.length > 0 ? costs[Math.min(costs.length - 1, Math.ceil(costs.length * 0.95) - 1)] : 0;
|
|
4144
|
+
res.json({
|
|
4145
|
+
runs,
|
|
4146
|
+
aggregate: { mean, median, p95 },
|
|
4147
|
+
flags: [],
|
|
4148
|
+
nodeCosts: [],
|
|
4149
|
+
dataCoverage: agents.length > 0 ? 1 : 0
|
|
4150
|
+
});
|
|
4151
|
+
} catch {
|
|
4152
|
+
res.status(500).json({ error: "Failed to compute efficiency" });
|
|
4153
|
+
}
|
|
4154
|
+
});
|
|
4155
|
+
this.app.get("/api/soma/drift", async (req, res) => {
|
|
4156
|
+
const agentId = req.query.agentId;
|
|
4157
|
+
if (!agentId) return res.status(400).json({ error: "agentId query parameter required" });
|
|
4158
|
+
try {
|
|
4159
|
+
const somaVault = this.config.somaVault;
|
|
4160
|
+
if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
|
|
4161
|
+
const historyPath = path4.join(somaVault, "..", "conformance-history.json");
|
|
4162
|
+
let history = [];
|
|
4163
|
+
if (fs4.existsSync(historyPath)) {
|
|
4164
|
+
history = JSON.parse(fs4.readFileSync(historyPath, "utf-8"));
|
|
4165
|
+
}
|
|
4166
|
+
const agentHistory = history.filter((e) => e.agentId === agentId);
|
|
4167
|
+
try {
|
|
4168
|
+
const { detectDrift } = await import(
|
|
4169
|
+
/* webpackIgnore: true */
|
|
4170
|
+
"./ops-intel-PL6GGIKL.js"
|
|
4171
|
+
);
|
|
4172
|
+
const driftReport = detectDrift(agentHistory);
|
|
4173
|
+
return res.json({ drift: driftReport, points: agentHistory });
|
|
4174
|
+
} catch {
|
|
4175
|
+
}
|
|
4176
|
+
const n = agentHistory.length;
|
|
4177
|
+
if (n < 10) {
|
|
4178
|
+
return res.json({
|
|
4179
|
+
drift: { status: "insufficient_data", slope: 0, r2: 0, windowSize: n, dataPoints: n },
|
|
4180
|
+
points: agentHistory
|
|
4181
|
+
});
|
|
4182
|
+
}
|
|
4183
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
|
|
4184
|
+
for (let i = 0; i < n; i++) {
|
|
4185
|
+
const y = agentHistory[i].score;
|
|
4186
|
+
sumX += i;
|
|
4187
|
+
sumY += y;
|
|
4188
|
+
sumXY += i * y;
|
|
4189
|
+
sumX2 += i * i;
|
|
4190
|
+
}
|
|
4191
|
+
const denom = n * sumX2 - sumX * sumX;
|
|
4192
|
+
const slope = denom !== 0 ? (n * sumXY - sumX * sumY) / denom : 0;
|
|
4193
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
4194
|
+
const meanY = sumY / n;
|
|
4195
|
+
let ssRes = 0, ssTot = 0;
|
|
4196
|
+
for (let i = 0; i < n; i++) {
|
|
4197
|
+
const y = agentHistory[i].score;
|
|
4198
|
+
ssRes += (y - (intercept + slope * i)) ** 2;
|
|
4199
|
+
ssTot += (y - meanY) ** 2;
|
|
4200
|
+
}
|
|
4201
|
+
const r2 = ssTot > 0 ? 1 - ssRes / ssTot : 0;
|
|
4202
|
+
const status = r2 > 0.3 ? slope < 0 ? "degrading" : "improving" : "stable";
|
|
4203
|
+
res.json({
|
|
4204
|
+
drift: { status, slope, r2, windowSize: n, dataPoints: n },
|
|
4205
|
+
points: agentHistory
|
|
4206
|
+
});
|
|
4207
|
+
} catch {
|
|
4208
|
+
res.status(404).json({ error: "Drift detection not available" });
|
|
4209
|
+
}
|
|
4210
|
+
});
|
|
4211
|
+
this.app.get("/api/soma/cross-agent", (_req, res) => {
|
|
4212
|
+
const somaVault = this.config.somaVault;
|
|
4213
|
+
if (!somaVault) return res.json({ insights: [], pairs: [] });
|
|
4214
|
+
try {
|
|
4215
|
+
const insightDir = path4.join(somaVault, "insight");
|
|
4216
|
+
if (!fs4.existsSync(insightDir)) return res.json({ insights: [], pairs: [] });
|
|
4217
|
+
const crossAgent = [];
|
|
4218
|
+
for (const file of fs4.readdirSync(insightDir)) {
|
|
4219
|
+
if (!file.endsWith(".md")) continue;
|
|
4220
|
+
try {
|
|
4221
|
+
const content = fs4.readFileSync(path4.join(insightDir, file), "utf-8");
|
|
4222
|
+
const parsed = parseVaultFrontmatter(content);
|
|
4223
|
+
if (!parsed) continue;
|
|
4224
|
+
const sa = parsed.source_agents;
|
|
4225
|
+
if (!sa || !Array.isArray(sa) || sa.length < 2) continue;
|
|
4226
|
+
crossAgent.push({
|
|
4227
|
+
name: String(parsed.name ?? file.replace(".md", "")),
|
|
4228
|
+
claim: String(parsed.claim ?? "").slice(0, 200),
|
|
4229
|
+
sourceAgents: sa,
|
|
4230
|
+
tags: parsed.tags ?? []
|
|
4231
|
+
});
|
|
4232
|
+
} catch {
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
const pairMap = /* @__PURE__ */ new Map();
|
|
4236
|
+
for (const insight of crossAgent) {
|
|
4237
|
+
const key = [...insight.sourceAgents].sort().join(" \u2194 ");
|
|
4238
|
+
if (!pairMap.has(key)) pairMap.set(key, []);
|
|
4239
|
+
pairMap.get(key).push(insight);
|
|
4240
|
+
}
|
|
4241
|
+
const pairs = [...pairMap.entries()].map(([agents, insights]) => ({
|
|
4242
|
+
agents,
|
|
4243
|
+
count: insights.length,
|
|
4244
|
+
insights: insights.slice(0, 5)
|
|
4245
|
+
}));
|
|
4246
|
+
res.json({ total: crossAgent.length, pairs });
|
|
4247
|
+
} catch {
|
|
4248
|
+
res.json({ insights: [], pairs: [] });
|
|
4249
|
+
}
|
|
4250
|
+
});
|
|
4251
|
+
this.app.get("/api/external/commands", (_req, res) => {
|
|
4252
|
+
try {
|
|
4253
|
+
const { commands, errors } = getValidatedExternalCommands(this.userConfig);
|
|
4254
|
+
const commandList = Object.entries(commands).map(([id, command]) => ({
|
|
4255
|
+
id,
|
|
4256
|
+
name: command.name,
|
|
4257
|
+
description: command.description,
|
|
4258
|
+
category: command.category,
|
|
4259
|
+
allowConcurrent: command.allowConcurrent,
|
|
4260
|
+
timeout: command.timeout
|
|
4261
|
+
}));
|
|
4262
|
+
res.json({
|
|
4263
|
+
commands: commandList,
|
|
4264
|
+
configErrors: errors,
|
|
4265
|
+
total: commandList.length
|
|
4266
|
+
});
|
|
4267
|
+
} catch (error) {
|
|
4268
|
+
res.status(500).json({
|
|
4269
|
+
error: "Failed to load external commands",
|
|
4270
|
+
message: error.message
|
|
4271
|
+
});
|
|
4272
|
+
}
|
|
4273
|
+
});
|
|
4274
|
+
this.app.post("/api/external/commands/:commandId/execute", express.json(), async (req, res) => {
|
|
4275
|
+
try {
|
|
4276
|
+
const { commandId } = req.params;
|
|
4277
|
+
const { additionalArgs, timeout, context } = req.body;
|
|
4278
|
+
if (!isValidId(commandId)) {
|
|
4279
|
+
return res.status(400).json({ error: "Invalid command ID" });
|
|
4280
|
+
}
|
|
4281
|
+
const executionResult = await this.commandExecutor.executeCommand({
|
|
4282
|
+
commandId,
|
|
4283
|
+
additionalArgs: Array.isArray(additionalArgs) ? additionalArgs : void 0,
|
|
4284
|
+
timeout: typeof timeout === "number" ? timeout : void 0,
|
|
4285
|
+
context: context && typeof context === "object" ? context : void 0
|
|
4286
|
+
});
|
|
4287
|
+
res.json({
|
|
4288
|
+
executionId: executionResult.executionId,
|
|
4289
|
+
started: executionResult.started,
|
|
4290
|
+
status: executionResult.status,
|
|
4291
|
+
commandName: executionResult.command.name,
|
|
4292
|
+
pid: executionResult.pid,
|
|
4293
|
+
startedAt: executionResult.startedAt,
|
|
4294
|
+
error: executionResult.error
|
|
4295
|
+
});
|
|
4296
|
+
} catch (error) {
|
|
4297
|
+
res.status(500).json({
|
|
4298
|
+
error: "Command execution failed",
|
|
4299
|
+
message: error.message
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
});
|
|
4303
|
+
this.app.get("/api/external/executions/:executionId", (req, res) => {
|
|
4304
|
+
try {
|
|
4305
|
+
const { executionId } = req.params;
|
|
4306
|
+
if (!isValidId(executionId)) {
|
|
4307
|
+
return res.status(400).json({ error: "Invalid execution ID" });
|
|
4308
|
+
}
|
|
4309
|
+
const execution = this.commandExecutor.getExecution(executionId);
|
|
4310
|
+
if (!execution) {
|
|
4311
|
+
return res.status(404).json({ error: "Execution not found" });
|
|
4312
|
+
}
|
|
4313
|
+
res.json({
|
|
4314
|
+
executionId: execution.executionId,
|
|
4315
|
+
started: execution.started,
|
|
4316
|
+
status: execution.status,
|
|
4317
|
+
commandName: execution.command.name,
|
|
4318
|
+
pid: execution.pid,
|
|
4319
|
+
startedAt: execution.startedAt,
|
|
4320
|
+
completedAt: execution.completedAt,
|
|
4321
|
+
duration: execution.duration,
|
|
4322
|
+
exitCode: execution.exitCode,
|
|
4323
|
+
hasOutput: execution.stdout.length > 0,
|
|
4324
|
+
hasError: execution.stderr.length > 0,
|
|
4325
|
+
// Limit output size for API response
|
|
4326
|
+
stdout: execution.stdout.slice(-2e3),
|
|
4327
|
+
// Last 2KB
|
|
4328
|
+
stderr: execution.stderr.slice(-1e3),
|
|
4329
|
+
// Last 1KB
|
|
4330
|
+
error: execution.error
|
|
4331
|
+
});
|
|
4332
|
+
} catch (error) {
|
|
4333
|
+
res.status(500).json({
|
|
4334
|
+
error: "Failed to get execution status",
|
|
4335
|
+
message: error.message
|
|
4336
|
+
});
|
|
4337
|
+
}
|
|
4338
|
+
});
|
|
4339
|
+
this.app.post("/api/external/executions/:executionId/kill", (req, res) => {
|
|
4340
|
+
try {
|
|
4341
|
+
const { executionId } = req.params;
|
|
4342
|
+
if (!isValidId(executionId)) {
|
|
4343
|
+
return res.status(400).json({ error: "Invalid execution ID" });
|
|
4344
|
+
}
|
|
4345
|
+
const killed = this.commandExecutor.killExecution(executionId, "manual");
|
|
4346
|
+
if (!killed) {
|
|
4347
|
+
return res.status(404).json({ error: "Execution not found or not running" });
|
|
4348
|
+
}
|
|
4349
|
+
res.json({ success: true, message: "Execution killed" });
|
|
4350
|
+
} catch (error) {
|
|
4351
|
+
res.status(500).json({
|
|
4352
|
+
error: "Failed to kill execution",
|
|
4353
|
+
message: error.message
|
|
4354
|
+
});
|
|
4355
|
+
}
|
|
4356
|
+
});
|
|
4357
|
+
this.app.get("/api/external/executions", (req, res) => {
|
|
4358
|
+
try {
|
|
4359
|
+
const limit = Math.min(parseInt(String(req.query.limit)) || 50, 100);
|
|
4360
|
+
const status = req.query.status;
|
|
4361
|
+
let executions = this.commandExecutor.getAllExecutions(limit);
|
|
4362
|
+
if (status && ["running", "completed", "failed", "timeout", "killed"].includes(status)) {
|
|
4363
|
+
executions = executions.filter((exec) => exec.status === status);
|
|
4364
|
+
}
|
|
4365
|
+
const executionSummaries = executions.map((exec) => ({
|
|
4366
|
+
executionId: exec.executionId,
|
|
4367
|
+
commandName: exec.command.name,
|
|
4368
|
+
status: exec.status,
|
|
4369
|
+
startedAt: exec.startedAt,
|
|
4370
|
+
completedAt: exec.completedAt,
|
|
4371
|
+
duration: exec.duration,
|
|
4372
|
+
exitCode: exec.exitCode,
|
|
4373
|
+
hasOutput: exec.stdout.length > 0,
|
|
4374
|
+
hasError: exec.stderr.length > 0,
|
|
4375
|
+
error: exec.error
|
|
4376
|
+
}));
|
|
4377
|
+
res.json({
|
|
4378
|
+
executions: executionSummaries,
|
|
4379
|
+
total: executionSummaries.length,
|
|
4380
|
+
running: this.commandExecutor.getRunningExecutions().length
|
|
4381
|
+
});
|
|
4382
|
+
} catch (error) {
|
|
4383
|
+
res.status(500).json({
|
|
4384
|
+
error: "Failed to get executions",
|
|
4385
|
+
message: error.message
|
|
4386
|
+
});
|
|
4387
|
+
}
|
|
4388
|
+
});
|
|
4389
|
+
this.app.get("/api/external/executions/:executionId/audit", (req, res) => {
|
|
4390
|
+
try {
|
|
4391
|
+
const { executionId } = req.params;
|
|
4392
|
+
if (!isValidId(executionId)) {
|
|
4393
|
+
return res.status(400).json({ error: "Invalid execution ID" });
|
|
4394
|
+
}
|
|
4395
|
+
const auditTrail = this.commandExecutor.getAuditTrail(executionId);
|
|
4396
|
+
res.json({
|
|
4397
|
+
executionId,
|
|
4398
|
+
auditEntries: auditTrail,
|
|
4399
|
+
total: auditTrail.length
|
|
4400
|
+
});
|
|
4401
|
+
} catch (error) {
|
|
4402
|
+
res.status(500).json({
|
|
4403
|
+
error: "Failed to get audit trail",
|
|
4404
|
+
message: error.message
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
4407
|
+
});
|
|
4408
|
+
this.app.get("/api/external/audit/stats", (req, res) => {
|
|
4409
|
+
try {
|
|
4410
|
+
const days = Math.min(parseInt(String(req.query.days)) || 7, 30);
|
|
4411
|
+
const stats = this.commandExecutor.getAuditStats(days);
|
|
4412
|
+
res.json({
|
|
4413
|
+
period: `${days} days`,
|
|
4414
|
+
...stats
|
|
4415
|
+
});
|
|
4416
|
+
} catch (error) {
|
|
4417
|
+
res.status(500).json({
|
|
4418
|
+
error: "Failed to get audit statistics",
|
|
4419
|
+
message: error.message
|
|
4420
|
+
});
|
|
4421
|
+
}
|
|
4422
|
+
});
|
|
3032
4423
|
this.app.get("/api/process-health", (_req, res) => {
|
|
3033
4424
|
var _a, _b;
|
|
3034
4425
|
try {
|
|
@@ -3038,7 +4429,7 @@ var DashboardServer = class {
|
|
|
3038
4429
|
}
|
|
3039
4430
|
const discoveryDirs = [
|
|
3040
4431
|
this.config.tracesDir,
|
|
3041
|
-
|
|
4432
|
+
path4.dirname(this.config.tracesDir),
|
|
3042
4433
|
...this.config.dataDirs || []
|
|
3043
4434
|
];
|
|
3044
4435
|
let configs = discoverAllProcessConfigs(discoveryDirs);
|
|
@@ -3101,7 +4492,7 @@ var DashboardServer = class {
|
|
|
3101
4492
|
// Topology edges: parent-child relationships from process ppid
|
|
3102
4493
|
topology: uniqueProcesses.map((p) => {
|
|
3103
4494
|
try {
|
|
3104
|
-
const statusContent =
|
|
4495
|
+
const statusContent = fs4.readFileSync(`/proc/${p.pid}/status`, "utf8");
|
|
3105
4496
|
const ppidMatch = statusContent.match(/^PPid:\s+(\d+)/m);
|
|
3106
4497
|
const ppid = ppidMatch ? parseInt(ppidMatch[1] ?? "0", 10) : 0;
|
|
3107
4498
|
if (ppid > 1 && allKnownPids.has(ppid)) {
|
|
@@ -3121,11 +4512,11 @@ var DashboardServer = class {
|
|
|
3121
4512
|
this.app.get("/api/directories", (_req, res) => {
|
|
3122
4513
|
try {
|
|
3123
4514
|
const home = process.env.HOME ?? "/home/trader";
|
|
3124
|
-
const configPath =
|
|
4515
|
+
const configPath = path4.join(home, ".agentflow/dashboard-config.json");
|
|
3125
4516
|
let extraDirs = [];
|
|
3126
4517
|
try {
|
|
3127
|
-
if (
|
|
3128
|
-
const cfg = JSON.parse(
|
|
4518
|
+
if (fs4.existsSync(configPath)) {
|
|
4519
|
+
const cfg = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
|
|
3129
4520
|
extraDirs = cfg.extraDirs ?? [];
|
|
3130
4521
|
}
|
|
3131
4522
|
} catch {
|
|
@@ -3133,7 +4524,7 @@ var DashboardServer = class {
|
|
|
3133
4524
|
const watched = [
|
|
3134
4525
|
...new Set(
|
|
3135
4526
|
[this.config.tracesDir, ...this.config.dataDirs || [], ...extraDirs].map(
|
|
3136
|
-
(w) =>
|
|
4527
|
+
(w) => path4.resolve(w)
|
|
3137
4528
|
)
|
|
3138
4529
|
)
|
|
3139
4530
|
];
|
|
@@ -3152,8 +4543,8 @@ var DashboardServer = class {
|
|
|
3152
4543
|
for (const line of raw.split("\n")) {
|
|
3153
4544
|
const match = line.match(/path=([^\s;]+)/);
|
|
3154
4545
|
if (match == null ? void 0 : match[1]) {
|
|
3155
|
-
const dir =
|
|
3156
|
-
if (
|
|
4546
|
+
const dir = path4.dirname(match[1]);
|
|
4547
|
+
if (fs4.existsSync(dir)) discovered.push(dir);
|
|
3157
4548
|
}
|
|
3158
4549
|
}
|
|
3159
4550
|
} catch {
|
|
@@ -3161,15 +4552,15 @@ var DashboardServer = class {
|
|
|
3161
4552
|
}
|
|
3162
4553
|
const commonPaths = [
|
|
3163
4554
|
...getDiscoveryPaths(this.userConfig),
|
|
3164
|
-
|
|
4555
|
+
path4.join(home, ".agentflow/traces")
|
|
3165
4556
|
];
|
|
3166
4557
|
for (const p of commonPaths) {
|
|
3167
|
-
if (
|
|
4558
|
+
if (fs4.existsSync(p) && !discovered.includes(p)) {
|
|
3168
4559
|
discovered.push(p);
|
|
3169
4560
|
}
|
|
3170
4561
|
}
|
|
3171
|
-
const watchedSet = new Set(watched.map((w) =>
|
|
3172
|
-
const suggested = discovered.filter((d) => !watchedSet.has(
|
|
4562
|
+
const watchedSet = new Set(watched.map((w) => path4.resolve(w)));
|
|
4563
|
+
const suggested = discovered.filter((d) => !watchedSet.has(path4.resolve(d)));
|
|
3173
4564
|
res.json({ watched, discovered, suggested });
|
|
3174
4565
|
} catch (error) {
|
|
3175
4566
|
console.error("Directory discovery error:", error);
|
|
@@ -3180,22 +4571,22 @@ var DashboardServer = class {
|
|
|
3180
4571
|
try {
|
|
3181
4572
|
const { add, remove } = req.body;
|
|
3182
4573
|
if (add) {
|
|
3183
|
-
const resolved =
|
|
4574
|
+
const resolved = path4.resolve(add);
|
|
3184
4575
|
if (resolved !== add || add.includes("..")) {
|
|
3185
4576
|
return res.status(400).json({ error: "Invalid directory path" });
|
|
3186
4577
|
}
|
|
3187
|
-
if (!
|
|
4578
|
+
if (!fs4.existsSync(resolved)) {
|
|
3188
4579
|
return res.status(400).json({ error: `Directory does not exist: ${add}` });
|
|
3189
4580
|
}
|
|
3190
4581
|
}
|
|
3191
|
-
const configPath =
|
|
4582
|
+
const configPath = path4.join(
|
|
3192
4583
|
process.env.HOME ?? "/home/trader",
|
|
3193
4584
|
".agentflow/dashboard-config.json"
|
|
3194
4585
|
);
|
|
3195
4586
|
let config = {};
|
|
3196
4587
|
try {
|
|
3197
|
-
if (
|
|
3198
|
-
config = JSON.parse(
|
|
4588
|
+
if (fs4.existsSync(configPath)) {
|
|
4589
|
+
config = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
|
|
3199
4590
|
}
|
|
3200
4591
|
} catch {
|
|
3201
4592
|
}
|
|
@@ -3206,8 +4597,8 @@ var DashboardServer = class {
|
|
|
3206
4597
|
if (remove) {
|
|
3207
4598
|
config.extraDirs = config.extraDirs.filter((d) => d !== remove);
|
|
3208
4599
|
}
|
|
3209
|
-
|
|
3210
|
-
|
|
4600
|
+
fs4.mkdirSync(path4.dirname(configPath), { recursive: true });
|
|
4601
|
+
fs4.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
3211
4602
|
res.json({ ok: true, extraDirs: config.extraDirs });
|
|
3212
4603
|
} catch (error) {
|
|
3213
4604
|
console.error("Directory config error:", error);
|
|
@@ -3249,7 +4640,10 @@ var DashboardServer = class {
|
|
|
3249
4640
|
lastModified: Date.now(),
|
|
3250
4641
|
sourceDir: "http-collector"
|
|
3251
4642
|
};
|
|
3252
|
-
this.watcher.traces.set(
|
|
4643
|
+
this.watcher.traces.set(
|
|
4644
|
+
`otel:${trace.id}`,
|
|
4645
|
+
watched
|
|
4646
|
+
);
|
|
3253
4647
|
ingested++;
|
|
3254
4648
|
}
|
|
3255
4649
|
if (ingested > 0) {
|
|
@@ -3273,14 +4667,18 @@ var DashboardServer = class {
|
|
|
3273
4667
|
this.app.get("/ready", (_req, res) => {
|
|
3274
4668
|
res.json({ status: "ready" });
|
|
3275
4669
|
});
|
|
3276
|
-
this.app.get(
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
4670
|
+
this.app.get(
|
|
4671
|
+
"*",
|
|
4672
|
+
rateLimit({ windowMs: 60 * 1e3, max: 600, standardHeaders: true, legacyHeaders: false }),
|
|
4673
|
+
(_req, res) => {
|
|
4674
|
+
const clientIndex2 = path4.join(__dirname, "../dist/client/index.html");
|
|
4675
|
+
if (fs4.existsSync(clientIndex2)) {
|
|
4676
|
+
res.sendFile(clientIndex2);
|
|
4677
|
+
} else {
|
|
4678
|
+
res.status(404).send("Dashboard not found - public files may not be built");
|
|
4679
|
+
}
|
|
3282
4680
|
}
|
|
3283
|
-
|
|
4681
|
+
);
|
|
3284
4682
|
}
|
|
3285
4683
|
setupWebSocket() {
|
|
3286
4684
|
this.wss.on("connection", (ws) => {
|
|
@@ -3306,9 +4704,9 @@ var DashboardServer = class {
|
|
|
3306
4704
|
setupSomaReportWatcher() {
|
|
3307
4705
|
const somaVault = this.config.somaVault;
|
|
3308
4706
|
if (!somaVault) return;
|
|
3309
|
-
const reportPath =
|
|
3310
|
-
const reportDir =
|
|
3311
|
-
if (!
|
|
4707
|
+
const reportPath = path4.join(somaVault, "..", "soma-report.json");
|
|
4708
|
+
const reportDir = path4.dirname(reportPath);
|
|
4709
|
+
if (!fs4.existsSync(reportDir)) return;
|
|
3312
4710
|
let debounceTimer = null;
|
|
3313
4711
|
const watcher = chokidar2.watch(reportPath, {
|
|
3314
4712
|
ignoreInitial: true,
|
|
@@ -3320,7 +4718,7 @@ var DashboardServer = class {
|
|
|
3320
4718
|
debounceTimer = setTimeout(() => {
|
|
3321
4719
|
var _a, _b;
|
|
3322
4720
|
try {
|
|
3323
|
-
const report = JSON.parse(
|
|
4721
|
+
const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
|
|
3324
4722
|
this.broadcast({ type: "soma-report-updated", data: report });
|
|
3325
4723
|
if (report.generatedAt) {
|
|
3326
4724
|
this.broadcast({
|
|
@@ -3351,7 +4749,10 @@ var DashboardServer = class {
|
|
|
3351
4749
|
const nodes = trace.nodes;
|
|
3352
4750
|
if (!nodes || typeof nodes === "object" && Object.keys(nodes).length === 0) continue;
|
|
3353
4751
|
const nodeValues = Object.values(nodes);
|
|
3354
|
-
if (nodeValues.some(
|
|
4752
|
+
if (nodeValues.some(
|
|
4753
|
+
(n) => n.type === "log-file" || n.type === "log-entry"
|
|
4754
|
+
))
|
|
4755
|
+
continue;
|
|
3355
4756
|
graphs.push(loadGraph2(trace));
|
|
3356
4757
|
} catch {
|
|
3357
4758
|
}
|
|
@@ -3445,10 +4846,7 @@ var DashboardServer = class {
|
|
|
3445
4846
|
}
|
|
3446
4847
|
}
|
|
3447
4848
|
const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
|
|
3448
|
-
const maxNodeCount = Math.max(
|
|
3449
|
-
...nodes.filter((n) => !n.isVirtual).map((n) => n.count),
|
|
3450
|
-
1
|
|
3451
|
-
);
|
|
4849
|
+
const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
|
|
3452
4850
|
return { agentId, totalTraces: model.totalGraphs, nodes, edges, maxEdgeCount, maxNodeCount };
|
|
3453
4851
|
}
|
|
3454
4852
|
/**
|
|
@@ -3492,7 +4890,7 @@ var DashboardServer = class {
|
|
|
3492
4890
|
}
|
|
3493
4891
|
const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
|
|
3494
4892
|
for (let i = 0; i < seq.length; i++) {
|
|
3495
|
-
const act = seq[i];
|
|
4893
|
+
const act = seq[i] ?? "";
|
|
3496
4894
|
activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
|
|
3497
4895
|
if (i < seq.length - 1) {
|
|
3498
4896
|
const key = `${act} \u2192 ${seq[i + 1]}`;
|
|
@@ -3591,15 +4989,15 @@ var DashboardServer = class {
|
|
|
3591
4989
|
/** Check if any src/client file is newer than the built bundle. */
|
|
3592
4990
|
isClientStale(srcDir, distDir) {
|
|
3593
4991
|
try {
|
|
3594
|
-
const distIndex =
|
|
3595
|
-
if (!
|
|
3596
|
-
const distMtime =
|
|
4992
|
+
const distIndex = path4.join(distDir, "index.html");
|
|
4993
|
+
if (!fs4.existsSync(distIndex)) return true;
|
|
4994
|
+
const distMtime = fs4.statSync(distIndex).mtimeMs;
|
|
3597
4995
|
const check = (dir) => {
|
|
3598
|
-
for (const entry of
|
|
3599
|
-
const full =
|
|
4996
|
+
for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
|
|
4997
|
+
const full = path4.join(dir, entry.name);
|
|
3600
4998
|
if (entry.isDirectory()) {
|
|
3601
4999
|
if (check(full)) return true;
|
|
3602
|
-
} else if (
|
|
5000
|
+
} else if (fs4.statSync(full).mtimeMs > distMtime) {
|
|
3603
5001
|
return true;
|
|
3604
5002
|
}
|
|
3605
5003
|
}
|
|
@@ -3625,6 +5023,9 @@ var DashboardServer = class {
|
|
|
3625
5023
|
getStats() {
|
|
3626
5024
|
return this.stats.getGlobalStats();
|
|
3627
5025
|
}
|
|
5026
|
+
getTrace(filename) {
|
|
5027
|
+
return this.watcher.getTrace(filename);
|
|
5028
|
+
}
|
|
3628
5029
|
getTraces() {
|
|
3629
5030
|
return this.watcher.getAllTraces();
|
|
3630
5031
|
}
|