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/index.cjs
CHANGED
|
@@ -37,11 +37,86 @@ __export(index_exports, {
|
|
|
37
37
|
module.exports = __toCommonJS(index_exports);
|
|
38
38
|
|
|
39
39
|
// src/server.ts
|
|
40
|
+
var import_node_child_process = require("child_process");
|
|
40
41
|
var fs3 = __toESM(require("fs"), 1);
|
|
41
42
|
var import_node_http = require("http");
|
|
42
43
|
var path3 = __toESM(require("path"), 1);
|
|
43
44
|
var import_node_url = require("url");
|
|
45
|
+
|
|
46
|
+
// src/config.ts
|
|
47
|
+
var import_node_fs = require("fs");
|
|
48
|
+
var import_node_os = require("os");
|
|
49
|
+
var import_node_path = require("path");
|
|
50
|
+
var EMPTY_CONFIG = {};
|
|
51
|
+
function expandTilde(p) {
|
|
52
|
+
if (p.startsWith("~/") || p === "~") {
|
|
53
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), p.slice(1));
|
|
54
|
+
}
|
|
55
|
+
return p;
|
|
56
|
+
}
|
|
57
|
+
function loadConfig(explicitPath) {
|
|
58
|
+
const candidates = [];
|
|
59
|
+
if (explicitPath) {
|
|
60
|
+
candidates.push((0, import_node_path.resolve)(explicitPath));
|
|
61
|
+
}
|
|
62
|
+
if (process.env.AGENTFLOW_CONFIG) {
|
|
63
|
+
candidates.push((0, import_node_path.resolve)(process.env.AGENTFLOW_CONFIG));
|
|
64
|
+
}
|
|
65
|
+
candidates.push((0, import_node_path.resolve)("agentflow.config.json"));
|
|
66
|
+
candidates.push((0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "agentflow", "config.json"));
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
if (!(0, import_node_fs.existsSync)(candidate)) continue;
|
|
69
|
+
try {
|
|
70
|
+
const raw = (0, import_node_fs.readFileSync)(candidate, "utf-8");
|
|
71
|
+
const parsed = JSON.parse(raw);
|
|
72
|
+
const cleaned = stripCommentKeys(parsed);
|
|
73
|
+
console.log(`Loaded config: ${candidate}`);
|
|
74
|
+
return { config: cleaned, configPath: candidate };
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.warn(`Warning: Failed to load config from ${candidate}: ${err.message}`);
|
|
77
|
+
console.warn("Continuing with empty defaults.");
|
|
78
|
+
return { config: EMPTY_CONFIG, configPath: null };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { config: EMPTY_CONFIG, configPath: null };
|
|
82
|
+
}
|
|
83
|
+
function stripCommentKeys(obj) {
|
|
84
|
+
if (Array.isArray(obj)) return obj.map(stripCommentKeys);
|
|
85
|
+
if (obj && typeof obj === "object") {
|
|
86
|
+
const result = {};
|
|
87
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
88
|
+
if (key.startsWith("//")) continue;
|
|
89
|
+
result[key] = stripCommentKeys(value);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
return obj;
|
|
94
|
+
}
|
|
95
|
+
function getAliases(config) {
|
|
96
|
+
return config.aliases ?? {};
|
|
97
|
+
}
|
|
98
|
+
function getSkipFiles(config) {
|
|
99
|
+
return config.skipFiles ?? [];
|
|
100
|
+
}
|
|
101
|
+
function getSkipDirectories(config) {
|
|
102
|
+
return config.skipDirectories ?? [];
|
|
103
|
+
}
|
|
104
|
+
function getDiscoveryPaths(config) {
|
|
105
|
+
return (config.discoveryPaths ?? []).map(expandTilde);
|
|
106
|
+
}
|
|
107
|
+
function getSystemdServices(config) {
|
|
108
|
+
return config.systemdServices ?? [];
|
|
109
|
+
}
|
|
110
|
+
function getAgentDetection(config) {
|
|
111
|
+
return config.agentDetection ?? {};
|
|
112
|
+
}
|
|
113
|
+
function getProcessPreference(config) {
|
|
114
|
+
return config.processPreference ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/server.ts
|
|
44
118
|
var import_agentflow_core3 = require("agentflow-core");
|
|
119
|
+
var import_chokidar2 = __toESM(require("chokidar"), 1);
|
|
45
120
|
var import_express = __toESM(require("express"), 1);
|
|
46
121
|
var import_ws = require("ws");
|
|
47
122
|
|
|
@@ -74,17 +149,17 @@ var AgentFlowAdapter = class {
|
|
|
74
149
|
};
|
|
75
150
|
|
|
76
151
|
// src/adapters/openclaw.ts
|
|
77
|
-
var
|
|
78
|
-
var
|
|
152
|
+
var import_node_fs2 = require("fs");
|
|
153
|
+
var import_node_path2 = require("path");
|
|
79
154
|
var jobCache = /* @__PURE__ */ new Map();
|
|
80
155
|
function loadJobs(openclawDir) {
|
|
81
156
|
const cached = jobCache.get(openclawDir);
|
|
82
157
|
if (cached) return cached;
|
|
83
|
-
const jobsPath = (0,
|
|
158
|
+
const jobsPath = (0, import_node_path2.join)(openclawDir, "cron", "jobs.json");
|
|
84
159
|
const map = /* @__PURE__ */ new Map();
|
|
85
160
|
try {
|
|
86
|
-
if ((0,
|
|
87
|
-
const data = JSON.parse((0,
|
|
161
|
+
if ((0, import_node_fs2.existsSync)(jobsPath)) {
|
|
162
|
+
const data = JSON.parse((0, import_node_fs2.readFileSync)(jobsPath, "utf-8"));
|
|
88
163
|
const jobs = Array.isArray(data) ? data : data.jobs ?? [];
|
|
89
164
|
for (const job of jobs) {
|
|
90
165
|
if (job.id) map.set(job.id, job);
|
|
@@ -96,19 +171,19 @@ function loadJobs(openclawDir) {
|
|
|
96
171
|
return map;
|
|
97
172
|
}
|
|
98
173
|
function findOpenClawRoot(filePath) {
|
|
99
|
-
let dir = (0,
|
|
174
|
+
let dir = (0, import_node_path2.dirname)(filePath);
|
|
100
175
|
for (let i = 0; i < 5; i++) {
|
|
101
|
-
if ((0,
|
|
176
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, "cron", "jobs.json")) || (0, import_node_path2.basename)(dir) === ".openclaw") {
|
|
102
177
|
return dir;
|
|
103
178
|
}
|
|
104
|
-
dir = (0,
|
|
179
|
+
dir = (0, import_node_path2.dirname)(dir);
|
|
105
180
|
}
|
|
106
181
|
return null;
|
|
107
182
|
}
|
|
108
183
|
var OpenClawAdapter = class {
|
|
109
184
|
name = "openclaw";
|
|
110
185
|
detect(dirPath) {
|
|
111
|
-
return (0,
|
|
186
|
+
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"));
|
|
112
187
|
}
|
|
113
188
|
canHandle(filePath) {
|
|
114
189
|
if (!filePath.endsWith(".jsonl")) return false;
|
|
@@ -117,7 +192,7 @@ var OpenClawAdapter = class {
|
|
|
117
192
|
parse(filePath) {
|
|
118
193
|
const traces = [];
|
|
119
194
|
try {
|
|
120
|
-
const content = (0,
|
|
195
|
+
const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
121
196
|
const root = findOpenClawRoot(filePath);
|
|
122
197
|
const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
|
|
123
198
|
for (const line of content.split("\n")) {
|
|
@@ -129,7 +204,7 @@ var OpenClawAdapter = class {
|
|
|
129
204
|
continue;
|
|
130
205
|
}
|
|
131
206
|
if (entry.action !== "finished") continue;
|
|
132
|
-
const jobId = entry.jobId ?? (0,
|
|
207
|
+
const jobId = entry.jobId ?? (0, import_node_path2.basename)(filePath, ".jsonl");
|
|
133
208
|
const job = jobs.get(jobId);
|
|
134
209
|
const jobName = (job == null ? void 0 : job.name) ?? jobId;
|
|
135
210
|
const startTime = entry.runAtMs ?? entry.ts;
|
|
@@ -181,8 +256,8 @@ var OpenClawAdapter = class {
|
|
|
181
256
|
};
|
|
182
257
|
|
|
183
258
|
// src/adapters/otel.ts
|
|
184
|
-
var
|
|
185
|
-
var
|
|
259
|
+
var import_node_fs3 = require("fs");
|
|
260
|
+
var import_node_path3 = require("path");
|
|
186
261
|
var SPAN_TYPE_MAP = {
|
|
187
262
|
"gen_ai.chat": "llm",
|
|
188
263
|
"gen_ai.completion": "llm",
|
|
@@ -289,8 +364,8 @@ var OTelAdapter = class {
|
|
|
289
364
|
name = "otel";
|
|
290
365
|
detect(dirPath) {
|
|
291
366
|
try {
|
|
292
|
-
if ((0,
|
|
293
|
-
const files = (0,
|
|
367
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(dirPath, "otel-traces"))) return true;
|
|
368
|
+
const files = (0, import_node_fs3.readdirSync)(dirPath);
|
|
294
369
|
return files.some((f) => f.endsWith(".otlp.json"));
|
|
295
370
|
} catch {
|
|
296
371
|
return false;
|
|
@@ -301,7 +376,7 @@ var OTelAdapter = class {
|
|
|
301
376
|
}
|
|
302
377
|
parse(filePath) {
|
|
303
378
|
try {
|
|
304
|
-
const content = (0,
|
|
379
|
+
const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
|
|
305
380
|
const payload = JSON.parse(content);
|
|
306
381
|
const traces = parseOtlpPayload(payload);
|
|
307
382
|
for (const t of traces) t.filePath = filePath;
|
|
@@ -343,9 +418,7 @@ function extractSource(agentId) {
|
|
|
343
418
|
const colonIdx = agentId.indexOf(":");
|
|
344
419
|
if (colonIdx > 0 && colonIdx < 20) {
|
|
345
420
|
const prefix = agentId.slice(0, colonIdx);
|
|
346
|
-
|
|
347
|
-
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
348
|
-
}
|
|
421
|
+
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
349
422
|
}
|
|
350
423
|
return { source: "agentflow", localId: agentId };
|
|
351
424
|
}
|
|
@@ -376,16 +449,20 @@ function deduplicateAgents(agents) {
|
|
|
376
449
|
for (const a of tagged) {
|
|
377
450
|
const suffix = extractSuffix(a.localId);
|
|
378
451
|
if (!suffix) continue;
|
|
379
|
-
const
|
|
452
|
+
const key = `${a.source}:${suffix}`;
|
|
453
|
+
const group = suffixGroups.get(key) ?? [];
|
|
380
454
|
group.push(a);
|
|
381
|
-
suffixGroups.set(
|
|
455
|
+
suffixGroups.set(key, group);
|
|
382
456
|
}
|
|
383
457
|
const mergedIds = /* @__PURE__ */ new Set();
|
|
384
458
|
const mergedAgents = [];
|
|
385
|
-
for (const [
|
|
459
|
+
for (const [_key, group] of suffixGroups) {
|
|
460
|
+
const suffix = extractSuffix(group[0].localId);
|
|
386
461
|
if (group.length < 2) continue;
|
|
387
462
|
const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
|
|
388
463
|
if (prefixes.size < 2) continue;
|
|
464
|
+
const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
|
|
465
|
+
if (longPrefixes.length >= 2) continue;
|
|
389
466
|
const merged = {
|
|
390
467
|
agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
|
|
391
468
|
displayName: suffix,
|
|
@@ -433,10 +510,7 @@ function groupAgents(agents) {
|
|
|
433
510
|
}
|
|
434
511
|
const SOURCE_DISPLAY = {
|
|
435
512
|
agentflow: "AgentFlow",
|
|
436
|
-
|
|
437
|
-
otel: "OpenTelemetry",
|
|
438
|
-
langchain: "LangChain",
|
|
439
|
-
crewai: "CrewAI"
|
|
513
|
+
otel: "OpenTelemetry"
|
|
440
514
|
};
|
|
441
515
|
const groups = [];
|
|
442
516
|
for (const [source, sourceAgents] of sourceMap) {
|
|
@@ -782,10 +856,6 @@ function getUniversalNodeStatus(activity) {
|
|
|
782
856
|
return "completed";
|
|
783
857
|
}
|
|
784
858
|
function openClawSessionIdToAgent(sessionId) {
|
|
785
|
-
if (sessionId.startsWith("janitor-")) return "vault-janitor";
|
|
786
|
-
if (sessionId.startsWith("curator-")) return "vault-curator";
|
|
787
|
-
if (sessionId.startsWith("distiller-")) return "vault-distiller";
|
|
788
|
-
if (sessionId.startsWith("main-")) return "alfred-main";
|
|
789
859
|
const firstSegment = sessionId.split("-")[0];
|
|
790
860
|
if (firstSegment) return firstSegment;
|
|
791
861
|
return "openclaw";
|
|
@@ -798,19 +868,81 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
798
868
|
tracesDir;
|
|
799
869
|
dataDirs;
|
|
800
870
|
allWatchDirs;
|
|
871
|
+
maxAgeMs;
|
|
872
|
+
userConfig;
|
|
801
873
|
constructor(tracesDirOrOptions) {
|
|
802
874
|
super();
|
|
875
|
+
const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
|
|
876
|
+
const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
|
|
877
|
+
const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
|
|
803
878
|
if (typeof tracesDirOrOptions === "string") {
|
|
804
879
|
this.tracesDir = path.resolve(tracesDirOrOptions);
|
|
805
880
|
this.dataDirs = [];
|
|
881
|
+
this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
|
|
882
|
+
this.userConfig = {};
|
|
806
883
|
} else {
|
|
807
884
|
this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
|
|
808
885
|
this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
|
|
809
|
-
|
|
810
|
-
|
|
886
|
+
this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
|
|
887
|
+
this.userConfig = tracesDirOrOptions.userConfig ?? {};
|
|
888
|
+
}
|
|
889
|
+
this.skipFiles = /* @__PURE__ */ new Set([
|
|
890
|
+
..._TraceWatcher.STRUCTURAL_SKIP_FILES,
|
|
891
|
+
...getSkipFiles(this.userConfig)
|
|
892
|
+
]);
|
|
893
|
+
this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
|
|
894
|
+
this.allWatchDirs = [...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))];
|
|
811
895
|
this.ensureTracesDir();
|
|
812
896
|
this.loadExistingFiles();
|
|
897
|
+
this.archiveOldTraces();
|
|
813
898
|
this.startWatching();
|
|
899
|
+
setInterval(() => this.archiveOldTraces(), 6 * 60 * 60 * 1e3);
|
|
900
|
+
}
|
|
901
|
+
/** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
|
|
902
|
+
archiveOldTraces() {
|
|
903
|
+
const cutoff = Date.now() - this.maxAgeMs;
|
|
904
|
+
let archived = 0;
|
|
905
|
+
for (const dir of this.allWatchDirs) {
|
|
906
|
+
if (!fs.existsSync(dir)) continue;
|
|
907
|
+
try {
|
|
908
|
+
this.archiveDirectory(dir, cutoff, 0);
|
|
909
|
+
} catch (error) {
|
|
910
|
+
console.warn(`Archival error in ${dir}:`, error.message);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
archiveDirectory(dir, cutoff, depth) {
|
|
915
|
+
if (depth > 10) return 0;
|
|
916
|
+
if (path.basename(dir) === "archive") return 0;
|
|
917
|
+
let archived = 0;
|
|
918
|
+
try {
|
|
919
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
920
|
+
for (const entry of entries) {
|
|
921
|
+
if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
|
|
922
|
+
const fullPath = path.join(dir, entry.name);
|
|
923
|
+
if (entry.isDirectory()) {
|
|
924
|
+
archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
|
|
928
|
+
try {
|
|
929
|
+
const stats = fs.statSync(fullPath);
|
|
930
|
+
if (stats.mtimeMs >= cutoff) continue;
|
|
931
|
+
const mtime = new Date(stats.mtimeMs);
|
|
932
|
+
const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
|
|
933
|
+
const archiveDir = path.join(this.tracesDir, "archive", yearMonth);
|
|
934
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
935
|
+
const dest = path.join(archiveDir, entry.name);
|
|
936
|
+
fs.renameSync(fullPath, dest);
|
|
937
|
+
const key = this.traceKey(fullPath);
|
|
938
|
+
this.traces.delete(key);
|
|
939
|
+
archived++;
|
|
940
|
+
} catch {
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
} catch {
|
|
944
|
+
}
|
|
945
|
+
return archived;
|
|
814
946
|
}
|
|
815
947
|
ensureTracesDir() {
|
|
816
948
|
if (!fs.existsSync(this.tracesDir)) {
|
|
@@ -843,9 +975,17 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
843
975
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
844
976
|
for (const entry of entries) {
|
|
845
977
|
if (entry.name.startsWith(".")) continue;
|
|
978
|
+
if (entry.name === "archive") continue;
|
|
979
|
+
if (this.userSkipDirs.has(entry.name)) continue;
|
|
846
980
|
const fullPath = path.join(dir, entry.name);
|
|
847
981
|
if (entry.isFile()) {
|
|
848
982
|
if (this.isSupportedFile(entry.name)) {
|
|
983
|
+
try {
|
|
984
|
+
const mtime = fs.statSync(fullPath).mtimeMs;
|
|
985
|
+
if (Date.now() - mtime > this.maxAgeMs) continue;
|
|
986
|
+
} catch {
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
849
989
|
if (this.loadFile(fullPath)) {
|
|
850
990
|
fileCount++;
|
|
851
991
|
}
|
|
@@ -863,8 +1003,8 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
863
1003
|
isSupportedFile(filename) {
|
|
864
1004
|
return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
|
|
865
1005
|
}
|
|
866
|
-
/**
|
|
867
|
-
static
|
|
1006
|
+
/** Structural file names that are never trace data — always skipped. */
|
|
1007
|
+
static STRUCTURAL_SKIP_FILES = /* @__PURE__ */ new Set([
|
|
868
1008
|
"workers.json",
|
|
869
1009
|
"package.json",
|
|
870
1010
|
"package-lock.json",
|
|
@@ -879,6 +1019,10 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
879
1019
|
"update-check.json",
|
|
880
1020
|
"exec-approvals.json"
|
|
881
1021
|
]);
|
|
1022
|
+
/** Skip files = structural + user config */
|
|
1023
|
+
skipFiles;
|
|
1024
|
+
/** Skip directories from user config */
|
|
1025
|
+
userSkipDirs;
|
|
882
1026
|
static SKIP_SUFFIXES = [
|
|
883
1027
|
"-state.json",
|
|
884
1028
|
"-config.json",
|
|
@@ -890,7 +1034,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
890
1034
|
/** Load a file using the adapter registry, falling back to built-in parsing. */
|
|
891
1035
|
loadFile(filePath) {
|
|
892
1036
|
const filename = path.basename(filePath);
|
|
893
|
-
if (
|
|
1037
|
+
if (this.skipFiles.has(filename)) return false;
|
|
894
1038
|
if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
|
|
895
1039
|
const adapter = findAdapter(filePath);
|
|
896
1040
|
if (adapter && adapter.name !== "agentflow") {
|
|
@@ -1067,43 +1211,26 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1067
1211
|
}
|
|
1068
1212
|
return traces;
|
|
1069
1213
|
}
|
|
1070
|
-
/**
|
|
1071
|
-
* Normalise agent identifiers so that the same worker is never shown
|
|
1072
|
-
* under two different names (e.g. "vault-curator" vs "openclaw-vault-curator").
|
|
1073
|
-
*
|
|
1074
|
-
* Canonical names: alfred-main, vault-curator, vault-janitor,
|
|
1075
|
-
* vault-distiller, vault-surveyor
|
|
1076
|
-
*/
|
|
1077
|
-
static AGENT_ALIASES = {
|
|
1078
|
-
"openclaw-main": "alfred-main",
|
|
1079
|
-
"openclaw-vault-curator": "vault-curator",
|
|
1080
|
-
"openclaw-vault-janitor": "vault-janitor",
|
|
1081
|
-
"openclaw-vault-distiller": "vault-distiller",
|
|
1082
|
-
"openclaw-vault-surveyor": "vault-surveyor",
|
|
1083
|
-
"alfred-curator": "vault-curator",
|
|
1084
|
-
"alfred-janitor": "vault-janitor",
|
|
1085
|
-
"alfred-distiller": "vault-distiller",
|
|
1086
|
-
"alfred-surveyor": "vault-surveyor",
|
|
1087
|
-
curator: "vault-curator",
|
|
1088
|
-
janitor: "vault-janitor",
|
|
1089
|
-
distiller: "vault-distiller",
|
|
1090
|
-
surveyor: "vault-surveyor"
|
|
1091
|
-
};
|
|
1214
|
+
/** Normalise agent identifiers using config-driven alias map. */
|
|
1092
1215
|
normaliseAgentId(raw) {
|
|
1093
|
-
|
|
1216
|
+
const aliases = getAliases(this.userConfig);
|
|
1217
|
+
return aliases[raw] ?? raw;
|
|
1094
1218
|
}
|
|
1095
1219
|
detectAgentIdentifier(activity, _filename, filePath) {
|
|
1096
1220
|
if (activity.agent_id) {
|
|
1097
|
-
|
|
1098
|
-
if (agentId === "main" && filePath.includes(".alfred/")) return this.normaliseAgentId("alfred-main");
|
|
1099
|
-
return this.normaliseAgentId(agentId);
|
|
1221
|
+
return this.normaliseAgentId(activity.agent_id);
|
|
1100
1222
|
}
|
|
1101
1223
|
const pathAgent = this.extractAgentFromPath(filePath);
|
|
1102
|
-
|
|
1224
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1225
|
+
if (detection.filePatterns) {
|
|
1103
1226
|
const basename3 = path.basename(filePath, path.extname(filePath));
|
|
1104
|
-
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1227
|
+
for (const [pattern, template] of Object.entries(detection.filePatterns)) {
|
|
1228
|
+
const re = new RegExp(`^(${pattern})$`);
|
|
1229
|
+
const match = basename3.match(re);
|
|
1230
|
+
if (match) {
|
|
1231
|
+
const resolved = template.replace("${match}", match[1]);
|
|
1232
|
+
return this.normaliseAgentId(resolved);
|
|
1233
|
+
}
|
|
1107
1234
|
}
|
|
1108
1235
|
}
|
|
1109
1236
|
return this.normaliseAgentId(pathAgent);
|
|
@@ -1111,20 +1238,23 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1111
1238
|
extractAgentFromPath(filePath) {
|
|
1112
1239
|
const filename = path.basename(filePath, path.extname(filePath));
|
|
1113
1240
|
const pathParts = filePath.split(path.sep);
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1241
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1242
|
+
let pathPrefix = "";
|
|
1243
|
+
if (detection.pathPatterns) {
|
|
1244
|
+
for (const [pathSubstring, agentId] of Object.entries(detection.pathPatterns)) {
|
|
1245
|
+
if (filePath.includes(pathSubstring)) {
|
|
1246
|
+
pathPrefix = agentId;
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1121
1249
|
}
|
|
1122
|
-
return "openclaw";
|
|
1123
1250
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1251
|
+
const agentsIndex = pathParts.lastIndexOf("agents");
|
|
1252
|
+
if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
|
|
1253
|
+
const agentName = pathParts[agentsIndex + 1];
|
|
1254
|
+
return pathPrefix ? `${pathPrefix}-${agentName}` : agentName;
|
|
1126
1255
|
}
|
|
1127
|
-
|
|
1256
|
+
if (pathPrefix) return pathPrefix;
|
|
1257
|
+
for (const part of [...pathParts].reverse()) {
|
|
1128
1258
|
if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
|
|
1129
1259
|
return part;
|
|
1130
1260
|
}
|
|
@@ -1459,19 +1589,22 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1459
1589
|
const parentDir = path.basename(path.dirname(filePath));
|
|
1460
1590
|
const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
|
|
1461
1591
|
const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
|
|
1462
|
-
let
|
|
1592
|
+
let agentName;
|
|
1463
1593
|
if (parentDir === "sessions" && greatGrandParentDir === "agents") {
|
|
1464
|
-
|
|
1594
|
+
agentName = grandParentDir;
|
|
1465
1595
|
} else if (grandParentDir === "agents") {
|
|
1466
|
-
|
|
1467
|
-
} else if (parentDir === "runs" && grandParentDir === "cron") {
|
|
1468
|
-
agentId = "openclaw-cron";
|
|
1596
|
+
agentName = parentDir;
|
|
1469
1597
|
} else {
|
|
1470
|
-
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1598
|
+
agentName = parentDir;
|
|
1599
|
+
}
|
|
1600
|
+
let agentId = agentName;
|
|
1601
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1602
|
+
if (detection.pathPatterns) {
|
|
1603
|
+
for (const [pathSubstring, prefix] of Object.entries(detection.pathPatterns)) {
|
|
1604
|
+
if (filePath.includes(pathSubstring)) {
|
|
1605
|
+
agentId = `${prefix}-${agentName}`;
|
|
1606
|
+
break;
|
|
1607
|
+
}
|
|
1475
1608
|
}
|
|
1476
1609
|
}
|
|
1477
1610
|
const modelEvent = rawEvents.find((e) => e.type === "model_change");
|
|
@@ -1760,6 +1893,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1760
1893
|
edges: [],
|
|
1761
1894
|
events: [],
|
|
1762
1895
|
startTime,
|
|
1896
|
+
status,
|
|
1763
1897
|
agentId,
|
|
1764
1898
|
trigger,
|
|
1765
1899
|
name: rootName,
|
|
@@ -1880,8 +2014,12 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1880
2014
|
// Ignore git directories
|
|
1881
2015
|
/\.vscode/,
|
|
1882
2016
|
// Ignore vscode
|
|
1883
|
-
/\.idea
|
|
2017
|
+
/\.idea/,
|
|
1884
2018
|
// Ignore idea
|
|
2019
|
+
/\/archive\//,
|
|
2020
|
+
// Ignore archived trace files
|
|
2021
|
+
// Ignore user-configured skip directories
|
|
2022
|
+
...getSkipDirectories(this.userConfig).map((d) => new RegExp(`/${d}/`))
|
|
1885
2023
|
],
|
|
1886
2024
|
persistent: true,
|
|
1887
2025
|
ignoreInitial: true,
|
|
@@ -1935,29 +2073,42 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1935
2073
|
});
|
|
1936
2074
|
}
|
|
1937
2075
|
getTrace(filename) {
|
|
2076
|
+
const candidates = [];
|
|
1938
2077
|
const exact = this.traces.get(filename);
|
|
1939
|
-
if (exact)
|
|
2078
|
+
if (exact) candidates.push(exact);
|
|
1940
2079
|
if (filename.includes("::")) {
|
|
1941
2080
|
const [fname, startTimeStr] = filename.split("::");
|
|
1942
2081
|
const startTime = Number(startTimeStr);
|
|
1943
2082
|
if (fname && !Number.isNaN(startTime)) {
|
|
1944
2083
|
for (const trace of this.traces.values()) {
|
|
1945
2084
|
if (trace.filename === fname && trace.startTime === startTime) {
|
|
1946
|
-
|
|
2085
|
+
candidates.push(trace);
|
|
1947
2086
|
}
|
|
1948
2087
|
}
|
|
1949
2088
|
}
|
|
1950
2089
|
}
|
|
1951
2090
|
for (const prefix of ["openclaw:", "otel:", ""]) {
|
|
1952
2091
|
const prefixed = this.traces.get(prefix + filename);
|
|
1953
|
-
if (prefixed)
|
|
2092
|
+
if (prefixed) candidates.push(prefixed);
|
|
1954
2093
|
}
|
|
1955
2094
|
for (const [key, trace] of this.traces) {
|
|
1956
2095
|
if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
|
|
1957
|
-
|
|
2096
|
+
candidates.push(trace);
|
|
1958
2097
|
}
|
|
1959
2098
|
}
|
|
1960
|
-
return void 0;
|
|
2099
|
+
if (candidates.length === 0) return void 0;
|
|
2100
|
+
if (candidates.length === 1) return candidates[0];
|
|
2101
|
+
let best = candidates[0];
|
|
2102
|
+
let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
|
|
2103
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
2104
|
+
const c = candidates[i];
|
|
2105
|
+
const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
|
|
2106
|
+
if (nc > bestNodeCount) {
|
|
2107
|
+
best = c;
|
|
2108
|
+
bestNodeCount = nc;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
return best;
|
|
1961
2112
|
}
|
|
1962
2113
|
getTracesByAgent(agentId) {
|
|
1963
2114
|
return this.getAllTraces().filter((trace) => trace.agentId === agentId);
|
|
@@ -2004,7 +2155,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
2004
2155
|
var fs2 = __toESM(require("fs"), 1);
|
|
2005
2156
|
var os = __toESM(require("os"), 1);
|
|
2006
2157
|
var path2 = __toESM(require("path"), 1);
|
|
2007
|
-
var VERSION = "0.
|
|
2158
|
+
var VERSION = "0.8.0";
|
|
2008
2159
|
function getLanAddress() {
|
|
2009
2160
|
const interfaces = os.networkInterfaces();
|
|
2010
2161
|
for (const name of Object.keys(interfaces)) {
|
|
@@ -2016,7 +2167,7 @@ function getLanAddress() {
|
|
|
2016
2167
|
}
|
|
2017
2168
|
return null;
|
|
2018
2169
|
}
|
|
2019
|
-
function printBanner(config, traceCount, stats) {
|
|
2170
|
+
function printBanner(config, traceCount, stats, configPath) {
|
|
2020
2171
|
var _a;
|
|
2021
2172
|
const lan = getLanAddress();
|
|
2022
2173
|
const host = config.host || "localhost";
|
|
@@ -2032,26 +2183,25 @@ function printBanner(config, traceCount, stats) {
|
|
|
2032
2183
|
|
|
2033
2184
|
See your agents think.
|
|
2034
2185
|
|
|
2035
|
-
\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
|
|
2036
|
-
\u2502 \u{1F916} Agents \u2502 TRACE FILES \u2502 \u{1F4CA} AgentFlow \u2502 SHOWS YOU \u2502 \u{1F310} Your browser \u2502
|
|
2037
|
-
\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
|
|
2038
|
-
\u2502 write JSON \u2502 \u2502 builds graphs, \u2502 \u2502 graph, timeline, \u2502
|
|
2039
|
-
\u2502 trace files. \u2502 \u2502 serves dashboard.\u2502 \u2502 metrics, health. \u2502
|
|
2040
|
-
\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
|
|
2041
|
-
|
|
2042
|
-
Runs locally. Your data never leaves your machine.
|
|
2043
|
-
|
|
2044
|
-
Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
|
|
2045
|
-
|
|
2046
2186
|
Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? `
|
|
2047
2187
|
Data dirs: ${config.dataDirs.join("\n ")}` : ""}
|
|
2048
2188
|
Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
|
|
2049
2189
|
Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
|
|
2190
|
+
Config: ${configPath ?? "none (using defaults)"}
|
|
2050
2191
|
CORS: ${config.enableCors ? "enabled" : "disabled"}
|
|
2051
2192
|
WebSocket: live updates enabled
|
|
2193
|
+
Window: ${process.env.AGENTFLOW_TRACE_WINDOW_HOURS ?? "48"}h (set AGENTFLOW_TRACE_WINDOW_HOURS to change)
|
|
2052
2194
|
|
|
2053
2195
|
\u2192 http://localhost:${port}${isPublic && lan ? `
|
|
2054
2196
|
\u2192 http://${lan}:${port} (LAN)` : ""}
|
|
2197
|
+
|
|
2198
|
+
Pages: Agents \xB7 SOMA
|
|
2199
|
+
Agent: Profile \xB7 Execution Detail
|
|
2200
|
+
SOMA: Intelligence \xB7 Review \xB7 Policies \xB7 Knowledge \xB7 Activity
|
|
2201
|
+
Tabs: Flame Chart \xB7 Agent Flow \xB7 Metrics \xB7 Dependencies
|
|
2202
|
+
State Machine \xB7 Summary \xB7 Transcript
|
|
2203
|
+
|
|
2204
|
+
Runs locally. Your data never leaves your machine.
|
|
2055
2205
|
`);
|
|
2056
2206
|
}
|
|
2057
2207
|
async function startDashboard() {
|
|
@@ -2089,6 +2239,12 @@ async function startDashboard() {
|
|
|
2089
2239
|
case "--collector-token":
|
|
2090
2240
|
config.collectorAuthToken = args[++i];
|
|
2091
2241
|
break;
|
|
2242
|
+
case "--soma-vault":
|
|
2243
|
+
config.somaVault = args[++i];
|
|
2244
|
+
break;
|
|
2245
|
+
case "--config":
|
|
2246
|
+
config.configPath = args[++i];
|
|
2247
|
+
break;
|
|
2092
2248
|
case "--help":
|
|
2093
2249
|
printHelp();
|
|
2094
2250
|
process.exit(0);
|
|
@@ -2100,6 +2256,9 @@ async function startDashboard() {
|
|
|
2100
2256
|
if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
|
|
2101
2257
|
config.enableCollector = false;
|
|
2102
2258
|
}
|
|
2259
|
+
if (!config.somaVault && process.env.SOMA_VAULT) {
|
|
2260
|
+
config.somaVault = process.env.SOMA_VAULT;
|
|
2261
|
+
}
|
|
2103
2262
|
const tracesPath = path2.resolve(config.tracesDir);
|
|
2104
2263
|
if (!fs2.existsSync(tracesPath)) {
|
|
2105
2264
|
fs2.mkdirSync(tracesPath, { recursive: true });
|
|
@@ -2121,7 +2280,7 @@ async function startDashboard() {
|
|
|
2121
2280
|
setTimeout(() => {
|
|
2122
2281
|
const stats = dashboard.getStats();
|
|
2123
2282
|
const traces = dashboard.getTraces();
|
|
2124
|
-
printBanner(config, traces.length, stats);
|
|
2283
|
+
printBanner(config, traces.length, stats, dashboard.getConfigPath());
|
|
2125
2284
|
}, 1500);
|
|
2126
2285
|
} catch (error) {
|
|
2127
2286
|
console.error("\u274C Failed to start dashboard:", error);
|
|
@@ -2130,7 +2289,7 @@ async function startDashboard() {
|
|
|
2130
2289
|
}
|
|
2131
2290
|
function printHelp() {
|
|
2132
2291
|
console.log(`
|
|
2133
|
-
|
|
2292
|
+
AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
|
|
2134
2293
|
|
|
2135
2294
|
Usage:
|
|
2136
2295
|
agentflow-dashboard [options]
|
|
@@ -2141,22 +2300,34 @@ Options:
|
|
|
2141
2300
|
-t, --traces <path> Traces directory (default: ./traces)
|
|
2142
2301
|
-h, --host <address> Host address (default: localhost)
|
|
2143
2302
|
--data-dir <path> Extra data directory for process discovery (repeatable)
|
|
2303
|
+
--config <path> Path to agentflow.config.json (aliases, skip files, etc.)
|
|
2304
|
+
--soma-vault <path> SOMA vault directory for intelligence data
|
|
2144
2305
|
--cors Enable CORS headers
|
|
2145
2306
|
--no-collector Disable OTLP trace collector (POST /v1/traces)
|
|
2146
2307
|
--collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
|
|
2147
2308
|
--help Show this help message
|
|
2148
2309
|
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2310
|
+
Config file:
|
|
2311
|
+
The dashboard loads agentflow.config.json for agent aliases, skip files,
|
|
2312
|
+
discovery paths, and systemd services. Resolution order:
|
|
2313
|
+
1. --config flag
|
|
2314
|
+
2. AGENTFLOW_CONFIG env var
|
|
2315
|
+
3. ./agentflow.config.json
|
|
2316
|
+
4. ~/.config/agentflow/config.json
|
|
2317
|
+
|
|
2318
|
+
See agentflow.config.example.json for a complete reference.
|
|
2319
|
+
|
|
2320
|
+
Environment:
|
|
2321
|
+
AGENTFLOW_CONFIG Path to config file
|
|
2322
|
+
AGENTFLOW_TRACE_WINDOW_HOURS Max age of traces to load (default: 48)
|
|
2323
|
+
AGENTFLOW_COLLECTOR_TOKEN Auth token for OTLP collector
|
|
2324
|
+
AGENTFLOW_NO_COLLECTOR=true Disable OTLP collector
|
|
2325
|
+
SOMA_VAULT SOMA vault directory
|
|
2153
2326
|
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
\u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
|
|
2159
|
-
\u26A0\uFE0F Errors Failed and hung nodes with metadata
|
|
2327
|
+
Examples:
|
|
2328
|
+
agentflow-dashboard --traces ./traces --host 0.0.0.0
|
|
2329
|
+
agentflow-dashboard --traces ./traces --config ./agentflow.config.json
|
|
2330
|
+
agentflow-dashboard -p 8080 -t /var/log/agentflow --cors
|
|
2160
2331
|
`);
|
|
2161
2332
|
}
|
|
2162
2333
|
|
|
@@ -2179,12 +2350,15 @@ function serializeTrace(trace) {
|
|
|
2179
2350
|
var DashboardServer = class {
|
|
2180
2351
|
constructor(config) {
|
|
2181
2352
|
this.config = config;
|
|
2182
|
-
const
|
|
2183
|
-
|
|
2353
|
+
const { config: userCfg, configPath: cfgPath } = loadConfig(config.configPath);
|
|
2354
|
+
this.userConfig = userCfg;
|
|
2355
|
+
this.configPath = cfgPath;
|
|
2356
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
2357
|
+
const dashConfigPath = path3.join(home, ".agentflow/dashboard-config.json");
|
|
2184
2358
|
if (!config.dataDirs) config.dataDirs = [];
|
|
2185
2359
|
try {
|
|
2186
|
-
if (fs3.existsSync(
|
|
2187
|
-
const saved = JSON.parse(fs3.readFileSync(
|
|
2360
|
+
if (fs3.existsSync(dashConfigPath)) {
|
|
2361
|
+
const saved = JSON.parse(fs3.readFileSync(dashConfigPath, "utf-8"));
|
|
2188
2362
|
const extraDirs = saved.extraDirs ?? [];
|
|
2189
2363
|
for (const d of extraDirs) {
|
|
2190
2364
|
if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
|
|
@@ -2192,21 +2366,15 @@ var DashboardServer = class {
|
|
|
2192
2366
|
}
|
|
2193
2367
|
} catch {
|
|
2194
2368
|
}
|
|
2195
|
-
const
|
|
2196
|
-
path3.join(home, ".openclaw/cron/runs"),
|
|
2197
|
-
path3.join(home, ".openclaw/workspace/traces"),
|
|
2198
|
-
path3.join(home, ".openclaw/subagents"),
|
|
2199
|
-
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2200
|
-
path3.join(home, ".agentflow/traces")
|
|
2201
|
-
];
|
|
2202
|
-
for (const p of autoDiscoverPaths) {
|
|
2369
|
+
for (const p of getDiscoveryPaths(this.userConfig)) {
|
|
2203
2370
|
if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
|
|
2204
2371
|
config.dataDirs.push(p);
|
|
2205
2372
|
}
|
|
2206
2373
|
}
|
|
2207
2374
|
this.watcher = new TraceWatcher({
|
|
2208
2375
|
tracesDir: config.tracesDir,
|
|
2209
|
-
dataDirs: config.dataDirs
|
|
2376
|
+
dataDirs: config.dataDirs,
|
|
2377
|
+
userConfig: this.userConfig
|
|
2210
2378
|
});
|
|
2211
2379
|
this.stats = new AgentStats();
|
|
2212
2380
|
this.knowledgeStore = (0, import_agentflow_core3.createKnowledgeStore)({
|
|
@@ -2215,6 +2383,7 @@ var DashboardServer = class {
|
|
|
2215
2383
|
this.setupExpress();
|
|
2216
2384
|
this.setupWebSocket();
|
|
2217
2385
|
this.setupTraceWatcher();
|
|
2386
|
+
this.setupSomaReportWatcher();
|
|
2218
2387
|
let knowledgeCount = 0;
|
|
2219
2388
|
for (const trace of this.watcher.getAllTraces()) {
|
|
2220
2389
|
this.stats.processTrace(trace);
|
|
@@ -2241,6 +2410,8 @@ var DashboardServer = class {
|
|
|
2241
2410
|
ts: 0
|
|
2242
2411
|
};
|
|
2243
2412
|
knowledgeStore;
|
|
2413
|
+
userConfig;
|
|
2414
|
+
configPath;
|
|
2244
2415
|
setupExpress() {
|
|
2245
2416
|
if (this.config.enableCors) {
|
|
2246
2417
|
this.app.use((_req, res, next) => {
|
|
@@ -2252,18 +2423,35 @@ var DashboardServer = class {
|
|
|
2252
2423
|
next();
|
|
2253
2424
|
});
|
|
2254
2425
|
}
|
|
2255
|
-
const
|
|
2426
|
+
const pkgDir = path3.join(__dirname, "..");
|
|
2427
|
+
const clientDir = path3.join(pkgDir, "dist/client");
|
|
2428
|
+
const clientIndex = path3.join(clientDir, "index.html");
|
|
2429
|
+
const srcDir = path3.join(pkgDir, "src/client");
|
|
2430
|
+
const needsBuild = !fs3.existsSync(clientIndex) || fs3.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
|
|
2431
|
+
if (needsBuild) {
|
|
2432
|
+
try {
|
|
2433
|
+
console.log("Building dashboard client...");
|
|
2434
|
+
(0, import_node_child_process.execSync)("npm run build:client", { cwd: pkgDir, stdio: "inherit", timeout: 3e4 });
|
|
2435
|
+
} catch (err) {
|
|
2436
|
+
console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2256
2439
|
if (fs3.existsSync(clientDir)) {
|
|
2257
2440
|
this.app.use(import_express.default.static(clientDir));
|
|
2258
2441
|
}
|
|
2259
|
-
|
|
2260
|
-
if (fs3.existsSync(publicDir)) {
|
|
2261
|
-
this.app.use("/v1", import_express.default.static(publicDir));
|
|
2262
|
-
}
|
|
2263
|
-
this.app.get("/api/traces", (_req, res) => {
|
|
2442
|
+
this.app.get("/api/traces", (req, res) => {
|
|
2264
2443
|
try {
|
|
2265
|
-
const
|
|
2266
|
-
|
|
2444
|
+
const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
|
|
2445
|
+
const cursor = req.query.cursor ? parseFloat(req.query.cursor) : void 0;
|
|
2446
|
+
let allTraces = this.watcher.getAllTraces();
|
|
2447
|
+
if (cursor) {
|
|
2448
|
+
allTraces = allTraces.filter((t) => (t.lastModified || t.startTime) < cursor);
|
|
2449
|
+
}
|
|
2450
|
+
const page = allTraces.slice(0, limit);
|
|
2451
|
+
const serialized = page.map(serializeTrace);
|
|
2452
|
+
const lastTrace = page[page.length - 1];
|
|
2453
|
+
const nextCursor = page.length === limit && lastTrace ? lastTrace.lastModified || lastTrace.startTime : null;
|
|
2454
|
+
res.json({ traces: serialized, nextCursor });
|
|
2267
2455
|
} catch (_error) {
|
|
2268
2456
|
res.status(500).json({ error: "Failed to load traces" });
|
|
2269
2457
|
}
|
|
@@ -2554,6 +2742,235 @@ var DashboardServer = class {
|
|
|
2554
2742
|
res.status(500).json({ error: "Failed to load agent statistics" });
|
|
2555
2743
|
}
|
|
2556
2744
|
});
|
|
2745
|
+
this.app.get("/api/soma/tier", (_req, res) => {
|
|
2746
|
+
const somaVault = this.config.somaVault;
|
|
2747
|
+
if (!somaVault) {
|
|
2748
|
+
return res.json({ tier: "teaser", somaVault: false, governanceAvailable: false });
|
|
2749
|
+
}
|
|
2750
|
+
try {
|
|
2751
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2752
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2753
|
+
return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
|
|
2754
|
+
}
|
|
2755
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2756
|
+
const hasGovernance = report.governance && typeof report.governance.pending === "number";
|
|
2757
|
+
return res.json({
|
|
2758
|
+
tier: hasGovernance ? "pro" : "free",
|
|
2759
|
+
somaVault: true,
|
|
2760
|
+
governanceAvailable: !!hasGovernance
|
|
2761
|
+
});
|
|
2762
|
+
} catch {
|
|
2763
|
+
return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
|
|
2764
|
+
}
|
|
2765
|
+
});
|
|
2766
|
+
this.app.get("/api/soma/report", (_req, res) => {
|
|
2767
|
+
const somaVault = this.config.somaVault;
|
|
2768
|
+
if (!somaVault) {
|
|
2769
|
+
return res.json({ available: false, teaser: true });
|
|
2770
|
+
}
|
|
2771
|
+
try {
|
|
2772
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2773
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2774
|
+
return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
|
|
2775
|
+
}
|
|
2776
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2777
|
+
res.json(report);
|
|
2778
|
+
} catch (error) {
|
|
2779
|
+
console.error("Soma report error:", error);
|
|
2780
|
+
res.json({ available: false, teaser: false, message: "Failed to read report" });
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
this.app.get("/api/soma/governance", (_req, res) => {
|
|
2784
|
+
const somaVault = this.config.somaVault;
|
|
2785
|
+
if (!somaVault) {
|
|
2786
|
+
return res.json({ available: false });
|
|
2787
|
+
}
|
|
2788
|
+
try {
|
|
2789
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2790
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2791
|
+
return res.json({ available: false, message: "No report file. Run soma report." });
|
|
2792
|
+
}
|
|
2793
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2794
|
+
res.json({
|
|
2795
|
+
available: true,
|
|
2796
|
+
layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
|
|
2797
|
+
governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
|
|
2798
|
+
insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
|
|
2799
|
+
canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
|
|
2800
|
+
generatedAt: report.generatedAt
|
|
2801
|
+
});
|
|
2802
|
+
} catch (error) {
|
|
2803
|
+
console.error("Soma governance error:", error);
|
|
2804
|
+
res.status(500).json({ available: false, message: "Failed to read governance data" });
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
|
|
2808
|
+
const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
|
|
2809
|
+
this.app.post("/api/soma/governance/promote", (req, res) => {
|
|
2810
|
+
var _a;
|
|
2811
|
+
const somaVault = this.config.somaVault;
|
|
2812
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2813
|
+
const { entryId } = req.body ?? {};
|
|
2814
|
+
if (!entryId) return res.status(400).json({ error: "entryId required" });
|
|
2815
|
+
try {
|
|
2816
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2817
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2818
|
+
const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
|
|
2819
|
+
encoding: "utf-8",
|
|
2820
|
+
timeout: 1e4
|
|
2821
|
+
});
|
|
2822
|
+
res.json({ success: true, message: result.trim() });
|
|
2823
|
+
} catch (error) {
|
|
2824
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2825
|
+
}
|
|
2826
|
+
});
|
|
2827
|
+
this.app.post("/api/soma/governance/reject", (req, res) => {
|
|
2828
|
+
var _a;
|
|
2829
|
+
const somaVault = this.config.somaVault;
|
|
2830
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2831
|
+
const { entryId, reason } = req.body ?? {};
|
|
2832
|
+
if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
|
|
2833
|
+
try {
|
|
2834
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2835
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2836
|
+
const safeReason = sanitizeReason(String(reason));
|
|
2837
|
+
const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
|
|
2838
|
+
encoding: "utf-8",
|
|
2839
|
+
timeout: 1e4
|
|
2840
|
+
});
|
|
2841
|
+
res.json({ success: true, message: result.trim() });
|
|
2842
|
+
} catch (error) {
|
|
2843
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2844
|
+
}
|
|
2845
|
+
});
|
|
2846
|
+
this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
|
|
2847
|
+
var _a;
|
|
2848
|
+
const somaVault = this.config.somaVault;
|
|
2849
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2850
|
+
try {
|
|
2851
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2852
|
+
const safeId = sanitizeArg(String(req.params.id));
|
|
2853
|
+
const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
|
|
2854
|
+
encoding: "utf-8",
|
|
2855
|
+
timeout: 1e4
|
|
2856
|
+
});
|
|
2857
|
+
res.json({ available: true, output: result.trim() });
|
|
2858
|
+
} catch (error) {
|
|
2859
|
+
res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2860
|
+
}
|
|
2861
|
+
});
|
|
2862
|
+
this.app.get("/api/soma/policies", (_req, res) => {
|
|
2863
|
+
const somaVault = this.config.somaVault;
|
|
2864
|
+
if (!somaVault) return res.json({ policies: [] });
|
|
2865
|
+
try {
|
|
2866
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2867
|
+
if (!fs3.existsSync(reportPath)) return res.json({ policies: [] });
|
|
2868
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2869
|
+
res.json({ policies: report.policies ?? [] });
|
|
2870
|
+
} catch {
|
|
2871
|
+
res.json({ policies: [] });
|
|
2872
|
+
}
|
|
2873
|
+
});
|
|
2874
|
+
this.app.post("/api/soma/policies", import_express.default.json(), (req, res) => {
|
|
2875
|
+
var _a;
|
|
2876
|
+
const somaVault = this.config.somaVault;
|
|
2877
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2878
|
+
const { name, enforcement, scope, conditions } = req.body ?? {};
|
|
2879
|
+
if (!name) return res.status(400).json({ error: "name required" });
|
|
2880
|
+
try {
|
|
2881
|
+
const safeName = sanitizeArg(String(name));
|
|
2882
|
+
const safeEnf = sanitizeArg(String(enforcement || "warn"));
|
|
2883
|
+
const safeScope = sanitizeReason(String(scope || "all"));
|
|
2884
|
+
const safeCond = sanitizeReason(String(conditions || ""));
|
|
2885
|
+
const result = (0, import_node_child_process.execSync)(
|
|
2886
|
+
`npx soma policy create "${safeName}" --enforcement ${safeEnf} --scope "${safeScope}" --conditions "${safeCond}" --vault "${somaVault}"`,
|
|
2887
|
+
{ encoding: "utf-8", timeout: 1e4 }
|
|
2888
|
+
);
|
|
2889
|
+
res.json({ success: true, message: result.trim() });
|
|
2890
|
+
} catch (error) {
|
|
2891
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2892
|
+
}
|
|
2893
|
+
});
|
|
2894
|
+
this.app.delete("/api/soma/policies/:name", (req, res) => {
|
|
2895
|
+
var _a;
|
|
2896
|
+
const somaVault = this.config.somaVault;
|
|
2897
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2898
|
+
try {
|
|
2899
|
+
const safeName = sanitizeArg(String(req.params.name));
|
|
2900
|
+
const result = (0, import_node_child_process.execSync)(
|
|
2901
|
+
`npx soma policy delete "${safeName}" --vault "${somaVault}"`,
|
|
2902
|
+
{ encoding: "utf-8", timeout: 1e4 }
|
|
2903
|
+
);
|
|
2904
|
+
res.json({ success: true, message: result.trim() });
|
|
2905
|
+
} catch (error) {
|
|
2906
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2907
|
+
}
|
|
2908
|
+
});
|
|
2909
|
+
this.app.get("/api/soma/vault/entities", (req, res) => {
|
|
2910
|
+
const somaVault = this.config.somaVault;
|
|
2911
|
+
if (!somaVault) return res.json({ entities: [], total: 0 });
|
|
2912
|
+
try {
|
|
2913
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2914
|
+
if (!fs3.existsSync(reportPath)) return res.json({ entities: [], total: 0 });
|
|
2915
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2916
|
+
let entities = [
|
|
2917
|
+
...(report.agents ?? []).map((a) => ({ ...a, type: "agent", id: a.name })),
|
|
2918
|
+
...(report.insights ?? []).map((i, idx) => {
|
|
2919
|
+
var _a;
|
|
2920
|
+
return { ...i, type: i.type || "insight", id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}` };
|
|
2921
|
+
}),
|
|
2922
|
+
...(report.policies ?? []).map((p) => ({ ...p, type: "policy", id: p.name }))
|
|
2923
|
+
];
|
|
2924
|
+
const { type, layer, q, limit: limitStr, offset: offsetStr } = req.query;
|
|
2925
|
+
if (type) entities = entities.filter((e) => e.type === type);
|
|
2926
|
+
if (layer) entities = entities.filter((e) => e.layer === layer);
|
|
2927
|
+
if (q) {
|
|
2928
|
+
const lq = q.toLowerCase();
|
|
2929
|
+
entities = entities.filter((e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq));
|
|
2930
|
+
}
|
|
2931
|
+
const total = entities.length;
|
|
2932
|
+
const offset = parseInt(offsetStr || "0", 10);
|
|
2933
|
+
const limit = Math.min(parseInt(limitStr || "50", 10), 200);
|
|
2934
|
+
entities = entities.slice(offset, offset + limit);
|
|
2935
|
+
res.json({ entities, total });
|
|
2936
|
+
} catch (error) {
|
|
2937
|
+
console.error("Vault entities error:", error);
|
|
2938
|
+
res.json({ entities: [], total: 0 });
|
|
2939
|
+
}
|
|
2940
|
+
});
|
|
2941
|
+
this.app.get("/api/soma/vault/entities/:type/:id", (req, res) => {
|
|
2942
|
+
const somaVault = this.config.somaVault;
|
|
2943
|
+
if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
|
|
2944
|
+
try {
|
|
2945
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2946
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2947
|
+
const { type, id } = req.params;
|
|
2948
|
+
let entity = null;
|
|
2949
|
+
if (type === "agent") {
|
|
2950
|
+
entity = (report.agents ?? []).find((a) => a.name === id);
|
|
2951
|
+
} else if (type === "policy") {
|
|
2952
|
+
entity = (report.policies ?? []).find((p) => p.name === id);
|
|
2953
|
+
} else {
|
|
2954
|
+
entity = (report.insights ?? []).find(
|
|
2955
|
+
(i) => {
|
|
2956
|
+
var _a;
|
|
2957
|
+
return (((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || "") === id || i.title === id;
|
|
2958
|
+
}
|
|
2959
|
+
);
|
|
2960
|
+
}
|
|
2961
|
+
if (!entity) return res.status(404).json({ error: "Entity not found" });
|
|
2962
|
+
res.json({
|
|
2963
|
+
...entity,
|
|
2964
|
+
type,
|
|
2965
|
+
id,
|
|
2966
|
+
body: entity.claim || entity.conditions || "",
|
|
2967
|
+
tags: entity.tags ?? [],
|
|
2968
|
+
related: entity.related ?? []
|
|
2969
|
+
});
|
|
2970
|
+
} catch {
|
|
2971
|
+
res.status(404).json({ error: "Entity not found" });
|
|
2972
|
+
}
|
|
2973
|
+
});
|
|
2557
2974
|
this.app.get("/api/process-health", (_req, res) => {
|
|
2558
2975
|
var _a, _b;
|
|
2559
2976
|
try {
|
|
@@ -2566,7 +2983,14 @@ var DashboardServer = class {
|
|
|
2566
2983
|
path3.dirname(this.config.tracesDir),
|
|
2567
2984
|
...this.config.dataDirs || []
|
|
2568
2985
|
];
|
|
2569
|
-
|
|
2986
|
+
let configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
|
|
2987
|
+
const pref = getProcessPreference(this.userConfig);
|
|
2988
|
+
if (pref) {
|
|
2989
|
+
const hasPreferred = configs.some((c) => c.processName === pref.prefer);
|
|
2990
|
+
if (hasPreferred) {
|
|
2991
|
+
configs = configs.filter((c) => c.processName !== pref.over);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2570
2994
|
if (configs.length === 0) {
|
|
2571
2995
|
return res.json(null);
|
|
2572
2996
|
}
|
|
@@ -2650,35 +3074,32 @@ var DashboardServer = class {
|
|
|
2650
3074
|
}
|
|
2651
3075
|
} catch {
|
|
2652
3076
|
}
|
|
2653
|
-
const watched = [
|
|
3077
|
+
const watched = [...new Set([
|
|
2654
3078
|
this.config.tracesDir,
|
|
2655
3079
|
...this.config.dataDirs || [],
|
|
2656
3080
|
...extraDirs
|
|
2657
|
-
];
|
|
3081
|
+
].map((w) => path3.resolve(w)))];
|
|
2658
3082
|
const discovered = [];
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
const
|
|
2669
|
-
if (
|
|
3083
|
+
const svcNames = getSystemdServices(this.userConfig);
|
|
3084
|
+
if (svcNames.length > 0) {
|
|
3085
|
+
try {
|
|
3086
|
+
const { execSync: execSync2 } = require("child_process");
|
|
3087
|
+
const raw = execSync2(
|
|
3088
|
+
`systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
|
|
3089
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
3090
|
+
);
|
|
3091
|
+
for (const line of raw.split("\n")) {
|
|
3092
|
+
const match = line.match(/path=([^\s;]+)/);
|
|
3093
|
+
if (match == null ? void 0 : match[1]) {
|
|
3094
|
+
const dir = path3.dirname(match[1]);
|
|
3095
|
+
if (fs3.existsSync(dir)) discovered.push(dir);
|
|
3096
|
+
}
|
|
2670
3097
|
}
|
|
3098
|
+
} catch {
|
|
2671
3099
|
}
|
|
2672
|
-
} catch {
|
|
2673
3100
|
}
|
|
2674
3101
|
const commonPaths = [
|
|
2675
|
-
|
|
2676
|
-
path3.join(home, ".alfred/data"),
|
|
2677
|
-
path3.join(home, ".openclaw/workspace/traces"),
|
|
2678
|
-
path3.join(home, ".openclaw/subagents"),
|
|
2679
|
-
path3.join(home, ".openclaw/cron/runs"),
|
|
2680
|
-
path3.join(home, ".openclaw/cron"),
|
|
2681
|
-
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
3102
|
+
...getDiscoveryPaths(this.userConfig),
|
|
2682
3103
|
path3.join(home, ".agentflow/traces")
|
|
2683
3104
|
];
|
|
2684
3105
|
for (const p of commonPaths) {
|
|
@@ -2782,18 +3203,10 @@ var DashboardServer = class {
|
|
|
2782
3203
|
this.app.get("/ready", (_req, res) => {
|
|
2783
3204
|
res.json({ status: "ready" });
|
|
2784
3205
|
});
|
|
2785
|
-
this.app.get("/v1/*", (_req, res) => {
|
|
2786
|
-
const legacyIndex = path3.join(__dirname, "../public/index.html");
|
|
2787
|
-
if (fs3.existsSync(legacyIndex)) {
|
|
2788
|
-
res.sendFile(legacyIndex);
|
|
2789
|
-
} else {
|
|
2790
|
-
res.status(404).send("Legacy dashboard not found");
|
|
2791
|
-
}
|
|
2792
|
-
});
|
|
2793
3206
|
this.app.get("*", (_req, res) => {
|
|
2794
|
-
const
|
|
2795
|
-
if (fs3.existsSync(
|
|
2796
|
-
res.sendFile(
|
|
3207
|
+
const clientIndex2 = path3.join(__dirname, "../dist/client/index.html");
|
|
3208
|
+
if (fs3.existsSync(clientIndex2)) {
|
|
3209
|
+
res.sendFile(clientIndex2);
|
|
2797
3210
|
} else {
|
|
2798
3211
|
res.status(404).send("Dashboard not found - public files may not be built");
|
|
2799
3212
|
}
|
|
@@ -2819,6 +3232,41 @@ var DashboardServer = class {
|
|
|
2819
3232
|
});
|
|
2820
3233
|
});
|
|
2821
3234
|
}
|
|
3235
|
+
/** Watch soma-report.json for changes and broadcast updates via WebSocket. */
|
|
3236
|
+
setupSomaReportWatcher() {
|
|
3237
|
+
const somaVault = this.config.somaVault;
|
|
3238
|
+
if (!somaVault) return;
|
|
3239
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
3240
|
+
const reportDir = path3.dirname(reportPath);
|
|
3241
|
+
if (!fs3.existsSync(reportDir)) return;
|
|
3242
|
+
let debounceTimer = null;
|
|
3243
|
+
const watcher = import_chokidar2.default.watch(reportPath, {
|
|
3244
|
+
ignoreInitial: true,
|
|
3245
|
+
persistent: true,
|
|
3246
|
+
awaitWriteFinish: { stabilityThreshold: 500 }
|
|
3247
|
+
});
|
|
3248
|
+
watcher.on("change", () => {
|
|
3249
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3250
|
+
debounceTimer = setTimeout(() => {
|
|
3251
|
+
var _a, _b;
|
|
3252
|
+
try {
|
|
3253
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
3254
|
+
this.broadcast({ type: "soma-report-updated", data: report });
|
|
3255
|
+
if (report.generatedAt) {
|
|
3256
|
+
this.broadcast({
|
|
3257
|
+
type: "soma-activity",
|
|
3258
|
+
data: {
|
|
3259
|
+
action: "report-updated",
|
|
3260
|
+
description: `Report updated: ${((_a = report.totals) == null ? void 0 : _a.agents) ?? 0} agents, ${((_b = report.totals) == null ? void 0 : _b.insights) ?? 0} insights`,
|
|
3261
|
+
timestamp: report.generatedAt
|
|
3262
|
+
}
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
} catch {
|
|
3266
|
+
}
|
|
3267
|
+
}, 500);
|
|
3268
|
+
});
|
|
3269
|
+
}
|
|
2822
3270
|
/**
|
|
2823
3271
|
* Filter an agent's traces to valid ExecutionGraphs and convert via loadGraph().
|
|
2824
3272
|
* Returns only traces with proper nodes (Map or non-empty object), skipping session-only traces.
|
|
@@ -3040,24 +3488,49 @@ var DashboardServer = class {
|
|
|
3040
3488
|
});
|
|
3041
3489
|
}
|
|
3042
3490
|
async start() {
|
|
3043
|
-
return new Promise((
|
|
3491
|
+
return new Promise((resolve5) => {
|
|
3044
3492
|
const host = this.config.host || "localhost";
|
|
3045
3493
|
this.server.listen(this.config.port, host, () => {
|
|
3046
3494
|
console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
|
|
3047
3495
|
console.log(`Watching traces in: ${this.config.tracesDir}`);
|
|
3048
|
-
|
|
3496
|
+
resolve5();
|
|
3049
3497
|
});
|
|
3050
3498
|
});
|
|
3051
3499
|
}
|
|
3500
|
+
/** Check if any src/client file is newer than the built bundle. */
|
|
3501
|
+
isClientStale(srcDir, distDir) {
|
|
3502
|
+
try {
|
|
3503
|
+
const distIndex = path3.join(distDir, "index.html");
|
|
3504
|
+
if (!fs3.existsSync(distIndex)) return true;
|
|
3505
|
+
const distMtime = fs3.statSync(distIndex).mtimeMs;
|
|
3506
|
+
const check = (dir) => {
|
|
3507
|
+
for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
|
|
3508
|
+
const full = path3.join(dir, entry.name);
|
|
3509
|
+
if (entry.isDirectory()) {
|
|
3510
|
+
if (check(full)) return true;
|
|
3511
|
+
} else if (fs3.statSync(full).mtimeMs > distMtime) {
|
|
3512
|
+
return true;
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
return false;
|
|
3516
|
+
};
|
|
3517
|
+
return check(srcDir);
|
|
3518
|
+
} catch {
|
|
3519
|
+
return false;
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3052
3522
|
async stop() {
|
|
3053
|
-
return new Promise((
|
|
3523
|
+
return new Promise((resolve5) => {
|
|
3054
3524
|
this.watcher.stop();
|
|
3055
3525
|
this.server.close(() => {
|
|
3056
3526
|
console.log("Dashboard server stopped");
|
|
3057
|
-
|
|
3527
|
+
resolve5();
|
|
3058
3528
|
});
|
|
3059
3529
|
});
|
|
3060
3530
|
}
|
|
3531
|
+
getConfigPath() {
|
|
3532
|
+
return this.configPath;
|
|
3533
|
+
}
|
|
3061
3534
|
getStats() {
|
|
3062
3535
|
return this.stats.getGlobalStats();
|
|
3063
3536
|
}
|