agentflow-dashboard 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/{chunk-GA2Y6E62.js → chunk-NZFXRZYU.js} +529 -206
- package/dist/cli.cjs +533 -210
- package/dist/cli.js +1 -1
- package/dist/client/assets/{index-Ds_npIxI.css → index-CNZqCErb.css} +1 -1
- package/dist/client/assets/index-Cb5C1Pah.js +50 -0
- package/dist/client/index.html +2 -2
- package/dist/index.cjs +533 -210
- package/dist/index.js +1 -1
- package/dist/server.cjs +533 -210
- package/dist/server.js +1 -1
- package/package.json +3 -5
- package/dist/client/assets/index-BQUye2Lc.js +0 -50
- package/dist/client/dashboard.js +0 -3113
- package/dist/public/dashboard.js +0 -3113
- package/dist/public/index.html +0 -1385
- package/public/dashboard.js +0 -3113
- package/public/index.html +0 -1385
package/dist/server.cjs
CHANGED
|
@@ -32,10 +32,84 @@ __export(server_exports, {
|
|
|
32
32
|
DashboardServer: () => DashboardServer
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(server_exports);
|
|
35
|
+
var import_node_child_process = require("child_process");
|
|
35
36
|
var fs3 = __toESM(require("fs"), 1);
|
|
36
37
|
var import_node_http = require("http");
|
|
37
38
|
var path3 = __toESM(require("path"), 1);
|
|
38
39
|
var import_node_url = require("url");
|
|
40
|
+
|
|
41
|
+
// src/config.ts
|
|
42
|
+
var import_node_fs = require("fs");
|
|
43
|
+
var import_node_os = require("os");
|
|
44
|
+
var import_node_path = require("path");
|
|
45
|
+
var EMPTY_CONFIG = {};
|
|
46
|
+
function expandTilde(p) {
|
|
47
|
+
if (p.startsWith("~/") || p === "~") {
|
|
48
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), p.slice(1));
|
|
49
|
+
}
|
|
50
|
+
return p;
|
|
51
|
+
}
|
|
52
|
+
function loadConfig(explicitPath) {
|
|
53
|
+
const candidates = [];
|
|
54
|
+
if (explicitPath) {
|
|
55
|
+
candidates.push((0, import_node_path.resolve)(explicitPath));
|
|
56
|
+
}
|
|
57
|
+
if (process.env.AGENTFLOW_CONFIG) {
|
|
58
|
+
candidates.push((0, import_node_path.resolve)(process.env.AGENTFLOW_CONFIG));
|
|
59
|
+
}
|
|
60
|
+
candidates.push((0, import_node_path.resolve)("agentflow.config.json"));
|
|
61
|
+
candidates.push((0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "agentflow", "config.json"));
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
if (!(0, import_node_fs.existsSync)(candidate)) continue;
|
|
64
|
+
try {
|
|
65
|
+
const raw = (0, import_node_fs.readFileSync)(candidate, "utf-8");
|
|
66
|
+
const parsed = JSON.parse(raw);
|
|
67
|
+
const cleaned = stripCommentKeys(parsed);
|
|
68
|
+
console.log(`Loaded config: ${candidate}`);
|
|
69
|
+
return { config: cleaned, configPath: candidate };
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.warn(`Warning: Failed to load config from ${candidate}: ${err.message}`);
|
|
72
|
+
console.warn("Continuing with empty defaults.");
|
|
73
|
+
return { config: EMPTY_CONFIG, configPath: null };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { config: EMPTY_CONFIG, configPath: null };
|
|
77
|
+
}
|
|
78
|
+
function stripCommentKeys(obj) {
|
|
79
|
+
if (Array.isArray(obj)) return obj.map(stripCommentKeys);
|
|
80
|
+
if (obj && typeof obj === "object") {
|
|
81
|
+
const result = {};
|
|
82
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
83
|
+
if (key.startsWith("//")) continue;
|
|
84
|
+
result[key] = stripCommentKeys(value);
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
return obj;
|
|
89
|
+
}
|
|
90
|
+
function getAliases(config) {
|
|
91
|
+
return config.aliases ?? {};
|
|
92
|
+
}
|
|
93
|
+
function getSkipFiles(config) {
|
|
94
|
+
return config.skipFiles ?? [];
|
|
95
|
+
}
|
|
96
|
+
function getSkipDirectories(config) {
|
|
97
|
+
return config.skipDirectories ?? [];
|
|
98
|
+
}
|
|
99
|
+
function getDiscoveryPaths(config) {
|
|
100
|
+
return (config.discoveryPaths ?? []).map(expandTilde);
|
|
101
|
+
}
|
|
102
|
+
function getSystemdServices(config) {
|
|
103
|
+
return config.systemdServices ?? [];
|
|
104
|
+
}
|
|
105
|
+
function getAgentDetection(config) {
|
|
106
|
+
return config.agentDetection ?? {};
|
|
107
|
+
}
|
|
108
|
+
function getProcessPreference(config) {
|
|
109
|
+
return config.processPreference ?? null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/server.ts
|
|
39
113
|
var import_agentflow_core3 = require("agentflow-core");
|
|
40
114
|
var import_express = __toESM(require("express"), 1);
|
|
41
115
|
var import_ws = require("ws");
|
|
@@ -69,17 +143,17 @@ var AgentFlowAdapter = class {
|
|
|
69
143
|
};
|
|
70
144
|
|
|
71
145
|
// src/adapters/openclaw.ts
|
|
72
|
-
var
|
|
73
|
-
var
|
|
146
|
+
var import_node_fs2 = require("fs");
|
|
147
|
+
var import_node_path2 = require("path");
|
|
74
148
|
var jobCache = /* @__PURE__ */ new Map();
|
|
75
149
|
function loadJobs(openclawDir) {
|
|
76
150
|
const cached = jobCache.get(openclawDir);
|
|
77
151
|
if (cached) return cached;
|
|
78
|
-
const jobsPath = (0,
|
|
152
|
+
const jobsPath = (0, import_node_path2.join)(openclawDir, "cron", "jobs.json");
|
|
79
153
|
const map = /* @__PURE__ */ new Map();
|
|
80
154
|
try {
|
|
81
|
-
if ((0,
|
|
82
|
-
const data = JSON.parse((0,
|
|
155
|
+
if ((0, import_node_fs2.existsSync)(jobsPath)) {
|
|
156
|
+
const data = JSON.parse((0, import_node_fs2.readFileSync)(jobsPath, "utf-8"));
|
|
83
157
|
const jobs = Array.isArray(data) ? data : data.jobs ?? [];
|
|
84
158
|
for (const job of jobs) {
|
|
85
159
|
if (job.id) map.set(job.id, job);
|
|
@@ -91,19 +165,19 @@ function loadJobs(openclawDir) {
|
|
|
91
165
|
return map;
|
|
92
166
|
}
|
|
93
167
|
function findOpenClawRoot(filePath) {
|
|
94
|
-
let dir = (0,
|
|
168
|
+
let dir = (0, import_node_path2.dirname)(filePath);
|
|
95
169
|
for (let i = 0; i < 5; i++) {
|
|
96
|
-
if ((0,
|
|
170
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, "cron", "jobs.json")) || (0, import_node_path2.basename)(dir) === ".openclaw") {
|
|
97
171
|
return dir;
|
|
98
172
|
}
|
|
99
|
-
dir = (0,
|
|
173
|
+
dir = (0, import_node_path2.dirname)(dir);
|
|
100
174
|
}
|
|
101
175
|
return null;
|
|
102
176
|
}
|
|
103
177
|
var OpenClawAdapter = class {
|
|
104
178
|
name = "openclaw";
|
|
105
179
|
detect(dirPath) {
|
|
106
|
-
return (0,
|
|
180
|
+
return (0, import_node_fs2.existsSync)((0, import_node_path2.join)(dirPath, "cron", "jobs.json")) || dirPath.includes(".openclaw") || (0, import_node_fs2.existsSync)((0, import_node_path2.join)(dirPath, "cron", "runs"));
|
|
107
181
|
}
|
|
108
182
|
canHandle(filePath) {
|
|
109
183
|
if (!filePath.endsWith(".jsonl")) return false;
|
|
@@ -112,7 +186,7 @@ var OpenClawAdapter = class {
|
|
|
112
186
|
parse(filePath) {
|
|
113
187
|
const traces = [];
|
|
114
188
|
try {
|
|
115
|
-
const content = (0,
|
|
189
|
+
const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
116
190
|
const root = findOpenClawRoot(filePath);
|
|
117
191
|
const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
|
|
118
192
|
for (const line of content.split("\n")) {
|
|
@@ -124,7 +198,7 @@ var OpenClawAdapter = class {
|
|
|
124
198
|
continue;
|
|
125
199
|
}
|
|
126
200
|
if (entry.action !== "finished") continue;
|
|
127
|
-
const jobId = entry.jobId ?? (0,
|
|
201
|
+
const jobId = entry.jobId ?? (0, import_node_path2.basename)(filePath, ".jsonl");
|
|
128
202
|
const job = jobs.get(jobId);
|
|
129
203
|
const jobName = (job == null ? void 0 : job.name) ?? jobId;
|
|
130
204
|
const startTime = entry.runAtMs ?? entry.ts;
|
|
@@ -176,8 +250,8 @@ var OpenClawAdapter = class {
|
|
|
176
250
|
};
|
|
177
251
|
|
|
178
252
|
// src/adapters/otel.ts
|
|
179
|
-
var
|
|
180
|
-
var
|
|
253
|
+
var import_node_fs3 = require("fs");
|
|
254
|
+
var import_node_path3 = require("path");
|
|
181
255
|
var SPAN_TYPE_MAP = {
|
|
182
256
|
"gen_ai.chat": "llm",
|
|
183
257
|
"gen_ai.completion": "llm",
|
|
@@ -284,8 +358,8 @@ var OTelAdapter = class {
|
|
|
284
358
|
name = "otel";
|
|
285
359
|
detect(dirPath) {
|
|
286
360
|
try {
|
|
287
|
-
if ((0,
|
|
288
|
-
const files = (0,
|
|
361
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(dirPath, "otel-traces"))) return true;
|
|
362
|
+
const files = (0, import_node_fs3.readdirSync)(dirPath);
|
|
289
363
|
return files.some((f) => f.endsWith(".otlp.json"));
|
|
290
364
|
} catch {
|
|
291
365
|
return false;
|
|
@@ -296,7 +370,7 @@ var OTelAdapter = class {
|
|
|
296
370
|
}
|
|
297
371
|
parse(filePath) {
|
|
298
372
|
try {
|
|
299
|
-
const content = (0,
|
|
373
|
+
const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
|
|
300
374
|
const payload = JSON.parse(content);
|
|
301
375
|
const traces = parseOtlpPayload(payload);
|
|
302
376
|
for (const t of traces) t.filePath = filePath;
|
|
@@ -338,9 +412,7 @@ function extractSource(agentId) {
|
|
|
338
412
|
const colonIdx = agentId.indexOf(":");
|
|
339
413
|
if (colonIdx > 0 && colonIdx < 20) {
|
|
340
414
|
const prefix = agentId.slice(0, colonIdx);
|
|
341
|
-
|
|
342
|
-
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
343
|
-
}
|
|
415
|
+
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
344
416
|
}
|
|
345
417
|
return { source: "agentflow", localId: agentId };
|
|
346
418
|
}
|
|
@@ -371,16 +443,20 @@ function deduplicateAgents(agents) {
|
|
|
371
443
|
for (const a of tagged) {
|
|
372
444
|
const suffix = extractSuffix(a.localId);
|
|
373
445
|
if (!suffix) continue;
|
|
374
|
-
const
|
|
446
|
+
const key = `${a.source}:${suffix}`;
|
|
447
|
+
const group = suffixGroups.get(key) ?? [];
|
|
375
448
|
group.push(a);
|
|
376
|
-
suffixGroups.set(
|
|
449
|
+
suffixGroups.set(key, group);
|
|
377
450
|
}
|
|
378
451
|
const mergedIds = /* @__PURE__ */ new Set();
|
|
379
452
|
const mergedAgents = [];
|
|
380
|
-
for (const [
|
|
453
|
+
for (const [_key, group] of suffixGroups) {
|
|
454
|
+
const suffix = extractSuffix(group[0].localId);
|
|
381
455
|
if (group.length < 2) continue;
|
|
382
456
|
const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
|
|
383
457
|
if (prefixes.size < 2) continue;
|
|
458
|
+
const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
|
|
459
|
+
if (longPrefixes.length >= 2) continue;
|
|
384
460
|
const merged = {
|
|
385
461
|
agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
|
|
386
462
|
displayName: suffix,
|
|
@@ -428,10 +504,7 @@ function groupAgents(agents) {
|
|
|
428
504
|
}
|
|
429
505
|
const SOURCE_DISPLAY = {
|
|
430
506
|
agentflow: "AgentFlow",
|
|
431
|
-
|
|
432
|
-
otel: "OpenTelemetry",
|
|
433
|
-
langchain: "LangChain",
|
|
434
|
-
crewai: "CrewAI"
|
|
507
|
+
otel: "OpenTelemetry"
|
|
435
508
|
};
|
|
436
509
|
const groups = [];
|
|
437
510
|
for (const [source, sourceAgents] of sourceMap) {
|
|
@@ -777,10 +850,6 @@ function getUniversalNodeStatus(activity) {
|
|
|
777
850
|
return "completed";
|
|
778
851
|
}
|
|
779
852
|
function openClawSessionIdToAgent(sessionId) {
|
|
780
|
-
if (sessionId.startsWith("janitor-")) return "vault-janitor";
|
|
781
|
-
if (sessionId.startsWith("curator-")) return "vault-curator";
|
|
782
|
-
if (sessionId.startsWith("distiller-")) return "vault-distiller";
|
|
783
|
-
if (sessionId.startsWith("main-")) return "alfred-main";
|
|
784
853
|
const firstSegment = sessionId.split("-")[0];
|
|
785
854
|
if (firstSegment) return firstSegment;
|
|
786
855
|
return "openclaw";
|
|
@@ -793,19 +862,81 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
793
862
|
tracesDir;
|
|
794
863
|
dataDirs;
|
|
795
864
|
allWatchDirs;
|
|
865
|
+
maxAgeMs;
|
|
866
|
+
userConfig;
|
|
796
867
|
constructor(tracesDirOrOptions) {
|
|
797
868
|
super();
|
|
869
|
+
const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
|
|
870
|
+
const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
|
|
871
|
+
const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
|
|
798
872
|
if (typeof tracesDirOrOptions === "string") {
|
|
799
873
|
this.tracesDir = path.resolve(tracesDirOrOptions);
|
|
800
874
|
this.dataDirs = [];
|
|
875
|
+
this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
|
|
876
|
+
this.userConfig = {};
|
|
801
877
|
} else {
|
|
802
878
|
this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
|
|
803
879
|
this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
|
|
804
|
-
|
|
880
|
+
this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
|
|
881
|
+
this.userConfig = tracesDirOrOptions.userConfig ?? {};
|
|
882
|
+
}
|
|
883
|
+
this.skipFiles = /* @__PURE__ */ new Set([
|
|
884
|
+
..._TraceWatcher.STRUCTURAL_SKIP_FILES,
|
|
885
|
+
...getSkipFiles(this.userConfig)
|
|
886
|
+
]);
|
|
887
|
+
this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
|
|
805
888
|
this.allWatchDirs = [this.tracesDir, ...this.dataDirs];
|
|
806
889
|
this.ensureTracesDir();
|
|
807
890
|
this.loadExistingFiles();
|
|
891
|
+
this.archiveOldTraces();
|
|
808
892
|
this.startWatching();
|
|
893
|
+
setInterval(() => this.archiveOldTraces(), 6 * 60 * 60 * 1e3);
|
|
894
|
+
}
|
|
895
|
+
/** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
|
|
896
|
+
archiveOldTraces() {
|
|
897
|
+
const cutoff = Date.now() - this.maxAgeMs;
|
|
898
|
+
let archived = 0;
|
|
899
|
+
for (const dir of this.allWatchDirs) {
|
|
900
|
+
if (!fs.existsSync(dir)) continue;
|
|
901
|
+
try {
|
|
902
|
+
this.archiveDirectory(dir, cutoff, 0);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
console.warn(`Archival error in ${dir}:`, error.message);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
archiveDirectory(dir, cutoff, depth) {
|
|
909
|
+
if (depth > 10) return 0;
|
|
910
|
+
if (path.basename(dir) === "archive") return 0;
|
|
911
|
+
let archived = 0;
|
|
912
|
+
try {
|
|
913
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
914
|
+
for (const entry of entries) {
|
|
915
|
+
if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
|
|
916
|
+
const fullPath = path.join(dir, entry.name);
|
|
917
|
+
if (entry.isDirectory()) {
|
|
918
|
+
archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
|
|
922
|
+
try {
|
|
923
|
+
const stats = fs.statSync(fullPath);
|
|
924
|
+
if (stats.mtimeMs >= cutoff) continue;
|
|
925
|
+
const mtime = new Date(stats.mtimeMs);
|
|
926
|
+
const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
|
|
927
|
+
const archiveDir = path.join(this.tracesDir, "archive", yearMonth);
|
|
928
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
929
|
+
const dest = path.join(archiveDir, entry.name);
|
|
930
|
+
fs.renameSync(fullPath, dest);
|
|
931
|
+
const key = this.traceKey(fullPath);
|
|
932
|
+
this.traces.delete(key);
|
|
933
|
+
archived++;
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
} catch {
|
|
938
|
+
}
|
|
939
|
+
return archived;
|
|
809
940
|
}
|
|
810
941
|
ensureTracesDir() {
|
|
811
942
|
if (!fs.existsSync(this.tracesDir)) {
|
|
@@ -838,9 +969,17 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
838
969
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
839
970
|
for (const entry of entries) {
|
|
840
971
|
if (entry.name.startsWith(".")) continue;
|
|
972
|
+
if (entry.name === "archive") continue;
|
|
973
|
+
if (this.userSkipDirs.has(entry.name)) continue;
|
|
841
974
|
const fullPath = path.join(dir, entry.name);
|
|
842
975
|
if (entry.isFile()) {
|
|
843
976
|
if (this.isSupportedFile(entry.name)) {
|
|
977
|
+
try {
|
|
978
|
+
const mtime = fs.statSync(fullPath).mtimeMs;
|
|
979
|
+
if (Date.now() - mtime > this.maxAgeMs) continue;
|
|
980
|
+
} catch {
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
844
983
|
if (this.loadFile(fullPath)) {
|
|
845
984
|
fileCount++;
|
|
846
985
|
}
|
|
@@ -858,8 +997,8 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
858
997
|
isSupportedFile(filename) {
|
|
859
998
|
return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
|
|
860
999
|
}
|
|
861
|
-
/**
|
|
862
|
-
static
|
|
1000
|
+
/** Structural file names that are never trace data — always skipped. */
|
|
1001
|
+
static STRUCTURAL_SKIP_FILES = /* @__PURE__ */ new Set([
|
|
863
1002
|
"workers.json",
|
|
864
1003
|
"package.json",
|
|
865
1004
|
"package-lock.json",
|
|
@@ -874,6 +1013,10 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
874
1013
|
"update-check.json",
|
|
875
1014
|
"exec-approvals.json"
|
|
876
1015
|
]);
|
|
1016
|
+
/** Skip files = structural + user config */
|
|
1017
|
+
skipFiles;
|
|
1018
|
+
/** Skip directories from user config */
|
|
1019
|
+
userSkipDirs;
|
|
877
1020
|
static SKIP_SUFFIXES = [
|
|
878
1021
|
"-state.json",
|
|
879
1022
|
"-config.json",
|
|
@@ -885,7 +1028,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
885
1028
|
/** Load a file using the adapter registry, falling back to built-in parsing. */
|
|
886
1029
|
loadFile(filePath) {
|
|
887
1030
|
const filename = path.basename(filePath);
|
|
888
|
-
if (
|
|
1031
|
+
if (this.skipFiles.has(filename)) return false;
|
|
889
1032
|
if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
|
|
890
1033
|
const adapter = findAdapter(filePath);
|
|
891
1034
|
if (adapter && adapter.name !== "agentflow") {
|
|
@@ -1062,43 +1205,26 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1062
1205
|
}
|
|
1063
1206
|
return traces;
|
|
1064
1207
|
}
|
|
1065
|
-
/**
|
|
1066
|
-
* Normalise agent identifiers so that the same worker is never shown
|
|
1067
|
-
* under two different names (e.g. "vault-curator" vs "openclaw-vault-curator").
|
|
1068
|
-
*
|
|
1069
|
-
* Canonical names: alfred-main, vault-curator, vault-janitor,
|
|
1070
|
-
* vault-distiller, vault-surveyor
|
|
1071
|
-
*/
|
|
1072
|
-
static AGENT_ALIASES = {
|
|
1073
|
-
"openclaw-main": "alfred-main",
|
|
1074
|
-
"openclaw-vault-curator": "vault-curator",
|
|
1075
|
-
"openclaw-vault-janitor": "vault-janitor",
|
|
1076
|
-
"openclaw-vault-distiller": "vault-distiller",
|
|
1077
|
-
"openclaw-vault-surveyor": "vault-surveyor",
|
|
1078
|
-
"alfred-curator": "vault-curator",
|
|
1079
|
-
"alfred-janitor": "vault-janitor",
|
|
1080
|
-
"alfred-distiller": "vault-distiller",
|
|
1081
|
-
"alfred-surveyor": "vault-surveyor",
|
|
1082
|
-
curator: "vault-curator",
|
|
1083
|
-
janitor: "vault-janitor",
|
|
1084
|
-
distiller: "vault-distiller",
|
|
1085
|
-
surveyor: "vault-surveyor"
|
|
1086
|
-
};
|
|
1208
|
+
/** Normalise agent identifiers using config-driven alias map. */
|
|
1087
1209
|
normaliseAgentId(raw) {
|
|
1088
|
-
|
|
1210
|
+
const aliases = getAliases(this.userConfig);
|
|
1211
|
+
return aliases[raw] ?? raw;
|
|
1089
1212
|
}
|
|
1090
1213
|
detectAgentIdentifier(activity, _filename, filePath) {
|
|
1091
1214
|
if (activity.agent_id) {
|
|
1092
|
-
|
|
1093
|
-
if (agentId === "main" && filePath.includes(".alfred/")) return this.normaliseAgentId("alfred-main");
|
|
1094
|
-
return this.normaliseAgentId(agentId);
|
|
1215
|
+
return this.normaliseAgentId(activity.agent_id);
|
|
1095
1216
|
}
|
|
1096
1217
|
const pathAgent = this.extractAgentFromPath(filePath);
|
|
1097
|
-
|
|
1218
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1219
|
+
if (detection.filePatterns) {
|
|
1098
1220
|
const basename3 = path.basename(filePath, path.extname(filePath));
|
|
1099
|
-
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1221
|
+
for (const [pattern, template] of Object.entries(detection.filePatterns)) {
|
|
1222
|
+
const re = new RegExp(`^(${pattern})$`);
|
|
1223
|
+
const match = basename3.match(re);
|
|
1224
|
+
if (match) {
|
|
1225
|
+
const resolved = template.replace("${match}", match[1]);
|
|
1226
|
+
return this.normaliseAgentId(resolved);
|
|
1227
|
+
}
|
|
1102
1228
|
}
|
|
1103
1229
|
}
|
|
1104
1230
|
return this.normaliseAgentId(pathAgent);
|
|
@@ -1106,20 +1232,23 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1106
1232
|
extractAgentFromPath(filePath) {
|
|
1107
1233
|
const filename = path.basename(filePath, path.extname(filePath));
|
|
1108
1234
|
const pathParts = filePath.split(path.sep);
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1235
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1236
|
+
let pathPrefix = "";
|
|
1237
|
+
if (detection.pathPatterns) {
|
|
1238
|
+
for (const [pathSubstring, agentId] of Object.entries(detection.pathPatterns)) {
|
|
1239
|
+
if (filePath.includes(pathSubstring)) {
|
|
1240
|
+
pathPrefix = agentId;
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1116
1243
|
}
|
|
1117
|
-
return "openclaw";
|
|
1118
1244
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1245
|
+
const agentsIndex = pathParts.lastIndexOf("agents");
|
|
1246
|
+
if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
|
|
1247
|
+
const agentName = pathParts[agentsIndex + 1];
|
|
1248
|
+
return pathPrefix ? `${pathPrefix}-${agentName}` : agentName;
|
|
1121
1249
|
}
|
|
1122
|
-
|
|
1250
|
+
if (pathPrefix) return pathPrefix;
|
|
1251
|
+
for (const part of [...pathParts].reverse()) {
|
|
1123
1252
|
if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
|
|
1124
1253
|
return part;
|
|
1125
1254
|
}
|
|
@@ -1454,19 +1583,22 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1454
1583
|
const parentDir = path.basename(path.dirname(filePath));
|
|
1455
1584
|
const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
|
|
1456
1585
|
const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
|
|
1457
|
-
let
|
|
1586
|
+
let agentName;
|
|
1458
1587
|
if (parentDir === "sessions" && greatGrandParentDir === "agents") {
|
|
1459
|
-
|
|
1588
|
+
agentName = grandParentDir;
|
|
1460
1589
|
} else if (grandParentDir === "agents") {
|
|
1461
|
-
|
|
1462
|
-
} else if (parentDir === "runs" && grandParentDir === "cron") {
|
|
1463
|
-
agentId = "openclaw-cron";
|
|
1590
|
+
agentName = parentDir;
|
|
1464
1591
|
} else {
|
|
1465
|
-
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1592
|
+
agentName = parentDir;
|
|
1593
|
+
}
|
|
1594
|
+
let agentId = agentName;
|
|
1595
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1596
|
+
if (detection.pathPatterns) {
|
|
1597
|
+
for (const [pathSubstring, prefix] of Object.entries(detection.pathPatterns)) {
|
|
1598
|
+
if (filePath.includes(pathSubstring)) {
|
|
1599
|
+
agentId = `${prefix}-${agentName}`;
|
|
1600
|
+
break;
|
|
1601
|
+
}
|
|
1470
1602
|
}
|
|
1471
1603
|
}
|
|
1472
1604
|
const modelEvent = rawEvents.find((e) => e.type === "model_change");
|
|
@@ -1755,6 +1887,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1755
1887
|
edges: [],
|
|
1756
1888
|
events: [],
|
|
1757
1889
|
startTime,
|
|
1890
|
+
status,
|
|
1758
1891
|
agentId,
|
|
1759
1892
|
trigger,
|
|
1760
1893
|
name: rootName,
|
|
@@ -1875,8 +2008,12 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1875
2008
|
// Ignore git directories
|
|
1876
2009
|
/\.vscode/,
|
|
1877
2010
|
// Ignore vscode
|
|
1878
|
-
/\.idea
|
|
2011
|
+
/\.idea/,
|
|
1879
2012
|
// Ignore idea
|
|
2013
|
+
/\/archive\//,
|
|
2014
|
+
// Ignore archived trace files
|
|
2015
|
+
// Ignore user-configured skip directories
|
|
2016
|
+
...getSkipDirectories(this.userConfig).map((d) => new RegExp(`/${d}/`))
|
|
1880
2017
|
],
|
|
1881
2018
|
persistent: true,
|
|
1882
2019
|
ignoreInitial: true,
|
|
@@ -1930,29 +2067,42 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1930
2067
|
});
|
|
1931
2068
|
}
|
|
1932
2069
|
getTrace(filename) {
|
|
2070
|
+
const candidates = [];
|
|
1933
2071
|
const exact = this.traces.get(filename);
|
|
1934
|
-
if (exact)
|
|
2072
|
+
if (exact) candidates.push(exact);
|
|
1935
2073
|
if (filename.includes("::")) {
|
|
1936
2074
|
const [fname, startTimeStr] = filename.split("::");
|
|
1937
2075
|
const startTime = Number(startTimeStr);
|
|
1938
2076
|
if (fname && !Number.isNaN(startTime)) {
|
|
1939
2077
|
for (const trace of this.traces.values()) {
|
|
1940
2078
|
if (trace.filename === fname && trace.startTime === startTime) {
|
|
1941
|
-
|
|
2079
|
+
candidates.push(trace);
|
|
1942
2080
|
}
|
|
1943
2081
|
}
|
|
1944
2082
|
}
|
|
1945
2083
|
}
|
|
1946
2084
|
for (const prefix of ["openclaw:", "otel:", ""]) {
|
|
1947
2085
|
const prefixed = this.traces.get(prefix + filename);
|
|
1948
|
-
if (prefixed)
|
|
2086
|
+
if (prefixed) candidates.push(prefixed);
|
|
1949
2087
|
}
|
|
1950
2088
|
for (const [key, trace] of this.traces) {
|
|
1951
2089
|
if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
|
|
1952
|
-
|
|
2090
|
+
candidates.push(trace);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
if (candidates.length === 0) return void 0;
|
|
2094
|
+
if (candidates.length === 1) return candidates[0];
|
|
2095
|
+
let best = candidates[0];
|
|
2096
|
+
let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
|
|
2097
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
2098
|
+
const c = candidates[i];
|
|
2099
|
+
const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
|
|
2100
|
+
if (nc > bestNodeCount) {
|
|
2101
|
+
best = c;
|
|
2102
|
+
bestNodeCount = nc;
|
|
1953
2103
|
}
|
|
1954
2104
|
}
|
|
1955
|
-
return
|
|
2105
|
+
return best;
|
|
1956
2106
|
}
|
|
1957
2107
|
getTracesByAgent(agentId) {
|
|
1958
2108
|
return this.getAllTraces().filter((trace) => trace.agentId === agentId);
|
|
@@ -1999,7 +2149,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1999
2149
|
var fs2 = __toESM(require("fs"), 1);
|
|
2000
2150
|
var os = __toESM(require("os"), 1);
|
|
2001
2151
|
var path2 = __toESM(require("path"), 1);
|
|
2002
|
-
var VERSION = "0.
|
|
2152
|
+
var VERSION = "0.8.0";
|
|
2003
2153
|
function getLanAddress() {
|
|
2004
2154
|
const interfaces = os.networkInterfaces();
|
|
2005
2155
|
for (const name of Object.keys(interfaces)) {
|
|
@@ -2011,7 +2161,7 @@ function getLanAddress() {
|
|
|
2011
2161
|
}
|
|
2012
2162
|
return null;
|
|
2013
2163
|
}
|
|
2014
|
-
function printBanner(config, traceCount, stats) {
|
|
2164
|
+
function printBanner(config, traceCount, stats, configPath) {
|
|
2015
2165
|
var _a;
|
|
2016
2166
|
const lan = getLanAddress();
|
|
2017
2167
|
const host = config.host || "localhost";
|
|
@@ -2027,26 +2177,23 @@ function printBanner(config, traceCount, stats) {
|
|
|
2027
2177
|
|
|
2028
2178
|
See your agents think.
|
|
2029
2179
|
|
|
2030
|
-
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
2031
|
-
\u2502 \u{1F916} Agents \u2502 TRACE FILES \u2502 \u{1F4CA} AgentFlow \u2502 SHOWS YOU \u2502 \u{1F310} Your browser \u2502
|
|
2032
|
-
\u2502 Execute tasks, \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500> \u2502 Reads traces, \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500> \u2502 Interactive \u2502
|
|
2033
|
-
\u2502 write JSON \u2502 \u2502 builds graphs, \u2502 \u2502 graph, timeline, \u2502
|
|
2034
|
-
\u2502 trace files. \u2502 \u2502 serves dashboard.\u2502 \u2502 metrics, health. \u2502
|
|
2035
|
-
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
2036
|
-
|
|
2037
|
-
Runs locally. Your data never leaves your machine.
|
|
2038
|
-
|
|
2039
|
-
Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
|
|
2040
|
-
|
|
2041
2180
|
Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? `
|
|
2042
2181
|
Data dirs: ${config.dataDirs.join("\n ")}` : ""}
|
|
2043
2182
|
Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
|
|
2044
2183
|
Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
|
|
2184
|
+
Config: ${configPath ?? "none (using defaults)"}
|
|
2045
2185
|
CORS: ${config.enableCors ? "enabled" : "disabled"}
|
|
2046
2186
|
WebSocket: live updates enabled
|
|
2187
|
+
Window: ${process.env.AGENTFLOW_TRACE_WINDOW_HOURS ?? "48"}h (set AGENTFLOW_TRACE_WINDOW_HOURS to change)
|
|
2047
2188
|
|
|
2048
2189
|
\u2192 http://localhost:${port}${isPublic && lan ? `
|
|
2049
2190
|
\u2192 http://${lan}:${port} (LAN)` : ""}
|
|
2191
|
+
|
|
2192
|
+
Views: Agent Profile \xB7 Execution Detail \xB7 Governance
|
|
2193
|
+
Tabs: Flame Chart \xB7 Agent Flow \xB7 Metrics \xB7 Dependencies
|
|
2194
|
+
State Machine \xB7 Summary \xB7 Transcript
|
|
2195
|
+
|
|
2196
|
+
Runs locally. Your data never leaves your machine.
|
|
2050
2197
|
`);
|
|
2051
2198
|
}
|
|
2052
2199
|
async function startDashboard() {
|
|
@@ -2078,11 +2225,32 @@ async function startDashboard() {
|
|
|
2078
2225
|
case "--cors":
|
|
2079
2226
|
config.enableCors = true;
|
|
2080
2227
|
break;
|
|
2228
|
+
case "--no-collector":
|
|
2229
|
+
config.enableCollector = false;
|
|
2230
|
+
break;
|
|
2231
|
+
case "--collector-token":
|
|
2232
|
+
config.collectorAuthToken = args[++i];
|
|
2233
|
+
break;
|
|
2234
|
+
case "--soma-vault":
|
|
2235
|
+
config.somaVault = args[++i];
|
|
2236
|
+
break;
|
|
2237
|
+
case "--config":
|
|
2238
|
+
config.configPath = args[++i];
|
|
2239
|
+
break;
|
|
2081
2240
|
case "--help":
|
|
2082
2241
|
printHelp();
|
|
2083
2242
|
process.exit(0);
|
|
2084
2243
|
}
|
|
2085
2244
|
}
|
|
2245
|
+
if (!config.collectorAuthToken && process.env.AGENTFLOW_COLLECTOR_TOKEN) {
|
|
2246
|
+
config.collectorAuthToken = process.env.AGENTFLOW_COLLECTOR_TOKEN;
|
|
2247
|
+
}
|
|
2248
|
+
if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
|
|
2249
|
+
config.enableCollector = false;
|
|
2250
|
+
}
|
|
2251
|
+
if (!config.somaVault && process.env.SOMA_VAULT) {
|
|
2252
|
+
config.somaVault = process.env.SOMA_VAULT;
|
|
2253
|
+
}
|
|
2086
2254
|
const tracesPath = path2.resolve(config.tracesDir);
|
|
2087
2255
|
if (!fs2.existsSync(tracesPath)) {
|
|
2088
2256
|
fs2.mkdirSync(tracesPath, { recursive: true });
|
|
@@ -2104,7 +2272,7 @@ async function startDashboard() {
|
|
|
2104
2272
|
setTimeout(() => {
|
|
2105
2273
|
const stats = dashboard.getStats();
|
|
2106
2274
|
const traces = dashboard.getTraces();
|
|
2107
|
-
printBanner(config, traces.length, stats);
|
|
2275
|
+
printBanner(config, traces.length, stats, dashboard.getConfigPath());
|
|
2108
2276
|
}, 1500);
|
|
2109
2277
|
} catch (error) {
|
|
2110
2278
|
console.error("\u274C Failed to start dashboard:", error);
|
|
@@ -2113,7 +2281,7 @@ async function startDashboard() {
|
|
|
2113
2281
|
}
|
|
2114
2282
|
function printHelp() {
|
|
2115
2283
|
console.log(`
|
|
2116
|
-
|
|
2284
|
+
AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
|
|
2117
2285
|
|
|
2118
2286
|
Usage:
|
|
2119
2287
|
agentflow-dashboard [options]
|
|
@@ -2124,20 +2292,34 @@ Options:
|
|
|
2124
2292
|
-t, --traces <path> Traces directory (default: ./traces)
|
|
2125
2293
|
-h, --host <address> Host address (default: localhost)
|
|
2126
2294
|
--data-dir <path> Extra data directory for process discovery (repeatable)
|
|
2295
|
+
--config <path> Path to agentflow.config.json (aliases, skip files, etc.)
|
|
2296
|
+
--soma-vault <path> SOMA vault directory for intelligence data
|
|
2127
2297
|
--cors Enable CORS headers
|
|
2298
|
+
--no-collector Disable OTLP trace collector (POST /v1/traces)
|
|
2299
|
+
--collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
|
|
2128
2300
|
--help Show this help message
|
|
2129
2301
|
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2302
|
+
Config file:
|
|
2303
|
+
The dashboard loads agentflow.config.json for agent aliases, skip files,
|
|
2304
|
+
discovery paths, and systemd services. Resolution order:
|
|
2305
|
+
1. --config flag
|
|
2306
|
+
2. AGENTFLOW_CONFIG env var
|
|
2307
|
+
3. ./agentflow.config.json
|
|
2308
|
+
4. ~/.config/agentflow/config.json
|
|
2309
|
+
|
|
2310
|
+
See agentflow.config.example.json for a complete reference.
|
|
2311
|
+
|
|
2312
|
+
Environment:
|
|
2313
|
+
AGENTFLOW_CONFIG Path to config file
|
|
2314
|
+
AGENTFLOW_TRACE_WINDOW_HOURS Max age of traces to load (default: 48)
|
|
2315
|
+
AGENTFLOW_COLLECTOR_TOKEN Auth token for OTLP collector
|
|
2316
|
+
AGENTFLOW_NO_COLLECTOR=true Disable OTLP collector
|
|
2317
|
+
SOMA_VAULT SOMA vault directory
|
|
2134
2318
|
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
\u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
|
|
2140
|
-
\u26A0\uFE0F Errors Failed and hung nodes with metadata
|
|
2319
|
+
Examples:
|
|
2320
|
+
agentflow-dashboard --traces ./traces --host 0.0.0.0
|
|
2321
|
+
agentflow-dashboard --traces ./traces --config ./agentflow.config.json
|
|
2322
|
+
agentflow-dashboard -p 8080 -t /var/log/agentflow --cors
|
|
2141
2323
|
`);
|
|
2142
2324
|
}
|
|
2143
2325
|
|
|
@@ -2160,12 +2342,15 @@ function serializeTrace(trace) {
|
|
|
2160
2342
|
var DashboardServer = class {
|
|
2161
2343
|
constructor(config) {
|
|
2162
2344
|
this.config = config;
|
|
2163
|
-
const
|
|
2164
|
-
|
|
2345
|
+
const { config: userCfg, configPath: cfgPath } = loadConfig(config.configPath);
|
|
2346
|
+
this.userConfig = userCfg;
|
|
2347
|
+
this.configPath = cfgPath;
|
|
2348
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
2349
|
+
const dashConfigPath = path3.join(home, ".agentflow/dashboard-config.json");
|
|
2165
2350
|
if (!config.dataDirs) config.dataDirs = [];
|
|
2166
2351
|
try {
|
|
2167
|
-
if (fs3.existsSync(
|
|
2168
|
-
const saved = JSON.parse(fs3.readFileSync(
|
|
2352
|
+
if (fs3.existsSync(dashConfigPath)) {
|
|
2353
|
+
const saved = JSON.parse(fs3.readFileSync(dashConfigPath, "utf-8"));
|
|
2169
2354
|
const extraDirs = saved.extraDirs ?? [];
|
|
2170
2355
|
for (const d of extraDirs) {
|
|
2171
2356
|
if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
|
|
@@ -2173,21 +2358,15 @@ var DashboardServer = class {
|
|
|
2173
2358
|
}
|
|
2174
2359
|
} catch {
|
|
2175
2360
|
}
|
|
2176
|
-
const
|
|
2177
|
-
path3.join(home, ".openclaw/cron/runs"),
|
|
2178
|
-
path3.join(home, ".openclaw/workspace/traces"),
|
|
2179
|
-
path3.join(home, ".openclaw/subagents"),
|
|
2180
|
-
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2181
|
-
path3.join(home, ".agentflow/traces")
|
|
2182
|
-
];
|
|
2183
|
-
for (const p of autoDiscoverPaths) {
|
|
2361
|
+
for (const p of getDiscoveryPaths(this.userConfig)) {
|
|
2184
2362
|
if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
|
|
2185
2363
|
config.dataDirs.push(p);
|
|
2186
2364
|
}
|
|
2187
2365
|
}
|
|
2188
2366
|
this.watcher = new TraceWatcher({
|
|
2189
2367
|
tracesDir: config.tracesDir,
|
|
2190
|
-
dataDirs: config.dataDirs
|
|
2368
|
+
dataDirs: config.dataDirs,
|
|
2369
|
+
userConfig: this.userConfig
|
|
2191
2370
|
});
|
|
2192
2371
|
this.stats = new AgentStats();
|
|
2193
2372
|
this.knowledgeStore = (0, import_agentflow_core3.createKnowledgeStore)({
|
|
@@ -2222,6 +2401,8 @@ var DashboardServer = class {
|
|
|
2222
2401
|
ts: 0
|
|
2223
2402
|
};
|
|
2224
2403
|
knowledgeStore;
|
|
2404
|
+
userConfig;
|
|
2405
|
+
configPath;
|
|
2225
2406
|
setupExpress() {
|
|
2226
2407
|
if (this.config.enableCors) {
|
|
2227
2408
|
this.app.use((_req, res, next) => {
|
|
@@ -2233,18 +2414,35 @@ var DashboardServer = class {
|
|
|
2233
2414
|
next();
|
|
2234
2415
|
});
|
|
2235
2416
|
}
|
|
2236
|
-
const
|
|
2417
|
+
const pkgDir = path3.join(__dirname, "..");
|
|
2418
|
+
const clientDir = path3.join(pkgDir, "dist/client");
|
|
2419
|
+
const clientIndex = path3.join(clientDir, "index.html");
|
|
2420
|
+
const srcDir = path3.join(pkgDir, "src/client");
|
|
2421
|
+
const needsBuild = !fs3.existsSync(clientIndex) || fs3.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
|
|
2422
|
+
if (needsBuild) {
|
|
2423
|
+
try {
|
|
2424
|
+
console.log("Building dashboard client...");
|
|
2425
|
+
(0, import_node_child_process.execSync)("npm run build:client", { cwd: pkgDir, stdio: "inherit", timeout: 3e4 });
|
|
2426
|
+
} catch (err) {
|
|
2427
|
+
console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2237
2430
|
if (fs3.existsSync(clientDir)) {
|
|
2238
2431
|
this.app.use(import_express.default.static(clientDir));
|
|
2239
2432
|
}
|
|
2240
|
-
|
|
2241
|
-
if (fs3.existsSync(publicDir)) {
|
|
2242
|
-
this.app.use("/v1", import_express.default.static(publicDir));
|
|
2243
|
-
}
|
|
2244
|
-
this.app.get("/api/traces", (_req, res) => {
|
|
2433
|
+
this.app.get("/api/traces", (req, res) => {
|
|
2245
2434
|
try {
|
|
2246
|
-
const
|
|
2247
|
-
|
|
2435
|
+
const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
|
|
2436
|
+
const cursor = req.query.cursor ? parseFloat(req.query.cursor) : void 0;
|
|
2437
|
+
let allTraces = this.watcher.getAllTraces();
|
|
2438
|
+
if (cursor) {
|
|
2439
|
+
allTraces = allTraces.filter((t) => (t.lastModified || t.startTime) < cursor);
|
|
2440
|
+
}
|
|
2441
|
+
const page = allTraces.slice(0, limit);
|
|
2442
|
+
const serialized = page.map(serializeTrace);
|
|
2443
|
+
const lastTrace = page[page.length - 1];
|
|
2444
|
+
const nextCursor = page.length === limit && lastTrace ? lastTrace.lastModified || lastTrace.startTime : null;
|
|
2445
|
+
res.json({ traces: serialized, nextCursor });
|
|
2248
2446
|
} catch (_error) {
|
|
2249
2447
|
res.status(500).json({ error: "Failed to load traces" });
|
|
2250
2448
|
}
|
|
@@ -2535,6 +2733,102 @@ var DashboardServer = class {
|
|
|
2535
2733
|
res.status(500).json({ error: "Failed to load agent statistics" });
|
|
2536
2734
|
}
|
|
2537
2735
|
});
|
|
2736
|
+
this.app.get("/api/soma/report", (_req, res) => {
|
|
2737
|
+
const somaVault = this.config.somaVault;
|
|
2738
|
+
if (!somaVault) {
|
|
2739
|
+
return res.json({ available: false, teaser: true });
|
|
2740
|
+
}
|
|
2741
|
+
try {
|
|
2742
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2743
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2744
|
+
return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
|
|
2745
|
+
}
|
|
2746
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2747
|
+
res.json(report);
|
|
2748
|
+
} catch (error) {
|
|
2749
|
+
console.error("Soma report error:", error);
|
|
2750
|
+
res.json({ available: false, teaser: false, message: "Failed to read report" });
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2753
|
+
this.app.get("/api/soma/governance", (_req, res) => {
|
|
2754
|
+
const somaVault = this.config.somaVault;
|
|
2755
|
+
if (!somaVault) {
|
|
2756
|
+
return res.json({ available: false });
|
|
2757
|
+
}
|
|
2758
|
+
try {
|
|
2759
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2760
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2761
|
+
return res.json({ available: false, message: "No report file. Run soma report." });
|
|
2762
|
+
}
|
|
2763
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2764
|
+
res.json({
|
|
2765
|
+
available: true,
|
|
2766
|
+
layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
|
|
2767
|
+
governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
|
|
2768
|
+
insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
|
|
2769
|
+
canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
|
|
2770
|
+
generatedAt: report.generatedAt
|
|
2771
|
+
});
|
|
2772
|
+
} catch (error) {
|
|
2773
|
+
console.error("Soma governance error:", error);
|
|
2774
|
+
res.status(500).json({ available: false, message: "Failed to read governance data" });
|
|
2775
|
+
}
|
|
2776
|
+
});
|
|
2777
|
+
const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
|
|
2778
|
+
const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
|
|
2779
|
+
this.app.post("/api/soma/governance/promote", (req, res) => {
|
|
2780
|
+
var _a;
|
|
2781
|
+
const somaVault = this.config.somaVault;
|
|
2782
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2783
|
+
const { entryId } = req.body ?? {};
|
|
2784
|
+
if (!entryId) return res.status(400).json({ error: "entryId required" });
|
|
2785
|
+
try {
|
|
2786
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2787
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2788
|
+
const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
|
|
2789
|
+
encoding: "utf-8",
|
|
2790
|
+
timeout: 1e4
|
|
2791
|
+
});
|
|
2792
|
+
res.json({ success: true, message: result.trim() });
|
|
2793
|
+
} catch (error) {
|
|
2794
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2795
|
+
}
|
|
2796
|
+
});
|
|
2797
|
+
this.app.post("/api/soma/governance/reject", (req, res) => {
|
|
2798
|
+
var _a;
|
|
2799
|
+
const somaVault = this.config.somaVault;
|
|
2800
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2801
|
+
const { entryId, reason } = req.body ?? {};
|
|
2802
|
+
if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
|
|
2803
|
+
try {
|
|
2804
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2805
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2806
|
+
const safeReason = sanitizeReason(String(reason));
|
|
2807
|
+
const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
|
|
2808
|
+
encoding: "utf-8",
|
|
2809
|
+
timeout: 1e4
|
|
2810
|
+
});
|
|
2811
|
+
res.json({ success: true, message: result.trim() });
|
|
2812
|
+
} catch (error) {
|
|
2813
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
|
|
2817
|
+
var _a;
|
|
2818
|
+
const somaVault = this.config.somaVault;
|
|
2819
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2820
|
+
try {
|
|
2821
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2822
|
+
const safeId = sanitizeArg(String(req.params.id));
|
|
2823
|
+
const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
|
|
2824
|
+
encoding: "utf-8",
|
|
2825
|
+
timeout: 1e4
|
|
2826
|
+
});
|
|
2827
|
+
res.json({ available: true, output: result.trim() });
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2830
|
+
}
|
|
2831
|
+
});
|
|
2538
2832
|
this.app.get("/api/process-health", (_req, res) => {
|
|
2539
2833
|
var _a, _b;
|
|
2540
2834
|
try {
|
|
@@ -2547,7 +2841,14 @@ var DashboardServer = class {
|
|
|
2547
2841
|
path3.dirname(this.config.tracesDir),
|
|
2548
2842
|
...this.config.dataDirs || []
|
|
2549
2843
|
];
|
|
2550
|
-
|
|
2844
|
+
let configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
|
|
2845
|
+
const pref = getProcessPreference(this.userConfig);
|
|
2846
|
+
if (pref) {
|
|
2847
|
+
const hasPreferred = configs.some((c) => c.processName === pref.prefer);
|
|
2848
|
+
if (hasPreferred) {
|
|
2849
|
+
configs = configs.filter((c) => c.processName !== pref.over);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2551
2852
|
if (configs.length === 0) {
|
|
2552
2853
|
return res.json(null);
|
|
2553
2854
|
}
|
|
@@ -2637,29 +2938,26 @@ var DashboardServer = class {
|
|
|
2637
2938
|
...extraDirs
|
|
2638
2939
|
];
|
|
2639
2940
|
const discovered = [];
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
const
|
|
2650
|
-
if (
|
|
2941
|
+
const svcNames = getSystemdServices(this.userConfig);
|
|
2942
|
+
if (svcNames.length > 0) {
|
|
2943
|
+
try {
|
|
2944
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2945
|
+
const raw = execSync2(
|
|
2946
|
+
`systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
|
|
2947
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
2948
|
+
);
|
|
2949
|
+
for (const line of raw.split("\n")) {
|
|
2950
|
+
const match = line.match(/path=([^\s;]+)/);
|
|
2951
|
+
if (match == null ? void 0 : match[1]) {
|
|
2952
|
+
const dir = path3.dirname(match[1]);
|
|
2953
|
+
if (fs3.existsSync(dir)) discovered.push(dir);
|
|
2954
|
+
}
|
|
2651
2955
|
}
|
|
2956
|
+
} catch {
|
|
2652
2957
|
}
|
|
2653
|
-
} catch {
|
|
2654
2958
|
}
|
|
2655
2959
|
const commonPaths = [
|
|
2656
|
-
|
|
2657
|
-
path3.join(home, ".alfred/data"),
|
|
2658
|
-
path3.join(home, ".openclaw/workspace/traces"),
|
|
2659
|
-
path3.join(home, ".openclaw/subagents"),
|
|
2660
|
-
path3.join(home, ".openclaw/cron/runs"),
|
|
2661
|
-
path3.join(home, ".openclaw/cron"),
|
|
2662
|
-
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2960
|
+
...getDiscoveryPaths(this.userConfig),
|
|
2663
2961
|
path3.join(home, ".agentflow/traces")
|
|
2664
2962
|
];
|
|
2665
2963
|
for (const p of commonPaths) {
|
|
@@ -2704,46 +3002,54 @@ var DashboardServer = class {
|
|
|
2704
3002
|
res.status(500).json({ error: "Failed to update directory config" });
|
|
2705
3003
|
}
|
|
2706
3004
|
});
|
|
2707
|
-
this.
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
3005
|
+
if (this.config.enableCollector !== false) {
|
|
3006
|
+
this.app.post("/v1/traces", import_express.default.json({ limit: "10mb" }), (req, res) => {
|
|
3007
|
+
try {
|
|
3008
|
+
if (this.config.collectorAuthToken) {
|
|
3009
|
+
const auth = req.headers.authorization;
|
|
3010
|
+
if (!auth || auth !== `Bearer ${this.config.collectorAuthToken}`) {
|
|
3011
|
+
return res.status(401).json({ error: "Unauthorized \u2014 provide Authorization: Bearer <token>" });
|
|
3012
|
+
}
|
|
2715
3013
|
}
|
|
2716
|
-
const
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
3014
|
+
const traces = parseOtlpPayload(req.body);
|
|
3015
|
+
let ingested = 0;
|
|
3016
|
+
for (const trace of traces) {
|
|
3017
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
3018
|
+
for (const [id, node] of Object.entries(trace.nodes)) {
|
|
3019
|
+
nodes.set(id, { ...node, state: {} });
|
|
3020
|
+
}
|
|
3021
|
+
const watched = {
|
|
3022
|
+
id: trace.id,
|
|
3023
|
+
rootNodeId: Object.keys(trace.nodes)[0] ?? "",
|
|
3024
|
+
agentId: trace.agentId,
|
|
3025
|
+
name: trace.name,
|
|
3026
|
+
trigger: trace.trigger,
|
|
3027
|
+
startTime: trace.startTime,
|
|
3028
|
+
endTime: trace.endTime,
|
|
3029
|
+
status: trace.status,
|
|
3030
|
+
nodes,
|
|
3031
|
+
edges: [],
|
|
3032
|
+
events: [],
|
|
3033
|
+
metadata: { ...trace.metadata, adapterSource: "otel" },
|
|
3034
|
+
sessionEvents: [],
|
|
3035
|
+
sourceType: "session",
|
|
3036
|
+
filename: `otel-${trace.id}`,
|
|
3037
|
+
lastModified: Date.now(),
|
|
3038
|
+
sourceDir: "http-collector"
|
|
3039
|
+
};
|
|
3040
|
+
this.watcher.traces.set(`otel:${trace.id}`, watched);
|
|
3041
|
+
ingested++;
|
|
3042
|
+
}
|
|
3043
|
+
if (ingested > 0) {
|
|
3044
|
+
this.broadcast({ type: "traces-updated", count: ingested });
|
|
3045
|
+
}
|
|
3046
|
+
res.json({ ok: true, tracesIngested: ingested });
|
|
3047
|
+
} catch (error) {
|
|
3048
|
+
console.error("OTLP collector error:", error);
|
|
3049
|
+
res.status(400).json({ error: "Failed to parse OTLP payload" });
|
|
2740
3050
|
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
console.error("OTLP collector error:", error);
|
|
2744
|
-
res.status(400).json({ error: "Failed to parse OTLP payload" });
|
|
2745
|
-
}
|
|
2746
|
-
});
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
2747
3053
|
this.app.get("/health", (_req, res) => {
|
|
2748
3054
|
res.json({
|
|
2749
3055
|
status: "ok",
|
|
@@ -2755,18 +3061,10 @@ var DashboardServer = class {
|
|
|
2755
3061
|
this.app.get("/ready", (_req, res) => {
|
|
2756
3062
|
res.json({ status: "ready" });
|
|
2757
3063
|
});
|
|
2758
|
-
this.app.get("/v1/*", (_req, res) => {
|
|
2759
|
-
const legacyIndex = path3.join(__dirname, "../public/index.html");
|
|
2760
|
-
if (fs3.existsSync(legacyIndex)) {
|
|
2761
|
-
res.sendFile(legacyIndex);
|
|
2762
|
-
} else {
|
|
2763
|
-
res.status(404).send("Legacy dashboard not found");
|
|
2764
|
-
}
|
|
2765
|
-
});
|
|
2766
3064
|
this.app.get("*", (_req, res) => {
|
|
2767
|
-
const
|
|
2768
|
-
if (fs3.existsSync(
|
|
2769
|
-
res.sendFile(
|
|
3065
|
+
const clientIndex2 = path3.join(__dirname, "../dist/client/index.html");
|
|
3066
|
+
if (fs3.existsSync(clientIndex2)) {
|
|
3067
|
+
res.sendFile(clientIndex2);
|
|
2770
3068
|
} else {
|
|
2771
3069
|
res.status(404).send("Dashboard not found - public files may not be built");
|
|
2772
3070
|
}
|
|
@@ -3013,24 +3311,49 @@ var DashboardServer = class {
|
|
|
3013
3311
|
});
|
|
3014
3312
|
}
|
|
3015
3313
|
async start() {
|
|
3016
|
-
return new Promise((
|
|
3314
|
+
return new Promise((resolve5) => {
|
|
3017
3315
|
const host = this.config.host || "localhost";
|
|
3018
3316
|
this.server.listen(this.config.port, host, () => {
|
|
3019
3317
|
console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
|
|
3020
3318
|
console.log(`Watching traces in: ${this.config.tracesDir}`);
|
|
3021
|
-
|
|
3319
|
+
resolve5();
|
|
3022
3320
|
});
|
|
3023
3321
|
});
|
|
3024
3322
|
}
|
|
3323
|
+
/** Check if any src/client file is newer than the built bundle. */
|
|
3324
|
+
isClientStale(srcDir, distDir) {
|
|
3325
|
+
try {
|
|
3326
|
+
const distIndex = path3.join(distDir, "index.html");
|
|
3327
|
+
if (!fs3.existsSync(distIndex)) return true;
|
|
3328
|
+
const distMtime = fs3.statSync(distIndex).mtimeMs;
|
|
3329
|
+
const check = (dir) => {
|
|
3330
|
+
for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
|
|
3331
|
+
const full = path3.join(dir, entry.name);
|
|
3332
|
+
if (entry.isDirectory()) {
|
|
3333
|
+
if (check(full)) return true;
|
|
3334
|
+
} else if (fs3.statSync(full).mtimeMs > distMtime) {
|
|
3335
|
+
return true;
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
return false;
|
|
3339
|
+
};
|
|
3340
|
+
return check(srcDir);
|
|
3341
|
+
} catch {
|
|
3342
|
+
return false;
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3025
3345
|
async stop() {
|
|
3026
|
-
return new Promise((
|
|
3346
|
+
return new Promise((resolve5) => {
|
|
3027
3347
|
this.watcher.stop();
|
|
3028
3348
|
this.server.close(() => {
|
|
3029
3349
|
console.log("Dashboard server stopped");
|
|
3030
|
-
|
|
3350
|
+
resolve5();
|
|
3031
3351
|
});
|
|
3032
3352
|
});
|
|
3033
3353
|
}
|
|
3354
|
+
getConfigPath() {
|
|
3355
|
+
return this.configPath;
|
|
3356
|
+
}
|
|
3034
3357
|
getStats() {
|
|
3035
3358
|
return this.stats.getGlobalStats();
|
|
3036
3359
|
}
|