agentflow-dashboard 0.7.1 → 0.8.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-3S4AAIPA.js → chunk-JRVE5NM3.js} +644 -171
- package/dist/cli.cjs +648 -175
- package/dist/cli.js +1 -1
- package/dist/client/assets/index-CyQ7qX-x.js +50 -0
- package/dist/client/assets/{index-Ds_npIxI.css → index-DHcSpTgM.css} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/index.cjs +648 -175
- package/dist/index.js +1 -1
- package/dist/server.cjs +648 -175
- package/dist/server.js +1 -1
- package/package.json +3 -5
- package/dist/client/assets/index-DSuI0NgP.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,11 +32,86 @@ __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");
|
|
114
|
+
var import_chokidar2 = __toESM(require("chokidar"), 1);
|
|
40
115
|
var import_express = __toESM(require("express"), 1);
|
|
41
116
|
var import_ws = require("ws");
|
|
42
117
|
|
|
@@ -69,17 +144,17 @@ var AgentFlowAdapter = class {
|
|
|
69
144
|
};
|
|
70
145
|
|
|
71
146
|
// src/adapters/openclaw.ts
|
|
72
|
-
var
|
|
73
|
-
var
|
|
147
|
+
var import_node_fs2 = require("fs");
|
|
148
|
+
var import_node_path2 = require("path");
|
|
74
149
|
var jobCache = /* @__PURE__ */ new Map();
|
|
75
150
|
function loadJobs(openclawDir) {
|
|
76
151
|
const cached = jobCache.get(openclawDir);
|
|
77
152
|
if (cached) return cached;
|
|
78
|
-
const jobsPath = (0,
|
|
153
|
+
const jobsPath = (0, import_node_path2.join)(openclawDir, "cron", "jobs.json");
|
|
79
154
|
const map = /* @__PURE__ */ new Map();
|
|
80
155
|
try {
|
|
81
|
-
if ((0,
|
|
82
|
-
const data = JSON.parse((0,
|
|
156
|
+
if ((0, import_node_fs2.existsSync)(jobsPath)) {
|
|
157
|
+
const data = JSON.parse((0, import_node_fs2.readFileSync)(jobsPath, "utf-8"));
|
|
83
158
|
const jobs = Array.isArray(data) ? data : data.jobs ?? [];
|
|
84
159
|
for (const job of jobs) {
|
|
85
160
|
if (job.id) map.set(job.id, job);
|
|
@@ -91,19 +166,19 @@ function loadJobs(openclawDir) {
|
|
|
91
166
|
return map;
|
|
92
167
|
}
|
|
93
168
|
function findOpenClawRoot(filePath) {
|
|
94
|
-
let dir = (0,
|
|
169
|
+
let dir = (0, import_node_path2.dirname)(filePath);
|
|
95
170
|
for (let i = 0; i < 5; i++) {
|
|
96
|
-
if ((0,
|
|
171
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, "cron", "jobs.json")) || (0, import_node_path2.basename)(dir) === ".openclaw") {
|
|
97
172
|
return dir;
|
|
98
173
|
}
|
|
99
|
-
dir = (0,
|
|
174
|
+
dir = (0, import_node_path2.dirname)(dir);
|
|
100
175
|
}
|
|
101
176
|
return null;
|
|
102
177
|
}
|
|
103
178
|
var OpenClawAdapter = class {
|
|
104
179
|
name = "openclaw";
|
|
105
180
|
detect(dirPath) {
|
|
106
|
-
return (0,
|
|
181
|
+
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
182
|
}
|
|
108
183
|
canHandle(filePath) {
|
|
109
184
|
if (!filePath.endsWith(".jsonl")) return false;
|
|
@@ -112,7 +187,7 @@ var OpenClawAdapter = class {
|
|
|
112
187
|
parse(filePath) {
|
|
113
188
|
const traces = [];
|
|
114
189
|
try {
|
|
115
|
-
const content = (0,
|
|
190
|
+
const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
116
191
|
const root = findOpenClawRoot(filePath);
|
|
117
192
|
const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
|
|
118
193
|
for (const line of content.split("\n")) {
|
|
@@ -124,7 +199,7 @@ var OpenClawAdapter = class {
|
|
|
124
199
|
continue;
|
|
125
200
|
}
|
|
126
201
|
if (entry.action !== "finished") continue;
|
|
127
|
-
const jobId = entry.jobId ?? (0,
|
|
202
|
+
const jobId = entry.jobId ?? (0, import_node_path2.basename)(filePath, ".jsonl");
|
|
128
203
|
const job = jobs.get(jobId);
|
|
129
204
|
const jobName = (job == null ? void 0 : job.name) ?? jobId;
|
|
130
205
|
const startTime = entry.runAtMs ?? entry.ts;
|
|
@@ -176,8 +251,8 @@ var OpenClawAdapter = class {
|
|
|
176
251
|
};
|
|
177
252
|
|
|
178
253
|
// src/adapters/otel.ts
|
|
179
|
-
var
|
|
180
|
-
var
|
|
254
|
+
var import_node_fs3 = require("fs");
|
|
255
|
+
var import_node_path3 = require("path");
|
|
181
256
|
var SPAN_TYPE_MAP = {
|
|
182
257
|
"gen_ai.chat": "llm",
|
|
183
258
|
"gen_ai.completion": "llm",
|
|
@@ -284,8 +359,8 @@ var OTelAdapter = class {
|
|
|
284
359
|
name = "otel";
|
|
285
360
|
detect(dirPath) {
|
|
286
361
|
try {
|
|
287
|
-
if ((0,
|
|
288
|
-
const files = (0,
|
|
362
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(dirPath, "otel-traces"))) return true;
|
|
363
|
+
const files = (0, import_node_fs3.readdirSync)(dirPath);
|
|
289
364
|
return files.some((f) => f.endsWith(".otlp.json"));
|
|
290
365
|
} catch {
|
|
291
366
|
return false;
|
|
@@ -296,7 +371,7 @@ var OTelAdapter = class {
|
|
|
296
371
|
}
|
|
297
372
|
parse(filePath) {
|
|
298
373
|
try {
|
|
299
|
-
const content = (0,
|
|
374
|
+
const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
|
|
300
375
|
const payload = JSON.parse(content);
|
|
301
376
|
const traces = parseOtlpPayload(payload);
|
|
302
377
|
for (const t of traces) t.filePath = filePath;
|
|
@@ -338,9 +413,7 @@ function extractSource(agentId) {
|
|
|
338
413
|
const colonIdx = agentId.indexOf(":");
|
|
339
414
|
if (colonIdx > 0 && colonIdx < 20) {
|
|
340
415
|
const prefix = agentId.slice(0, colonIdx);
|
|
341
|
-
|
|
342
|
-
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
343
|
-
}
|
|
416
|
+
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
344
417
|
}
|
|
345
418
|
return { source: "agentflow", localId: agentId };
|
|
346
419
|
}
|
|
@@ -371,16 +444,20 @@ function deduplicateAgents(agents) {
|
|
|
371
444
|
for (const a of tagged) {
|
|
372
445
|
const suffix = extractSuffix(a.localId);
|
|
373
446
|
if (!suffix) continue;
|
|
374
|
-
const
|
|
447
|
+
const key = `${a.source}:${suffix}`;
|
|
448
|
+
const group = suffixGroups.get(key) ?? [];
|
|
375
449
|
group.push(a);
|
|
376
|
-
suffixGroups.set(
|
|
450
|
+
suffixGroups.set(key, group);
|
|
377
451
|
}
|
|
378
452
|
const mergedIds = /* @__PURE__ */ new Set();
|
|
379
453
|
const mergedAgents = [];
|
|
380
|
-
for (const [
|
|
454
|
+
for (const [_key, group] of suffixGroups) {
|
|
455
|
+
const suffix = extractSuffix(group[0].localId);
|
|
381
456
|
if (group.length < 2) continue;
|
|
382
457
|
const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
|
|
383
458
|
if (prefixes.size < 2) continue;
|
|
459
|
+
const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
|
|
460
|
+
if (longPrefixes.length >= 2) continue;
|
|
384
461
|
const merged = {
|
|
385
462
|
agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
|
|
386
463
|
displayName: suffix,
|
|
@@ -428,10 +505,7 @@ function groupAgents(agents) {
|
|
|
428
505
|
}
|
|
429
506
|
const SOURCE_DISPLAY = {
|
|
430
507
|
agentflow: "AgentFlow",
|
|
431
|
-
|
|
432
|
-
otel: "OpenTelemetry",
|
|
433
|
-
langchain: "LangChain",
|
|
434
|
-
crewai: "CrewAI"
|
|
508
|
+
otel: "OpenTelemetry"
|
|
435
509
|
};
|
|
436
510
|
const groups = [];
|
|
437
511
|
for (const [source, sourceAgents] of sourceMap) {
|
|
@@ -777,10 +851,6 @@ function getUniversalNodeStatus(activity) {
|
|
|
777
851
|
return "completed";
|
|
778
852
|
}
|
|
779
853
|
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
854
|
const firstSegment = sessionId.split("-")[0];
|
|
785
855
|
if (firstSegment) return firstSegment;
|
|
786
856
|
return "openclaw";
|
|
@@ -793,19 +863,81 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
793
863
|
tracesDir;
|
|
794
864
|
dataDirs;
|
|
795
865
|
allWatchDirs;
|
|
866
|
+
maxAgeMs;
|
|
867
|
+
userConfig;
|
|
796
868
|
constructor(tracesDirOrOptions) {
|
|
797
869
|
super();
|
|
870
|
+
const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
|
|
871
|
+
const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
|
|
872
|
+
const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
|
|
798
873
|
if (typeof tracesDirOrOptions === "string") {
|
|
799
874
|
this.tracesDir = path.resolve(tracesDirOrOptions);
|
|
800
875
|
this.dataDirs = [];
|
|
876
|
+
this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
|
|
877
|
+
this.userConfig = {};
|
|
801
878
|
} else {
|
|
802
879
|
this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
|
|
803
880
|
this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
|
|
804
|
-
|
|
805
|
-
|
|
881
|
+
this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
|
|
882
|
+
this.userConfig = tracesDirOrOptions.userConfig ?? {};
|
|
883
|
+
}
|
|
884
|
+
this.skipFiles = /* @__PURE__ */ new Set([
|
|
885
|
+
..._TraceWatcher.STRUCTURAL_SKIP_FILES,
|
|
886
|
+
...getSkipFiles(this.userConfig)
|
|
887
|
+
]);
|
|
888
|
+
this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
|
|
889
|
+
this.allWatchDirs = [...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))];
|
|
806
890
|
this.ensureTracesDir();
|
|
807
891
|
this.loadExistingFiles();
|
|
892
|
+
this.archiveOldTraces();
|
|
808
893
|
this.startWatching();
|
|
894
|
+
setInterval(() => this.archiveOldTraces(), 6 * 60 * 60 * 1e3);
|
|
895
|
+
}
|
|
896
|
+
/** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
|
|
897
|
+
archiveOldTraces() {
|
|
898
|
+
const cutoff = Date.now() - this.maxAgeMs;
|
|
899
|
+
let archived = 0;
|
|
900
|
+
for (const dir of this.allWatchDirs) {
|
|
901
|
+
if (!fs.existsSync(dir)) continue;
|
|
902
|
+
try {
|
|
903
|
+
this.archiveDirectory(dir, cutoff, 0);
|
|
904
|
+
} catch (error) {
|
|
905
|
+
console.warn(`Archival error in ${dir}:`, error.message);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
archiveDirectory(dir, cutoff, depth) {
|
|
910
|
+
if (depth > 10) return 0;
|
|
911
|
+
if (path.basename(dir) === "archive") return 0;
|
|
912
|
+
let archived = 0;
|
|
913
|
+
try {
|
|
914
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
915
|
+
for (const entry of entries) {
|
|
916
|
+
if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
|
|
917
|
+
const fullPath = path.join(dir, entry.name);
|
|
918
|
+
if (entry.isDirectory()) {
|
|
919
|
+
archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
|
|
923
|
+
try {
|
|
924
|
+
const stats = fs.statSync(fullPath);
|
|
925
|
+
if (stats.mtimeMs >= cutoff) continue;
|
|
926
|
+
const mtime = new Date(stats.mtimeMs);
|
|
927
|
+
const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
|
|
928
|
+
const archiveDir = path.join(this.tracesDir, "archive", yearMonth);
|
|
929
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
930
|
+
const dest = path.join(archiveDir, entry.name);
|
|
931
|
+
fs.renameSync(fullPath, dest);
|
|
932
|
+
const key = this.traceKey(fullPath);
|
|
933
|
+
this.traces.delete(key);
|
|
934
|
+
archived++;
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
} catch {
|
|
939
|
+
}
|
|
940
|
+
return archived;
|
|
809
941
|
}
|
|
810
942
|
ensureTracesDir() {
|
|
811
943
|
if (!fs.existsSync(this.tracesDir)) {
|
|
@@ -838,9 +970,17 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
838
970
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
839
971
|
for (const entry of entries) {
|
|
840
972
|
if (entry.name.startsWith(".")) continue;
|
|
973
|
+
if (entry.name === "archive") continue;
|
|
974
|
+
if (this.userSkipDirs.has(entry.name)) continue;
|
|
841
975
|
const fullPath = path.join(dir, entry.name);
|
|
842
976
|
if (entry.isFile()) {
|
|
843
977
|
if (this.isSupportedFile(entry.name)) {
|
|
978
|
+
try {
|
|
979
|
+
const mtime = fs.statSync(fullPath).mtimeMs;
|
|
980
|
+
if (Date.now() - mtime > this.maxAgeMs) continue;
|
|
981
|
+
} catch {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
844
984
|
if (this.loadFile(fullPath)) {
|
|
845
985
|
fileCount++;
|
|
846
986
|
}
|
|
@@ -858,8 +998,8 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
858
998
|
isSupportedFile(filename) {
|
|
859
999
|
return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
|
|
860
1000
|
}
|
|
861
|
-
/**
|
|
862
|
-
static
|
|
1001
|
+
/** Structural file names that are never trace data — always skipped. */
|
|
1002
|
+
static STRUCTURAL_SKIP_FILES = /* @__PURE__ */ new Set([
|
|
863
1003
|
"workers.json",
|
|
864
1004
|
"package.json",
|
|
865
1005
|
"package-lock.json",
|
|
@@ -874,6 +1014,10 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
874
1014
|
"update-check.json",
|
|
875
1015
|
"exec-approvals.json"
|
|
876
1016
|
]);
|
|
1017
|
+
/** Skip files = structural + user config */
|
|
1018
|
+
skipFiles;
|
|
1019
|
+
/** Skip directories from user config */
|
|
1020
|
+
userSkipDirs;
|
|
877
1021
|
static SKIP_SUFFIXES = [
|
|
878
1022
|
"-state.json",
|
|
879
1023
|
"-config.json",
|
|
@@ -885,7 +1029,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
885
1029
|
/** Load a file using the adapter registry, falling back to built-in parsing. */
|
|
886
1030
|
loadFile(filePath) {
|
|
887
1031
|
const filename = path.basename(filePath);
|
|
888
|
-
if (
|
|
1032
|
+
if (this.skipFiles.has(filename)) return false;
|
|
889
1033
|
if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
|
|
890
1034
|
const adapter = findAdapter(filePath);
|
|
891
1035
|
if (adapter && adapter.name !== "agentflow") {
|
|
@@ -1062,43 +1206,26 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1062
1206
|
}
|
|
1063
1207
|
return traces;
|
|
1064
1208
|
}
|
|
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
|
-
};
|
|
1209
|
+
/** Normalise agent identifiers using config-driven alias map. */
|
|
1087
1210
|
normaliseAgentId(raw) {
|
|
1088
|
-
|
|
1211
|
+
const aliases = getAliases(this.userConfig);
|
|
1212
|
+
return aliases[raw] ?? raw;
|
|
1089
1213
|
}
|
|
1090
1214
|
detectAgentIdentifier(activity, _filename, filePath) {
|
|
1091
1215
|
if (activity.agent_id) {
|
|
1092
|
-
|
|
1093
|
-
if (agentId === "main" && filePath.includes(".alfred/")) return this.normaliseAgentId("alfred-main");
|
|
1094
|
-
return this.normaliseAgentId(agentId);
|
|
1216
|
+
return this.normaliseAgentId(activity.agent_id);
|
|
1095
1217
|
}
|
|
1096
1218
|
const pathAgent = this.extractAgentFromPath(filePath);
|
|
1097
|
-
|
|
1219
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1220
|
+
if (detection.filePatterns) {
|
|
1098
1221
|
const basename3 = path.basename(filePath, path.extname(filePath));
|
|
1099
|
-
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1222
|
+
for (const [pattern, template] of Object.entries(detection.filePatterns)) {
|
|
1223
|
+
const re = new RegExp(`^(${pattern})$`);
|
|
1224
|
+
const match = basename3.match(re);
|
|
1225
|
+
if (match) {
|
|
1226
|
+
const resolved = template.replace("${match}", match[1]);
|
|
1227
|
+
return this.normaliseAgentId(resolved);
|
|
1228
|
+
}
|
|
1102
1229
|
}
|
|
1103
1230
|
}
|
|
1104
1231
|
return this.normaliseAgentId(pathAgent);
|
|
@@ -1106,20 +1233,23 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1106
1233
|
extractAgentFromPath(filePath) {
|
|
1107
1234
|
const filename = path.basename(filePath, path.extname(filePath));
|
|
1108
1235
|
const pathParts = filePath.split(path.sep);
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1236
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1237
|
+
let pathPrefix = "";
|
|
1238
|
+
if (detection.pathPatterns) {
|
|
1239
|
+
for (const [pathSubstring, agentId] of Object.entries(detection.pathPatterns)) {
|
|
1240
|
+
if (filePath.includes(pathSubstring)) {
|
|
1241
|
+
pathPrefix = agentId;
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1116
1244
|
}
|
|
1117
|
-
return "openclaw";
|
|
1118
1245
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1246
|
+
const agentsIndex = pathParts.lastIndexOf("agents");
|
|
1247
|
+
if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
|
|
1248
|
+
const agentName = pathParts[agentsIndex + 1];
|
|
1249
|
+
return pathPrefix ? `${pathPrefix}-${agentName}` : agentName;
|
|
1121
1250
|
}
|
|
1122
|
-
|
|
1251
|
+
if (pathPrefix) return pathPrefix;
|
|
1252
|
+
for (const part of [...pathParts].reverse()) {
|
|
1123
1253
|
if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
|
|
1124
1254
|
return part;
|
|
1125
1255
|
}
|
|
@@ -1454,19 +1584,22 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1454
1584
|
const parentDir = path.basename(path.dirname(filePath));
|
|
1455
1585
|
const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
|
|
1456
1586
|
const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
|
|
1457
|
-
let
|
|
1587
|
+
let agentName;
|
|
1458
1588
|
if (parentDir === "sessions" && greatGrandParentDir === "agents") {
|
|
1459
|
-
|
|
1589
|
+
agentName = grandParentDir;
|
|
1460
1590
|
} else if (grandParentDir === "agents") {
|
|
1461
|
-
|
|
1462
|
-
} else if (parentDir === "runs" && grandParentDir === "cron") {
|
|
1463
|
-
agentId = "openclaw-cron";
|
|
1591
|
+
agentName = parentDir;
|
|
1464
1592
|
} else {
|
|
1465
|
-
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1593
|
+
agentName = parentDir;
|
|
1594
|
+
}
|
|
1595
|
+
let agentId = agentName;
|
|
1596
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1597
|
+
if (detection.pathPatterns) {
|
|
1598
|
+
for (const [pathSubstring, prefix] of Object.entries(detection.pathPatterns)) {
|
|
1599
|
+
if (filePath.includes(pathSubstring)) {
|
|
1600
|
+
agentId = `${prefix}-${agentName}`;
|
|
1601
|
+
break;
|
|
1602
|
+
}
|
|
1470
1603
|
}
|
|
1471
1604
|
}
|
|
1472
1605
|
const modelEvent = rawEvents.find((e) => e.type === "model_change");
|
|
@@ -1755,6 +1888,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1755
1888
|
edges: [],
|
|
1756
1889
|
events: [],
|
|
1757
1890
|
startTime,
|
|
1891
|
+
status,
|
|
1758
1892
|
agentId,
|
|
1759
1893
|
trigger,
|
|
1760
1894
|
name: rootName,
|
|
@@ -1875,8 +2009,12 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1875
2009
|
// Ignore git directories
|
|
1876
2010
|
/\.vscode/,
|
|
1877
2011
|
// Ignore vscode
|
|
1878
|
-
/\.idea
|
|
2012
|
+
/\.idea/,
|
|
1879
2013
|
// Ignore idea
|
|
2014
|
+
/\/archive\//,
|
|
2015
|
+
// Ignore archived trace files
|
|
2016
|
+
// Ignore user-configured skip directories
|
|
2017
|
+
...getSkipDirectories(this.userConfig).map((d) => new RegExp(`/${d}/`))
|
|
1880
2018
|
],
|
|
1881
2019
|
persistent: true,
|
|
1882
2020
|
ignoreInitial: true,
|
|
@@ -1930,29 +2068,42 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1930
2068
|
});
|
|
1931
2069
|
}
|
|
1932
2070
|
getTrace(filename) {
|
|
2071
|
+
const candidates = [];
|
|
1933
2072
|
const exact = this.traces.get(filename);
|
|
1934
|
-
if (exact)
|
|
2073
|
+
if (exact) candidates.push(exact);
|
|
1935
2074
|
if (filename.includes("::")) {
|
|
1936
2075
|
const [fname, startTimeStr] = filename.split("::");
|
|
1937
2076
|
const startTime = Number(startTimeStr);
|
|
1938
2077
|
if (fname && !Number.isNaN(startTime)) {
|
|
1939
2078
|
for (const trace of this.traces.values()) {
|
|
1940
2079
|
if (trace.filename === fname && trace.startTime === startTime) {
|
|
1941
|
-
|
|
2080
|
+
candidates.push(trace);
|
|
1942
2081
|
}
|
|
1943
2082
|
}
|
|
1944
2083
|
}
|
|
1945
2084
|
}
|
|
1946
2085
|
for (const prefix of ["openclaw:", "otel:", ""]) {
|
|
1947
2086
|
const prefixed = this.traces.get(prefix + filename);
|
|
1948
|
-
if (prefixed)
|
|
2087
|
+
if (prefixed) candidates.push(prefixed);
|
|
1949
2088
|
}
|
|
1950
2089
|
for (const [key, trace] of this.traces) {
|
|
1951
2090
|
if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
|
|
1952
|
-
|
|
2091
|
+
candidates.push(trace);
|
|
1953
2092
|
}
|
|
1954
2093
|
}
|
|
1955
|
-
return void 0;
|
|
2094
|
+
if (candidates.length === 0) return void 0;
|
|
2095
|
+
if (candidates.length === 1) return candidates[0];
|
|
2096
|
+
let best = candidates[0];
|
|
2097
|
+
let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
|
|
2098
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
2099
|
+
const c = candidates[i];
|
|
2100
|
+
const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
|
|
2101
|
+
if (nc > bestNodeCount) {
|
|
2102
|
+
best = c;
|
|
2103
|
+
bestNodeCount = nc;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
return best;
|
|
1956
2107
|
}
|
|
1957
2108
|
getTracesByAgent(agentId) {
|
|
1958
2109
|
return this.getAllTraces().filter((trace) => trace.agentId === agentId);
|
|
@@ -1999,7 +2150,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1999
2150
|
var fs2 = __toESM(require("fs"), 1);
|
|
2000
2151
|
var os = __toESM(require("os"), 1);
|
|
2001
2152
|
var path2 = __toESM(require("path"), 1);
|
|
2002
|
-
var VERSION = "0.
|
|
2153
|
+
var VERSION = "0.8.0";
|
|
2003
2154
|
function getLanAddress() {
|
|
2004
2155
|
const interfaces = os.networkInterfaces();
|
|
2005
2156
|
for (const name of Object.keys(interfaces)) {
|
|
@@ -2011,7 +2162,7 @@ function getLanAddress() {
|
|
|
2011
2162
|
}
|
|
2012
2163
|
return null;
|
|
2013
2164
|
}
|
|
2014
|
-
function printBanner(config, traceCount, stats) {
|
|
2165
|
+
function printBanner(config, traceCount, stats, configPath) {
|
|
2015
2166
|
var _a;
|
|
2016
2167
|
const lan = getLanAddress();
|
|
2017
2168
|
const host = config.host || "localhost";
|
|
@@ -2027,26 +2178,25 @@ function printBanner(config, traceCount, stats) {
|
|
|
2027
2178
|
|
|
2028
2179
|
See your agents think.
|
|
2029
2180
|
|
|
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
2181
|
Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? `
|
|
2042
2182
|
Data dirs: ${config.dataDirs.join("\n ")}` : ""}
|
|
2043
2183
|
Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
|
|
2044
2184
|
Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
|
|
2185
|
+
Config: ${configPath ?? "none (using defaults)"}
|
|
2045
2186
|
CORS: ${config.enableCors ? "enabled" : "disabled"}
|
|
2046
2187
|
WebSocket: live updates enabled
|
|
2188
|
+
Window: ${process.env.AGENTFLOW_TRACE_WINDOW_HOURS ?? "48"}h (set AGENTFLOW_TRACE_WINDOW_HOURS to change)
|
|
2047
2189
|
|
|
2048
2190
|
\u2192 http://localhost:${port}${isPublic && lan ? `
|
|
2049
2191
|
\u2192 http://${lan}:${port} (LAN)` : ""}
|
|
2192
|
+
|
|
2193
|
+
Pages: Agents \xB7 SOMA
|
|
2194
|
+
Agent: Profile \xB7 Execution Detail
|
|
2195
|
+
SOMA: Intelligence \xB7 Review \xB7 Policies \xB7 Knowledge \xB7 Activity
|
|
2196
|
+
Tabs: Flame Chart \xB7 Agent Flow \xB7 Metrics \xB7 Dependencies
|
|
2197
|
+
State Machine \xB7 Summary \xB7 Transcript
|
|
2198
|
+
|
|
2199
|
+
Runs locally. Your data never leaves your machine.
|
|
2050
2200
|
`);
|
|
2051
2201
|
}
|
|
2052
2202
|
async function startDashboard() {
|
|
@@ -2084,6 +2234,12 @@ async function startDashboard() {
|
|
|
2084
2234
|
case "--collector-token":
|
|
2085
2235
|
config.collectorAuthToken = args[++i];
|
|
2086
2236
|
break;
|
|
2237
|
+
case "--soma-vault":
|
|
2238
|
+
config.somaVault = args[++i];
|
|
2239
|
+
break;
|
|
2240
|
+
case "--config":
|
|
2241
|
+
config.configPath = args[++i];
|
|
2242
|
+
break;
|
|
2087
2243
|
case "--help":
|
|
2088
2244
|
printHelp();
|
|
2089
2245
|
process.exit(0);
|
|
@@ -2095,6 +2251,9 @@ async function startDashboard() {
|
|
|
2095
2251
|
if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
|
|
2096
2252
|
config.enableCollector = false;
|
|
2097
2253
|
}
|
|
2254
|
+
if (!config.somaVault && process.env.SOMA_VAULT) {
|
|
2255
|
+
config.somaVault = process.env.SOMA_VAULT;
|
|
2256
|
+
}
|
|
2098
2257
|
const tracesPath = path2.resolve(config.tracesDir);
|
|
2099
2258
|
if (!fs2.existsSync(tracesPath)) {
|
|
2100
2259
|
fs2.mkdirSync(tracesPath, { recursive: true });
|
|
@@ -2116,7 +2275,7 @@ async function startDashboard() {
|
|
|
2116
2275
|
setTimeout(() => {
|
|
2117
2276
|
const stats = dashboard.getStats();
|
|
2118
2277
|
const traces = dashboard.getTraces();
|
|
2119
|
-
printBanner(config, traces.length, stats);
|
|
2278
|
+
printBanner(config, traces.length, stats, dashboard.getConfigPath());
|
|
2120
2279
|
}, 1500);
|
|
2121
2280
|
} catch (error) {
|
|
2122
2281
|
console.error("\u274C Failed to start dashboard:", error);
|
|
@@ -2125,7 +2284,7 @@ async function startDashboard() {
|
|
|
2125
2284
|
}
|
|
2126
2285
|
function printHelp() {
|
|
2127
2286
|
console.log(`
|
|
2128
|
-
|
|
2287
|
+
AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
|
|
2129
2288
|
|
|
2130
2289
|
Usage:
|
|
2131
2290
|
agentflow-dashboard [options]
|
|
@@ -2136,22 +2295,34 @@ Options:
|
|
|
2136
2295
|
-t, --traces <path> Traces directory (default: ./traces)
|
|
2137
2296
|
-h, --host <address> Host address (default: localhost)
|
|
2138
2297
|
--data-dir <path> Extra data directory for process discovery (repeatable)
|
|
2298
|
+
--config <path> Path to agentflow.config.json (aliases, skip files, etc.)
|
|
2299
|
+
--soma-vault <path> SOMA vault directory for intelligence data
|
|
2139
2300
|
--cors Enable CORS headers
|
|
2140
2301
|
--no-collector Disable OTLP trace collector (POST /v1/traces)
|
|
2141
2302
|
--collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
|
|
2142
2303
|
--help Show this help message
|
|
2143
2304
|
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2305
|
+
Config file:
|
|
2306
|
+
The dashboard loads agentflow.config.json for agent aliases, skip files,
|
|
2307
|
+
discovery paths, and systemd services. Resolution order:
|
|
2308
|
+
1. --config flag
|
|
2309
|
+
2. AGENTFLOW_CONFIG env var
|
|
2310
|
+
3. ./agentflow.config.json
|
|
2311
|
+
4. ~/.config/agentflow/config.json
|
|
2312
|
+
|
|
2313
|
+
See agentflow.config.example.json for a complete reference.
|
|
2314
|
+
|
|
2315
|
+
Environment:
|
|
2316
|
+
AGENTFLOW_CONFIG Path to config file
|
|
2317
|
+
AGENTFLOW_TRACE_WINDOW_HOURS Max age of traces to load (default: 48)
|
|
2318
|
+
AGENTFLOW_COLLECTOR_TOKEN Auth token for OTLP collector
|
|
2319
|
+
AGENTFLOW_NO_COLLECTOR=true Disable OTLP collector
|
|
2320
|
+
SOMA_VAULT SOMA vault directory
|
|
2148
2321
|
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
\u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
|
|
2154
|
-
\u26A0\uFE0F Errors Failed and hung nodes with metadata
|
|
2322
|
+
Examples:
|
|
2323
|
+
agentflow-dashboard --traces ./traces --host 0.0.0.0
|
|
2324
|
+
agentflow-dashboard --traces ./traces --config ./agentflow.config.json
|
|
2325
|
+
agentflow-dashboard -p 8080 -t /var/log/agentflow --cors
|
|
2155
2326
|
`);
|
|
2156
2327
|
}
|
|
2157
2328
|
|
|
@@ -2174,12 +2345,15 @@ function serializeTrace(trace) {
|
|
|
2174
2345
|
var DashboardServer = class {
|
|
2175
2346
|
constructor(config) {
|
|
2176
2347
|
this.config = config;
|
|
2177
|
-
const
|
|
2178
|
-
|
|
2348
|
+
const { config: userCfg, configPath: cfgPath } = loadConfig(config.configPath);
|
|
2349
|
+
this.userConfig = userCfg;
|
|
2350
|
+
this.configPath = cfgPath;
|
|
2351
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
2352
|
+
const dashConfigPath = path3.join(home, ".agentflow/dashboard-config.json");
|
|
2179
2353
|
if (!config.dataDirs) config.dataDirs = [];
|
|
2180
2354
|
try {
|
|
2181
|
-
if (fs3.existsSync(
|
|
2182
|
-
const saved = JSON.parse(fs3.readFileSync(
|
|
2355
|
+
if (fs3.existsSync(dashConfigPath)) {
|
|
2356
|
+
const saved = JSON.parse(fs3.readFileSync(dashConfigPath, "utf-8"));
|
|
2183
2357
|
const extraDirs = saved.extraDirs ?? [];
|
|
2184
2358
|
for (const d of extraDirs) {
|
|
2185
2359
|
if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
|
|
@@ -2187,21 +2361,15 @@ var DashboardServer = class {
|
|
|
2187
2361
|
}
|
|
2188
2362
|
} catch {
|
|
2189
2363
|
}
|
|
2190
|
-
const
|
|
2191
|
-
path3.join(home, ".openclaw/cron/runs"),
|
|
2192
|
-
path3.join(home, ".openclaw/workspace/traces"),
|
|
2193
|
-
path3.join(home, ".openclaw/subagents"),
|
|
2194
|
-
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2195
|
-
path3.join(home, ".agentflow/traces")
|
|
2196
|
-
];
|
|
2197
|
-
for (const p of autoDiscoverPaths) {
|
|
2364
|
+
for (const p of getDiscoveryPaths(this.userConfig)) {
|
|
2198
2365
|
if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
|
|
2199
2366
|
config.dataDirs.push(p);
|
|
2200
2367
|
}
|
|
2201
2368
|
}
|
|
2202
2369
|
this.watcher = new TraceWatcher({
|
|
2203
2370
|
tracesDir: config.tracesDir,
|
|
2204
|
-
dataDirs: config.dataDirs
|
|
2371
|
+
dataDirs: config.dataDirs,
|
|
2372
|
+
userConfig: this.userConfig
|
|
2205
2373
|
});
|
|
2206
2374
|
this.stats = new AgentStats();
|
|
2207
2375
|
this.knowledgeStore = (0, import_agentflow_core3.createKnowledgeStore)({
|
|
@@ -2210,6 +2378,7 @@ var DashboardServer = class {
|
|
|
2210
2378
|
this.setupExpress();
|
|
2211
2379
|
this.setupWebSocket();
|
|
2212
2380
|
this.setupTraceWatcher();
|
|
2381
|
+
this.setupSomaReportWatcher();
|
|
2213
2382
|
let knowledgeCount = 0;
|
|
2214
2383
|
for (const trace of this.watcher.getAllTraces()) {
|
|
2215
2384
|
this.stats.processTrace(trace);
|
|
@@ -2236,6 +2405,8 @@ var DashboardServer = class {
|
|
|
2236
2405
|
ts: 0
|
|
2237
2406
|
};
|
|
2238
2407
|
knowledgeStore;
|
|
2408
|
+
userConfig;
|
|
2409
|
+
configPath;
|
|
2239
2410
|
setupExpress() {
|
|
2240
2411
|
if (this.config.enableCors) {
|
|
2241
2412
|
this.app.use((_req, res, next) => {
|
|
@@ -2247,18 +2418,35 @@ var DashboardServer = class {
|
|
|
2247
2418
|
next();
|
|
2248
2419
|
});
|
|
2249
2420
|
}
|
|
2250
|
-
const
|
|
2421
|
+
const pkgDir = path3.join(__dirname, "..");
|
|
2422
|
+
const clientDir = path3.join(pkgDir, "dist/client");
|
|
2423
|
+
const clientIndex = path3.join(clientDir, "index.html");
|
|
2424
|
+
const srcDir = path3.join(pkgDir, "src/client");
|
|
2425
|
+
const needsBuild = !fs3.existsSync(clientIndex) || fs3.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
|
|
2426
|
+
if (needsBuild) {
|
|
2427
|
+
try {
|
|
2428
|
+
console.log("Building dashboard client...");
|
|
2429
|
+
(0, import_node_child_process.execSync)("npm run build:client", { cwd: pkgDir, stdio: "inherit", timeout: 3e4 });
|
|
2430
|
+
} catch (err) {
|
|
2431
|
+
console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2251
2434
|
if (fs3.existsSync(clientDir)) {
|
|
2252
2435
|
this.app.use(import_express.default.static(clientDir));
|
|
2253
2436
|
}
|
|
2254
|
-
|
|
2255
|
-
if (fs3.existsSync(publicDir)) {
|
|
2256
|
-
this.app.use("/v1", import_express.default.static(publicDir));
|
|
2257
|
-
}
|
|
2258
|
-
this.app.get("/api/traces", (_req, res) => {
|
|
2437
|
+
this.app.get("/api/traces", (req, res) => {
|
|
2259
2438
|
try {
|
|
2260
|
-
const
|
|
2261
|
-
|
|
2439
|
+
const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
|
|
2440
|
+
const cursor = req.query.cursor ? parseFloat(req.query.cursor) : void 0;
|
|
2441
|
+
let allTraces = this.watcher.getAllTraces();
|
|
2442
|
+
if (cursor) {
|
|
2443
|
+
allTraces = allTraces.filter((t) => (t.lastModified || t.startTime) < cursor);
|
|
2444
|
+
}
|
|
2445
|
+
const page = allTraces.slice(0, limit);
|
|
2446
|
+
const serialized = page.map(serializeTrace);
|
|
2447
|
+
const lastTrace = page[page.length - 1];
|
|
2448
|
+
const nextCursor = page.length === limit && lastTrace ? lastTrace.lastModified || lastTrace.startTime : null;
|
|
2449
|
+
res.json({ traces: serialized, nextCursor });
|
|
2262
2450
|
} catch (_error) {
|
|
2263
2451
|
res.status(500).json({ error: "Failed to load traces" });
|
|
2264
2452
|
}
|
|
@@ -2549,6 +2737,235 @@ var DashboardServer = class {
|
|
|
2549
2737
|
res.status(500).json({ error: "Failed to load agent statistics" });
|
|
2550
2738
|
}
|
|
2551
2739
|
});
|
|
2740
|
+
this.app.get("/api/soma/tier", (_req, res) => {
|
|
2741
|
+
const somaVault = this.config.somaVault;
|
|
2742
|
+
if (!somaVault) {
|
|
2743
|
+
return res.json({ tier: "teaser", somaVault: false, governanceAvailable: false });
|
|
2744
|
+
}
|
|
2745
|
+
try {
|
|
2746
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2747
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2748
|
+
return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
|
|
2749
|
+
}
|
|
2750
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2751
|
+
const hasGovernance = report.governance && typeof report.governance.pending === "number";
|
|
2752
|
+
return res.json({
|
|
2753
|
+
tier: hasGovernance ? "pro" : "free",
|
|
2754
|
+
somaVault: true,
|
|
2755
|
+
governanceAvailable: !!hasGovernance
|
|
2756
|
+
});
|
|
2757
|
+
} catch {
|
|
2758
|
+
return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
this.app.get("/api/soma/report", (_req, res) => {
|
|
2762
|
+
const somaVault = this.config.somaVault;
|
|
2763
|
+
if (!somaVault) {
|
|
2764
|
+
return res.json({ available: false, teaser: true });
|
|
2765
|
+
}
|
|
2766
|
+
try {
|
|
2767
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2768
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2769
|
+
return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
|
|
2770
|
+
}
|
|
2771
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2772
|
+
res.json(report);
|
|
2773
|
+
} catch (error) {
|
|
2774
|
+
console.error("Soma report error:", error);
|
|
2775
|
+
res.json({ available: false, teaser: false, message: "Failed to read report" });
|
|
2776
|
+
}
|
|
2777
|
+
});
|
|
2778
|
+
this.app.get("/api/soma/governance", (_req, res) => {
|
|
2779
|
+
const somaVault = this.config.somaVault;
|
|
2780
|
+
if (!somaVault) {
|
|
2781
|
+
return res.json({ available: false });
|
|
2782
|
+
}
|
|
2783
|
+
try {
|
|
2784
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2785
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2786
|
+
return res.json({ available: false, message: "No report file. Run soma report." });
|
|
2787
|
+
}
|
|
2788
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2789
|
+
res.json({
|
|
2790
|
+
available: true,
|
|
2791
|
+
layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
|
|
2792
|
+
governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
|
|
2793
|
+
insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
|
|
2794
|
+
canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
|
|
2795
|
+
generatedAt: report.generatedAt
|
|
2796
|
+
});
|
|
2797
|
+
} catch (error) {
|
|
2798
|
+
console.error("Soma governance error:", error);
|
|
2799
|
+
res.status(500).json({ available: false, message: "Failed to read governance data" });
|
|
2800
|
+
}
|
|
2801
|
+
});
|
|
2802
|
+
const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
|
|
2803
|
+
const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
|
|
2804
|
+
this.app.post("/api/soma/governance/promote", (req, res) => {
|
|
2805
|
+
var _a;
|
|
2806
|
+
const somaVault = this.config.somaVault;
|
|
2807
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2808
|
+
const { entryId } = req.body ?? {};
|
|
2809
|
+
if (!entryId) return res.status(400).json({ error: "entryId required" });
|
|
2810
|
+
try {
|
|
2811
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2812
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2813
|
+
const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
|
|
2814
|
+
encoding: "utf-8",
|
|
2815
|
+
timeout: 1e4
|
|
2816
|
+
});
|
|
2817
|
+
res.json({ success: true, message: result.trim() });
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2820
|
+
}
|
|
2821
|
+
});
|
|
2822
|
+
this.app.post("/api/soma/governance/reject", (req, res) => {
|
|
2823
|
+
var _a;
|
|
2824
|
+
const somaVault = this.config.somaVault;
|
|
2825
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2826
|
+
const { entryId, reason } = req.body ?? {};
|
|
2827
|
+
if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
|
|
2828
|
+
try {
|
|
2829
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2830
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2831
|
+
const safeReason = sanitizeReason(String(reason));
|
|
2832
|
+
const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
|
|
2833
|
+
encoding: "utf-8",
|
|
2834
|
+
timeout: 1e4
|
|
2835
|
+
});
|
|
2836
|
+
res.json({ success: true, message: result.trim() });
|
|
2837
|
+
} catch (error) {
|
|
2838
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2839
|
+
}
|
|
2840
|
+
});
|
|
2841
|
+
this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
|
|
2842
|
+
var _a;
|
|
2843
|
+
const somaVault = this.config.somaVault;
|
|
2844
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2845
|
+
try {
|
|
2846
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2847
|
+
const safeId = sanitizeArg(String(req.params.id));
|
|
2848
|
+
const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
|
|
2849
|
+
encoding: "utf-8",
|
|
2850
|
+
timeout: 1e4
|
|
2851
|
+
});
|
|
2852
|
+
res.json({ available: true, output: result.trim() });
|
|
2853
|
+
} catch (error) {
|
|
2854
|
+
res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2855
|
+
}
|
|
2856
|
+
});
|
|
2857
|
+
this.app.get("/api/soma/policies", (_req, res) => {
|
|
2858
|
+
const somaVault = this.config.somaVault;
|
|
2859
|
+
if (!somaVault) return res.json({ policies: [] });
|
|
2860
|
+
try {
|
|
2861
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2862
|
+
if (!fs3.existsSync(reportPath)) return res.json({ policies: [] });
|
|
2863
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2864
|
+
res.json({ policies: report.policies ?? [] });
|
|
2865
|
+
} catch {
|
|
2866
|
+
res.json({ policies: [] });
|
|
2867
|
+
}
|
|
2868
|
+
});
|
|
2869
|
+
this.app.post("/api/soma/policies", import_express.default.json(), (req, res) => {
|
|
2870
|
+
var _a;
|
|
2871
|
+
const somaVault = this.config.somaVault;
|
|
2872
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2873
|
+
const { name, enforcement, scope, conditions } = req.body ?? {};
|
|
2874
|
+
if (!name) return res.status(400).json({ error: "name required" });
|
|
2875
|
+
try {
|
|
2876
|
+
const safeName = sanitizeArg(String(name));
|
|
2877
|
+
const safeEnf = sanitizeArg(String(enforcement || "warn"));
|
|
2878
|
+
const safeScope = sanitizeReason(String(scope || "all"));
|
|
2879
|
+
const safeCond = sanitizeReason(String(conditions || ""));
|
|
2880
|
+
const result = (0, import_node_child_process.execSync)(
|
|
2881
|
+
`npx soma policy create "${safeName}" --enforcement ${safeEnf} --scope "${safeScope}" --conditions "${safeCond}" --vault "${somaVault}"`,
|
|
2882
|
+
{ encoding: "utf-8", timeout: 1e4 }
|
|
2883
|
+
);
|
|
2884
|
+
res.json({ success: true, message: result.trim() });
|
|
2885
|
+
} catch (error) {
|
|
2886
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2889
|
+
this.app.delete("/api/soma/policies/:name", (req, res) => {
|
|
2890
|
+
var _a;
|
|
2891
|
+
const somaVault = this.config.somaVault;
|
|
2892
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2893
|
+
try {
|
|
2894
|
+
const safeName = sanitizeArg(String(req.params.name));
|
|
2895
|
+
const result = (0, import_node_child_process.execSync)(
|
|
2896
|
+
`npx soma policy delete "${safeName}" --vault "${somaVault}"`,
|
|
2897
|
+
{ encoding: "utf-8", timeout: 1e4 }
|
|
2898
|
+
);
|
|
2899
|
+
res.json({ success: true, message: result.trim() });
|
|
2900
|
+
} catch (error) {
|
|
2901
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2902
|
+
}
|
|
2903
|
+
});
|
|
2904
|
+
this.app.get("/api/soma/vault/entities", (req, res) => {
|
|
2905
|
+
const somaVault = this.config.somaVault;
|
|
2906
|
+
if (!somaVault) return res.json({ entities: [], total: 0 });
|
|
2907
|
+
try {
|
|
2908
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2909
|
+
if (!fs3.existsSync(reportPath)) return res.json({ entities: [], total: 0 });
|
|
2910
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2911
|
+
let entities = [
|
|
2912
|
+
...(report.agents ?? []).map((a) => ({ ...a, type: "agent", id: a.name })),
|
|
2913
|
+
...(report.insights ?? []).map((i, idx) => {
|
|
2914
|
+
var _a;
|
|
2915
|
+
return { ...i, type: i.type || "insight", id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}` };
|
|
2916
|
+
}),
|
|
2917
|
+
...(report.policies ?? []).map((p) => ({ ...p, type: "policy", id: p.name }))
|
|
2918
|
+
];
|
|
2919
|
+
const { type, layer, q, limit: limitStr, offset: offsetStr } = req.query;
|
|
2920
|
+
if (type) entities = entities.filter((e) => e.type === type);
|
|
2921
|
+
if (layer) entities = entities.filter((e) => e.layer === layer);
|
|
2922
|
+
if (q) {
|
|
2923
|
+
const lq = q.toLowerCase();
|
|
2924
|
+
entities = entities.filter((e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq));
|
|
2925
|
+
}
|
|
2926
|
+
const total = entities.length;
|
|
2927
|
+
const offset = parseInt(offsetStr || "0", 10);
|
|
2928
|
+
const limit = Math.min(parseInt(limitStr || "50", 10), 200);
|
|
2929
|
+
entities = entities.slice(offset, offset + limit);
|
|
2930
|
+
res.json({ entities, total });
|
|
2931
|
+
} catch (error) {
|
|
2932
|
+
console.error("Vault entities error:", error);
|
|
2933
|
+
res.json({ entities: [], total: 0 });
|
|
2934
|
+
}
|
|
2935
|
+
});
|
|
2936
|
+
this.app.get("/api/soma/vault/entities/:type/:id", (req, res) => {
|
|
2937
|
+
const somaVault = this.config.somaVault;
|
|
2938
|
+
if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
|
|
2939
|
+
try {
|
|
2940
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2941
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2942
|
+
const { type, id } = req.params;
|
|
2943
|
+
let entity = null;
|
|
2944
|
+
if (type === "agent") {
|
|
2945
|
+
entity = (report.agents ?? []).find((a) => a.name === id);
|
|
2946
|
+
} else if (type === "policy") {
|
|
2947
|
+
entity = (report.policies ?? []).find((p) => p.name === id);
|
|
2948
|
+
} else {
|
|
2949
|
+
entity = (report.insights ?? []).find(
|
|
2950
|
+
(i) => {
|
|
2951
|
+
var _a;
|
|
2952
|
+
return (((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || "") === id || i.title === id;
|
|
2953
|
+
}
|
|
2954
|
+
);
|
|
2955
|
+
}
|
|
2956
|
+
if (!entity) return res.status(404).json({ error: "Entity not found" });
|
|
2957
|
+
res.json({
|
|
2958
|
+
...entity,
|
|
2959
|
+
type,
|
|
2960
|
+
id,
|
|
2961
|
+
body: entity.claim || entity.conditions || "",
|
|
2962
|
+
tags: entity.tags ?? [],
|
|
2963
|
+
related: entity.related ?? []
|
|
2964
|
+
});
|
|
2965
|
+
} catch {
|
|
2966
|
+
res.status(404).json({ error: "Entity not found" });
|
|
2967
|
+
}
|
|
2968
|
+
});
|
|
2552
2969
|
this.app.get("/api/process-health", (_req, res) => {
|
|
2553
2970
|
var _a, _b;
|
|
2554
2971
|
try {
|
|
@@ -2561,7 +2978,14 @@ var DashboardServer = class {
|
|
|
2561
2978
|
path3.dirname(this.config.tracesDir),
|
|
2562
2979
|
...this.config.dataDirs || []
|
|
2563
2980
|
];
|
|
2564
|
-
|
|
2981
|
+
let configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
|
|
2982
|
+
const pref = getProcessPreference(this.userConfig);
|
|
2983
|
+
if (pref) {
|
|
2984
|
+
const hasPreferred = configs.some((c) => c.processName === pref.prefer);
|
|
2985
|
+
if (hasPreferred) {
|
|
2986
|
+
configs = configs.filter((c) => c.processName !== pref.over);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2565
2989
|
if (configs.length === 0) {
|
|
2566
2990
|
return res.json(null);
|
|
2567
2991
|
}
|
|
@@ -2645,35 +3069,32 @@ var DashboardServer = class {
|
|
|
2645
3069
|
}
|
|
2646
3070
|
} catch {
|
|
2647
3071
|
}
|
|
2648
|
-
const watched = [
|
|
3072
|
+
const watched = [...new Set([
|
|
2649
3073
|
this.config.tracesDir,
|
|
2650
3074
|
...this.config.dataDirs || [],
|
|
2651
3075
|
...extraDirs
|
|
2652
|
-
];
|
|
3076
|
+
].map((w) => path3.resolve(w)))];
|
|
2653
3077
|
const discovered = [];
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
const
|
|
2664
|
-
if (
|
|
3078
|
+
const svcNames = getSystemdServices(this.userConfig);
|
|
3079
|
+
if (svcNames.length > 0) {
|
|
3080
|
+
try {
|
|
3081
|
+
const { execSync: execSync2 } = require("child_process");
|
|
3082
|
+
const raw = execSync2(
|
|
3083
|
+
`systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
|
|
3084
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
3085
|
+
);
|
|
3086
|
+
for (const line of raw.split("\n")) {
|
|
3087
|
+
const match = line.match(/path=([^\s;]+)/);
|
|
3088
|
+
if (match == null ? void 0 : match[1]) {
|
|
3089
|
+
const dir = path3.dirname(match[1]);
|
|
3090
|
+
if (fs3.existsSync(dir)) discovered.push(dir);
|
|
3091
|
+
}
|
|
2665
3092
|
}
|
|
3093
|
+
} catch {
|
|
2666
3094
|
}
|
|
2667
|
-
} catch {
|
|
2668
3095
|
}
|
|
2669
3096
|
const commonPaths = [
|
|
2670
|
-
|
|
2671
|
-
path3.join(home, ".alfred/data"),
|
|
2672
|
-
path3.join(home, ".openclaw/workspace/traces"),
|
|
2673
|
-
path3.join(home, ".openclaw/subagents"),
|
|
2674
|
-
path3.join(home, ".openclaw/cron/runs"),
|
|
2675
|
-
path3.join(home, ".openclaw/cron"),
|
|
2676
|
-
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
3097
|
+
...getDiscoveryPaths(this.userConfig),
|
|
2677
3098
|
path3.join(home, ".agentflow/traces")
|
|
2678
3099
|
];
|
|
2679
3100
|
for (const p of commonPaths) {
|
|
@@ -2777,18 +3198,10 @@ var DashboardServer = class {
|
|
|
2777
3198
|
this.app.get("/ready", (_req, res) => {
|
|
2778
3199
|
res.json({ status: "ready" });
|
|
2779
3200
|
});
|
|
2780
|
-
this.app.get("/v1/*", (_req, res) => {
|
|
2781
|
-
const legacyIndex = path3.join(__dirname, "../public/index.html");
|
|
2782
|
-
if (fs3.existsSync(legacyIndex)) {
|
|
2783
|
-
res.sendFile(legacyIndex);
|
|
2784
|
-
} else {
|
|
2785
|
-
res.status(404).send("Legacy dashboard not found");
|
|
2786
|
-
}
|
|
2787
|
-
});
|
|
2788
3201
|
this.app.get("*", (_req, res) => {
|
|
2789
|
-
const
|
|
2790
|
-
if (fs3.existsSync(
|
|
2791
|
-
res.sendFile(
|
|
3202
|
+
const clientIndex2 = path3.join(__dirname, "../dist/client/index.html");
|
|
3203
|
+
if (fs3.existsSync(clientIndex2)) {
|
|
3204
|
+
res.sendFile(clientIndex2);
|
|
2792
3205
|
} else {
|
|
2793
3206
|
res.status(404).send("Dashboard not found - public files may not be built");
|
|
2794
3207
|
}
|
|
@@ -2814,6 +3227,41 @@ var DashboardServer = class {
|
|
|
2814
3227
|
});
|
|
2815
3228
|
});
|
|
2816
3229
|
}
|
|
3230
|
+
/** Watch soma-report.json for changes and broadcast updates via WebSocket. */
|
|
3231
|
+
setupSomaReportWatcher() {
|
|
3232
|
+
const somaVault = this.config.somaVault;
|
|
3233
|
+
if (!somaVault) return;
|
|
3234
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
3235
|
+
const reportDir = path3.dirname(reportPath);
|
|
3236
|
+
if (!fs3.existsSync(reportDir)) return;
|
|
3237
|
+
let debounceTimer = null;
|
|
3238
|
+
const watcher = import_chokidar2.default.watch(reportPath, {
|
|
3239
|
+
ignoreInitial: true,
|
|
3240
|
+
persistent: true,
|
|
3241
|
+
awaitWriteFinish: { stabilityThreshold: 500 }
|
|
3242
|
+
});
|
|
3243
|
+
watcher.on("change", () => {
|
|
3244
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3245
|
+
debounceTimer = setTimeout(() => {
|
|
3246
|
+
var _a, _b;
|
|
3247
|
+
try {
|
|
3248
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
3249
|
+
this.broadcast({ type: "soma-report-updated", data: report });
|
|
3250
|
+
if (report.generatedAt) {
|
|
3251
|
+
this.broadcast({
|
|
3252
|
+
type: "soma-activity",
|
|
3253
|
+
data: {
|
|
3254
|
+
action: "report-updated",
|
|
3255
|
+
description: `Report updated: ${((_a = report.totals) == null ? void 0 : _a.agents) ?? 0} agents, ${((_b = report.totals) == null ? void 0 : _b.insights) ?? 0} insights`,
|
|
3256
|
+
timestamp: report.generatedAt
|
|
3257
|
+
}
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
} catch {
|
|
3261
|
+
}
|
|
3262
|
+
}, 500);
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
2817
3265
|
/**
|
|
2818
3266
|
* Filter an agent's traces to valid ExecutionGraphs and convert via loadGraph().
|
|
2819
3267
|
* Returns only traces with proper nodes (Map or non-empty object), skipping session-only traces.
|
|
@@ -3035,24 +3483,49 @@ var DashboardServer = class {
|
|
|
3035
3483
|
});
|
|
3036
3484
|
}
|
|
3037
3485
|
async start() {
|
|
3038
|
-
return new Promise((
|
|
3486
|
+
return new Promise((resolve5) => {
|
|
3039
3487
|
const host = this.config.host || "localhost";
|
|
3040
3488
|
this.server.listen(this.config.port, host, () => {
|
|
3041
3489
|
console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
|
|
3042
3490
|
console.log(`Watching traces in: ${this.config.tracesDir}`);
|
|
3043
|
-
|
|
3491
|
+
resolve5();
|
|
3044
3492
|
});
|
|
3045
3493
|
});
|
|
3046
3494
|
}
|
|
3495
|
+
/** Check if any src/client file is newer than the built bundle. */
|
|
3496
|
+
isClientStale(srcDir, distDir) {
|
|
3497
|
+
try {
|
|
3498
|
+
const distIndex = path3.join(distDir, "index.html");
|
|
3499
|
+
if (!fs3.existsSync(distIndex)) return true;
|
|
3500
|
+
const distMtime = fs3.statSync(distIndex).mtimeMs;
|
|
3501
|
+
const check = (dir) => {
|
|
3502
|
+
for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
|
|
3503
|
+
const full = path3.join(dir, entry.name);
|
|
3504
|
+
if (entry.isDirectory()) {
|
|
3505
|
+
if (check(full)) return true;
|
|
3506
|
+
} else if (fs3.statSync(full).mtimeMs > distMtime) {
|
|
3507
|
+
return true;
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
return false;
|
|
3511
|
+
};
|
|
3512
|
+
return check(srcDir);
|
|
3513
|
+
} catch {
|
|
3514
|
+
return false;
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3047
3517
|
async stop() {
|
|
3048
|
-
return new Promise((
|
|
3518
|
+
return new Promise((resolve5) => {
|
|
3049
3519
|
this.watcher.stop();
|
|
3050
3520
|
this.server.close(() => {
|
|
3051
3521
|
console.log("Dashboard server stopped");
|
|
3052
|
-
|
|
3522
|
+
resolve5();
|
|
3053
3523
|
});
|
|
3054
3524
|
});
|
|
3055
3525
|
}
|
|
3526
|
+
getConfigPath() {
|
|
3527
|
+
return this.configPath;
|
|
3528
|
+
}
|
|
3056
3529
|
getStats() {
|
|
3057
3530
|
return this.stats.getGlobalStats();
|
|
3058
3531
|
}
|