agentflow-dashboard 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/{chunk-GA2Y6E62.js → chunk-NZFXRZYU.js} +529 -206
- package/dist/cli.cjs +533 -210
- package/dist/cli.js +1 -1
- package/dist/client/assets/{index-Ds_npIxI.css → index-CNZqCErb.css} +1 -1
- package/dist/client/assets/index-Cb5C1Pah.js +50 -0
- package/dist/client/index.html +2 -2
- package/dist/index.cjs +533 -210
- package/dist/index.js +1 -1
- package/dist/server.cjs +533 -210
- package/dist/server.js +1 -1
- package/package.json +3 -5
- package/dist/client/assets/index-BQUye2Lc.js +0 -50
- package/dist/client/dashboard.js +0 -3113
- package/dist/public/dashboard.js +0 -3113
- package/dist/public/index.html +0 -1385
- package/public/dashboard.js +0 -3113
- package/public/index.html +0 -1385
package/dist/index.cjs
CHANGED
|
@@ -37,10 +37,84 @@ __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");
|
|
45
119
|
var import_express = __toESM(require("express"), 1);
|
|
46
120
|
var import_ws = require("ws");
|
|
@@ -74,17 +148,17 @@ var AgentFlowAdapter = class {
|
|
|
74
148
|
};
|
|
75
149
|
|
|
76
150
|
// src/adapters/openclaw.ts
|
|
77
|
-
var
|
|
78
|
-
var
|
|
151
|
+
var import_node_fs2 = require("fs");
|
|
152
|
+
var import_node_path2 = require("path");
|
|
79
153
|
var jobCache = /* @__PURE__ */ new Map();
|
|
80
154
|
function loadJobs(openclawDir) {
|
|
81
155
|
const cached = jobCache.get(openclawDir);
|
|
82
156
|
if (cached) return cached;
|
|
83
|
-
const jobsPath = (0,
|
|
157
|
+
const jobsPath = (0, import_node_path2.join)(openclawDir, "cron", "jobs.json");
|
|
84
158
|
const map = /* @__PURE__ */ new Map();
|
|
85
159
|
try {
|
|
86
|
-
if ((0,
|
|
87
|
-
const data = JSON.parse((0,
|
|
160
|
+
if ((0, import_node_fs2.existsSync)(jobsPath)) {
|
|
161
|
+
const data = JSON.parse((0, import_node_fs2.readFileSync)(jobsPath, "utf-8"));
|
|
88
162
|
const jobs = Array.isArray(data) ? data : data.jobs ?? [];
|
|
89
163
|
for (const job of jobs) {
|
|
90
164
|
if (job.id) map.set(job.id, job);
|
|
@@ -96,19 +170,19 @@ function loadJobs(openclawDir) {
|
|
|
96
170
|
return map;
|
|
97
171
|
}
|
|
98
172
|
function findOpenClawRoot(filePath) {
|
|
99
|
-
let dir = (0,
|
|
173
|
+
let dir = (0, import_node_path2.dirname)(filePath);
|
|
100
174
|
for (let i = 0; i < 5; i++) {
|
|
101
|
-
if ((0,
|
|
175
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, "cron", "jobs.json")) || (0, import_node_path2.basename)(dir) === ".openclaw") {
|
|
102
176
|
return dir;
|
|
103
177
|
}
|
|
104
|
-
dir = (0,
|
|
178
|
+
dir = (0, import_node_path2.dirname)(dir);
|
|
105
179
|
}
|
|
106
180
|
return null;
|
|
107
181
|
}
|
|
108
182
|
var OpenClawAdapter = class {
|
|
109
183
|
name = "openclaw";
|
|
110
184
|
detect(dirPath) {
|
|
111
|
-
return (0,
|
|
185
|
+
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
186
|
}
|
|
113
187
|
canHandle(filePath) {
|
|
114
188
|
if (!filePath.endsWith(".jsonl")) return false;
|
|
@@ -117,7 +191,7 @@ var OpenClawAdapter = class {
|
|
|
117
191
|
parse(filePath) {
|
|
118
192
|
const traces = [];
|
|
119
193
|
try {
|
|
120
|
-
const content = (0,
|
|
194
|
+
const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
121
195
|
const root = findOpenClawRoot(filePath);
|
|
122
196
|
const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
|
|
123
197
|
for (const line of content.split("\n")) {
|
|
@@ -129,7 +203,7 @@ var OpenClawAdapter = class {
|
|
|
129
203
|
continue;
|
|
130
204
|
}
|
|
131
205
|
if (entry.action !== "finished") continue;
|
|
132
|
-
const jobId = entry.jobId ?? (0,
|
|
206
|
+
const jobId = entry.jobId ?? (0, import_node_path2.basename)(filePath, ".jsonl");
|
|
133
207
|
const job = jobs.get(jobId);
|
|
134
208
|
const jobName = (job == null ? void 0 : job.name) ?? jobId;
|
|
135
209
|
const startTime = entry.runAtMs ?? entry.ts;
|
|
@@ -181,8 +255,8 @@ var OpenClawAdapter = class {
|
|
|
181
255
|
};
|
|
182
256
|
|
|
183
257
|
// src/adapters/otel.ts
|
|
184
|
-
var
|
|
185
|
-
var
|
|
258
|
+
var import_node_fs3 = require("fs");
|
|
259
|
+
var import_node_path3 = require("path");
|
|
186
260
|
var SPAN_TYPE_MAP = {
|
|
187
261
|
"gen_ai.chat": "llm",
|
|
188
262
|
"gen_ai.completion": "llm",
|
|
@@ -289,8 +363,8 @@ var OTelAdapter = class {
|
|
|
289
363
|
name = "otel";
|
|
290
364
|
detect(dirPath) {
|
|
291
365
|
try {
|
|
292
|
-
if ((0,
|
|
293
|
-
const files = (0,
|
|
366
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(dirPath, "otel-traces"))) return true;
|
|
367
|
+
const files = (0, import_node_fs3.readdirSync)(dirPath);
|
|
294
368
|
return files.some((f) => f.endsWith(".otlp.json"));
|
|
295
369
|
} catch {
|
|
296
370
|
return false;
|
|
@@ -301,7 +375,7 @@ var OTelAdapter = class {
|
|
|
301
375
|
}
|
|
302
376
|
parse(filePath) {
|
|
303
377
|
try {
|
|
304
|
-
const content = (0,
|
|
378
|
+
const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
|
|
305
379
|
const payload = JSON.parse(content);
|
|
306
380
|
const traces = parseOtlpPayload(payload);
|
|
307
381
|
for (const t of traces) t.filePath = filePath;
|
|
@@ -343,9 +417,7 @@ function extractSource(agentId) {
|
|
|
343
417
|
const colonIdx = agentId.indexOf(":");
|
|
344
418
|
if (colonIdx > 0 && colonIdx < 20) {
|
|
345
419
|
const prefix = agentId.slice(0, colonIdx);
|
|
346
|
-
|
|
347
|
-
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
348
|
-
}
|
|
420
|
+
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
349
421
|
}
|
|
350
422
|
return { source: "agentflow", localId: agentId };
|
|
351
423
|
}
|
|
@@ -376,16 +448,20 @@ function deduplicateAgents(agents) {
|
|
|
376
448
|
for (const a of tagged) {
|
|
377
449
|
const suffix = extractSuffix(a.localId);
|
|
378
450
|
if (!suffix) continue;
|
|
379
|
-
const
|
|
451
|
+
const key = `${a.source}:${suffix}`;
|
|
452
|
+
const group = suffixGroups.get(key) ?? [];
|
|
380
453
|
group.push(a);
|
|
381
|
-
suffixGroups.set(
|
|
454
|
+
suffixGroups.set(key, group);
|
|
382
455
|
}
|
|
383
456
|
const mergedIds = /* @__PURE__ */ new Set();
|
|
384
457
|
const mergedAgents = [];
|
|
385
|
-
for (const [
|
|
458
|
+
for (const [_key, group] of suffixGroups) {
|
|
459
|
+
const suffix = extractSuffix(group[0].localId);
|
|
386
460
|
if (group.length < 2) continue;
|
|
387
461
|
const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
|
|
388
462
|
if (prefixes.size < 2) continue;
|
|
463
|
+
const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
|
|
464
|
+
if (longPrefixes.length >= 2) continue;
|
|
389
465
|
const merged = {
|
|
390
466
|
agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
|
|
391
467
|
displayName: suffix,
|
|
@@ -433,10 +509,7 @@ function groupAgents(agents) {
|
|
|
433
509
|
}
|
|
434
510
|
const SOURCE_DISPLAY = {
|
|
435
511
|
agentflow: "AgentFlow",
|
|
436
|
-
|
|
437
|
-
otel: "OpenTelemetry",
|
|
438
|
-
langchain: "LangChain",
|
|
439
|
-
crewai: "CrewAI"
|
|
512
|
+
otel: "OpenTelemetry"
|
|
440
513
|
};
|
|
441
514
|
const groups = [];
|
|
442
515
|
for (const [source, sourceAgents] of sourceMap) {
|
|
@@ -782,10 +855,6 @@ function getUniversalNodeStatus(activity) {
|
|
|
782
855
|
return "completed";
|
|
783
856
|
}
|
|
784
857
|
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
858
|
const firstSegment = sessionId.split("-")[0];
|
|
790
859
|
if (firstSegment) return firstSegment;
|
|
791
860
|
return "openclaw";
|
|
@@ -798,19 +867,81 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
798
867
|
tracesDir;
|
|
799
868
|
dataDirs;
|
|
800
869
|
allWatchDirs;
|
|
870
|
+
maxAgeMs;
|
|
871
|
+
userConfig;
|
|
801
872
|
constructor(tracesDirOrOptions) {
|
|
802
873
|
super();
|
|
874
|
+
const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
|
|
875
|
+
const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
|
|
876
|
+
const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
|
|
803
877
|
if (typeof tracesDirOrOptions === "string") {
|
|
804
878
|
this.tracesDir = path.resolve(tracesDirOrOptions);
|
|
805
879
|
this.dataDirs = [];
|
|
880
|
+
this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
|
|
881
|
+
this.userConfig = {};
|
|
806
882
|
} else {
|
|
807
883
|
this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
|
|
808
884
|
this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
|
|
809
|
-
|
|
885
|
+
this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
|
|
886
|
+
this.userConfig = tracesDirOrOptions.userConfig ?? {};
|
|
887
|
+
}
|
|
888
|
+
this.skipFiles = /* @__PURE__ */ new Set([
|
|
889
|
+
..._TraceWatcher.STRUCTURAL_SKIP_FILES,
|
|
890
|
+
...getSkipFiles(this.userConfig)
|
|
891
|
+
]);
|
|
892
|
+
this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
|
|
810
893
|
this.allWatchDirs = [this.tracesDir, ...this.dataDirs];
|
|
811
894
|
this.ensureTracesDir();
|
|
812
895
|
this.loadExistingFiles();
|
|
896
|
+
this.archiveOldTraces();
|
|
813
897
|
this.startWatching();
|
|
898
|
+
setInterval(() => this.archiveOldTraces(), 6 * 60 * 60 * 1e3);
|
|
899
|
+
}
|
|
900
|
+
/** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
|
|
901
|
+
archiveOldTraces() {
|
|
902
|
+
const cutoff = Date.now() - this.maxAgeMs;
|
|
903
|
+
let archived = 0;
|
|
904
|
+
for (const dir of this.allWatchDirs) {
|
|
905
|
+
if (!fs.existsSync(dir)) continue;
|
|
906
|
+
try {
|
|
907
|
+
this.archiveDirectory(dir, cutoff, 0);
|
|
908
|
+
} catch (error) {
|
|
909
|
+
console.warn(`Archival error in ${dir}:`, error.message);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
archiveDirectory(dir, cutoff, depth) {
|
|
914
|
+
if (depth > 10) return 0;
|
|
915
|
+
if (path.basename(dir) === "archive") return 0;
|
|
916
|
+
let archived = 0;
|
|
917
|
+
try {
|
|
918
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
919
|
+
for (const entry of entries) {
|
|
920
|
+
if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
|
|
921
|
+
const fullPath = path.join(dir, entry.name);
|
|
922
|
+
if (entry.isDirectory()) {
|
|
923
|
+
archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
|
|
927
|
+
try {
|
|
928
|
+
const stats = fs.statSync(fullPath);
|
|
929
|
+
if (stats.mtimeMs >= cutoff) continue;
|
|
930
|
+
const mtime = new Date(stats.mtimeMs);
|
|
931
|
+
const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
|
|
932
|
+
const archiveDir = path.join(this.tracesDir, "archive", yearMonth);
|
|
933
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
934
|
+
const dest = path.join(archiveDir, entry.name);
|
|
935
|
+
fs.renameSync(fullPath, dest);
|
|
936
|
+
const key = this.traceKey(fullPath);
|
|
937
|
+
this.traces.delete(key);
|
|
938
|
+
archived++;
|
|
939
|
+
} catch {
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
} catch {
|
|
943
|
+
}
|
|
944
|
+
return archived;
|
|
814
945
|
}
|
|
815
946
|
ensureTracesDir() {
|
|
816
947
|
if (!fs.existsSync(this.tracesDir)) {
|
|
@@ -843,9 +974,17 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
843
974
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
844
975
|
for (const entry of entries) {
|
|
845
976
|
if (entry.name.startsWith(".")) continue;
|
|
977
|
+
if (entry.name === "archive") continue;
|
|
978
|
+
if (this.userSkipDirs.has(entry.name)) continue;
|
|
846
979
|
const fullPath = path.join(dir, entry.name);
|
|
847
980
|
if (entry.isFile()) {
|
|
848
981
|
if (this.isSupportedFile(entry.name)) {
|
|
982
|
+
try {
|
|
983
|
+
const mtime = fs.statSync(fullPath).mtimeMs;
|
|
984
|
+
if (Date.now() - mtime > this.maxAgeMs) continue;
|
|
985
|
+
} catch {
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
849
988
|
if (this.loadFile(fullPath)) {
|
|
850
989
|
fileCount++;
|
|
851
990
|
}
|
|
@@ -863,8 +1002,8 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
863
1002
|
isSupportedFile(filename) {
|
|
864
1003
|
return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
|
|
865
1004
|
}
|
|
866
|
-
/**
|
|
867
|
-
static
|
|
1005
|
+
/** Structural file names that are never trace data — always skipped. */
|
|
1006
|
+
static STRUCTURAL_SKIP_FILES = /* @__PURE__ */ new Set([
|
|
868
1007
|
"workers.json",
|
|
869
1008
|
"package.json",
|
|
870
1009
|
"package-lock.json",
|
|
@@ -879,6 +1018,10 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
879
1018
|
"update-check.json",
|
|
880
1019
|
"exec-approvals.json"
|
|
881
1020
|
]);
|
|
1021
|
+
/** Skip files = structural + user config */
|
|
1022
|
+
skipFiles;
|
|
1023
|
+
/** Skip directories from user config */
|
|
1024
|
+
userSkipDirs;
|
|
882
1025
|
static SKIP_SUFFIXES = [
|
|
883
1026
|
"-state.json",
|
|
884
1027
|
"-config.json",
|
|
@@ -890,7 +1033,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
890
1033
|
/** Load a file using the adapter registry, falling back to built-in parsing. */
|
|
891
1034
|
loadFile(filePath) {
|
|
892
1035
|
const filename = path.basename(filePath);
|
|
893
|
-
if (
|
|
1036
|
+
if (this.skipFiles.has(filename)) return false;
|
|
894
1037
|
if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
|
|
895
1038
|
const adapter = findAdapter(filePath);
|
|
896
1039
|
if (adapter && adapter.name !== "agentflow") {
|
|
@@ -1067,43 +1210,26 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1067
1210
|
}
|
|
1068
1211
|
return traces;
|
|
1069
1212
|
}
|
|
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
|
-
};
|
|
1213
|
+
/** Normalise agent identifiers using config-driven alias map. */
|
|
1092
1214
|
normaliseAgentId(raw) {
|
|
1093
|
-
|
|
1215
|
+
const aliases = getAliases(this.userConfig);
|
|
1216
|
+
return aliases[raw] ?? raw;
|
|
1094
1217
|
}
|
|
1095
1218
|
detectAgentIdentifier(activity, _filename, filePath) {
|
|
1096
1219
|
if (activity.agent_id) {
|
|
1097
|
-
|
|
1098
|
-
if (agentId === "main" && filePath.includes(".alfred/")) return this.normaliseAgentId("alfred-main");
|
|
1099
|
-
return this.normaliseAgentId(agentId);
|
|
1220
|
+
return this.normaliseAgentId(activity.agent_id);
|
|
1100
1221
|
}
|
|
1101
1222
|
const pathAgent = this.extractAgentFromPath(filePath);
|
|
1102
|
-
|
|
1223
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1224
|
+
if (detection.filePatterns) {
|
|
1103
1225
|
const basename3 = path.basename(filePath, path.extname(filePath));
|
|
1104
|
-
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1226
|
+
for (const [pattern, template] of Object.entries(detection.filePatterns)) {
|
|
1227
|
+
const re = new RegExp(`^(${pattern})$`);
|
|
1228
|
+
const match = basename3.match(re);
|
|
1229
|
+
if (match) {
|
|
1230
|
+
const resolved = template.replace("${match}", match[1]);
|
|
1231
|
+
return this.normaliseAgentId(resolved);
|
|
1232
|
+
}
|
|
1107
1233
|
}
|
|
1108
1234
|
}
|
|
1109
1235
|
return this.normaliseAgentId(pathAgent);
|
|
@@ -1111,20 +1237,23 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1111
1237
|
extractAgentFromPath(filePath) {
|
|
1112
1238
|
const filename = path.basename(filePath, path.extname(filePath));
|
|
1113
1239
|
const pathParts = filePath.split(path.sep);
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1240
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1241
|
+
let pathPrefix = "";
|
|
1242
|
+
if (detection.pathPatterns) {
|
|
1243
|
+
for (const [pathSubstring, agentId] of Object.entries(detection.pathPatterns)) {
|
|
1244
|
+
if (filePath.includes(pathSubstring)) {
|
|
1245
|
+
pathPrefix = agentId;
|
|
1246
|
+
break;
|
|
1247
|
+
}
|
|
1121
1248
|
}
|
|
1122
|
-
return "openclaw";
|
|
1123
1249
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1250
|
+
const agentsIndex = pathParts.lastIndexOf("agents");
|
|
1251
|
+
if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
|
|
1252
|
+
const agentName = pathParts[agentsIndex + 1];
|
|
1253
|
+
return pathPrefix ? `${pathPrefix}-${agentName}` : agentName;
|
|
1126
1254
|
}
|
|
1127
|
-
|
|
1255
|
+
if (pathPrefix) return pathPrefix;
|
|
1256
|
+
for (const part of [...pathParts].reverse()) {
|
|
1128
1257
|
if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
|
|
1129
1258
|
return part;
|
|
1130
1259
|
}
|
|
@@ -1459,19 +1588,22 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1459
1588
|
const parentDir = path.basename(path.dirname(filePath));
|
|
1460
1589
|
const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
|
|
1461
1590
|
const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
|
|
1462
|
-
let
|
|
1591
|
+
let agentName;
|
|
1463
1592
|
if (parentDir === "sessions" && greatGrandParentDir === "agents") {
|
|
1464
|
-
|
|
1593
|
+
agentName = grandParentDir;
|
|
1465
1594
|
} else if (grandParentDir === "agents") {
|
|
1466
|
-
|
|
1467
|
-
} else if (parentDir === "runs" && grandParentDir === "cron") {
|
|
1468
|
-
agentId = "openclaw-cron";
|
|
1595
|
+
agentName = parentDir;
|
|
1469
1596
|
} else {
|
|
1470
|
-
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1597
|
+
agentName = parentDir;
|
|
1598
|
+
}
|
|
1599
|
+
let agentId = agentName;
|
|
1600
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1601
|
+
if (detection.pathPatterns) {
|
|
1602
|
+
for (const [pathSubstring, prefix] of Object.entries(detection.pathPatterns)) {
|
|
1603
|
+
if (filePath.includes(pathSubstring)) {
|
|
1604
|
+
agentId = `${prefix}-${agentName}`;
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1475
1607
|
}
|
|
1476
1608
|
}
|
|
1477
1609
|
const modelEvent = rawEvents.find((e) => e.type === "model_change");
|
|
@@ -1760,6 +1892,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1760
1892
|
edges: [],
|
|
1761
1893
|
events: [],
|
|
1762
1894
|
startTime,
|
|
1895
|
+
status,
|
|
1763
1896
|
agentId,
|
|
1764
1897
|
trigger,
|
|
1765
1898
|
name: rootName,
|
|
@@ -1880,8 +2013,12 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1880
2013
|
// Ignore git directories
|
|
1881
2014
|
/\.vscode/,
|
|
1882
2015
|
// Ignore vscode
|
|
1883
|
-
/\.idea
|
|
2016
|
+
/\.idea/,
|
|
1884
2017
|
// Ignore idea
|
|
2018
|
+
/\/archive\//,
|
|
2019
|
+
// Ignore archived trace files
|
|
2020
|
+
// Ignore user-configured skip directories
|
|
2021
|
+
...getSkipDirectories(this.userConfig).map((d) => new RegExp(`/${d}/`))
|
|
1885
2022
|
],
|
|
1886
2023
|
persistent: true,
|
|
1887
2024
|
ignoreInitial: true,
|
|
@@ -1935,29 +2072,42 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1935
2072
|
});
|
|
1936
2073
|
}
|
|
1937
2074
|
getTrace(filename) {
|
|
2075
|
+
const candidates = [];
|
|
1938
2076
|
const exact = this.traces.get(filename);
|
|
1939
|
-
if (exact)
|
|
2077
|
+
if (exact) candidates.push(exact);
|
|
1940
2078
|
if (filename.includes("::")) {
|
|
1941
2079
|
const [fname, startTimeStr] = filename.split("::");
|
|
1942
2080
|
const startTime = Number(startTimeStr);
|
|
1943
2081
|
if (fname && !Number.isNaN(startTime)) {
|
|
1944
2082
|
for (const trace of this.traces.values()) {
|
|
1945
2083
|
if (trace.filename === fname && trace.startTime === startTime) {
|
|
1946
|
-
|
|
2084
|
+
candidates.push(trace);
|
|
1947
2085
|
}
|
|
1948
2086
|
}
|
|
1949
2087
|
}
|
|
1950
2088
|
}
|
|
1951
2089
|
for (const prefix of ["openclaw:", "otel:", ""]) {
|
|
1952
2090
|
const prefixed = this.traces.get(prefix + filename);
|
|
1953
|
-
if (prefixed)
|
|
2091
|
+
if (prefixed) candidates.push(prefixed);
|
|
1954
2092
|
}
|
|
1955
2093
|
for (const [key, trace] of this.traces) {
|
|
1956
2094
|
if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
|
|
1957
|
-
|
|
2095
|
+
candidates.push(trace);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
if (candidates.length === 0) return void 0;
|
|
2099
|
+
if (candidates.length === 1) return candidates[0];
|
|
2100
|
+
let best = candidates[0];
|
|
2101
|
+
let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
|
|
2102
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
2103
|
+
const c = candidates[i];
|
|
2104
|
+
const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
|
|
2105
|
+
if (nc > bestNodeCount) {
|
|
2106
|
+
best = c;
|
|
2107
|
+
bestNodeCount = nc;
|
|
1958
2108
|
}
|
|
1959
2109
|
}
|
|
1960
|
-
return
|
|
2110
|
+
return best;
|
|
1961
2111
|
}
|
|
1962
2112
|
getTracesByAgent(agentId) {
|
|
1963
2113
|
return this.getAllTraces().filter((trace) => trace.agentId === agentId);
|
|
@@ -2004,7 +2154,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
2004
2154
|
var fs2 = __toESM(require("fs"), 1);
|
|
2005
2155
|
var os = __toESM(require("os"), 1);
|
|
2006
2156
|
var path2 = __toESM(require("path"), 1);
|
|
2007
|
-
var VERSION = "0.
|
|
2157
|
+
var VERSION = "0.8.0";
|
|
2008
2158
|
function getLanAddress() {
|
|
2009
2159
|
const interfaces = os.networkInterfaces();
|
|
2010
2160
|
for (const name of Object.keys(interfaces)) {
|
|
@@ -2016,7 +2166,7 @@ function getLanAddress() {
|
|
|
2016
2166
|
}
|
|
2017
2167
|
return null;
|
|
2018
2168
|
}
|
|
2019
|
-
function printBanner(config, traceCount, stats) {
|
|
2169
|
+
function printBanner(config, traceCount, stats, configPath) {
|
|
2020
2170
|
var _a;
|
|
2021
2171
|
const lan = getLanAddress();
|
|
2022
2172
|
const host = config.host || "localhost";
|
|
@@ -2032,26 +2182,23 @@ function printBanner(config, traceCount, stats) {
|
|
|
2032
2182
|
|
|
2033
2183
|
See your agents think.
|
|
2034
2184
|
|
|
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
2185
|
Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? `
|
|
2047
2186
|
Data dirs: ${config.dataDirs.join("\n ")}` : ""}
|
|
2048
2187
|
Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
|
|
2049
2188
|
Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
|
|
2189
|
+
Config: ${configPath ?? "none (using defaults)"}
|
|
2050
2190
|
CORS: ${config.enableCors ? "enabled" : "disabled"}
|
|
2051
2191
|
WebSocket: live updates enabled
|
|
2192
|
+
Window: ${process.env.AGENTFLOW_TRACE_WINDOW_HOURS ?? "48"}h (set AGENTFLOW_TRACE_WINDOW_HOURS to change)
|
|
2052
2193
|
|
|
2053
2194
|
\u2192 http://localhost:${port}${isPublic && lan ? `
|
|
2054
2195
|
\u2192 http://${lan}:${port} (LAN)` : ""}
|
|
2196
|
+
|
|
2197
|
+
Views: Agent Profile \xB7 Execution Detail \xB7 Governance
|
|
2198
|
+
Tabs: Flame Chart \xB7 Agent Flow \xB7 Metrics \xB7 Dependencies
|
|
2199
|
+
State Machine \xB7 Summary \xB7 Transcript
|
|
2200
|
+
|
|
2201
|
+
Runs locally. Your data never leaves your machine.
|
|
2055
2202
|
`);
|
|
2056
2203
|
}
|
|
2057
2204
|
async function startDashboard() {
|
|
@@ -2083,11 +2230,32 @@ async function startDashboard() {
|
|
|
2083
2230
|
case "--cors":
|
|
2084
2231
|
config.enableCors = true;
|
|
2085
2232
|
break;
|
|
2233
|
+
case "--no-collector":
|
|
2234
|
+
config.enableCollector = false;
|
|
2235
|
+
break;
|
|
2236
|
+
case "--collector-token":
|
|
2237
|
+
config.collectorAuthToken = args[++i];
|
|
2238
|
+
break;
|
|
2239
|
+
case "--soma-vault":
|
|
2240
|
+
config.somaVault = args[++i];
|
|
2241
|
+
break;
|
|
2242
|
+
case "--config":
|
|
2243
|
+
config.configPath = args[++i];
|
|
2244
|
+
break;
|
|
2086
2245
|
case "--help":
|
|
2087
2246
|
printHelp();
|
|
2088
2247
|
process.exit(0);
|
|
2089
2248
|
}
|
|
2090
2249
|
}
|
|
2250
|
+
if (!config.collectorAuthToken && process.env.AGENTFLOW_COLLECTOR_TOKEN) {
|
|
2251
|
+
config.collectorAuthToken = process.env.AGENTFLOW_COLLECTOR_TOKEN;
|
|
2252
|
+
}
|
|
2253
|
+
if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
|
|
2254
|
+
config.enableCollector = false;
|
|
2255
|
+
}
|
|
2256
|
+
if (!config.somaVault && process.env.SOMA_VAULT) {
|
|
2257
|
+
config.somaVault = process.env.SOMA_VAULT;
|
|
2258
|
+
}
|
|
2091
2259
|
const tracesPath = path2.resolve(config.tracesDir);
|
|
2092
2260
|
if (!fs2.existsSync(tracesPath)) {
|
|
2093
2261
|
fs2.mkdirSync(tracesPath, { recursive: true });
|
|
@@ -2109,7 +2277,7 @@ async function startDashboard() {
|
|
|
2109
2277
|
setTimeout(() => {
|
|
2110
2278
|
const stats = dashboard.getStats();
|
|
2111
2279
|
const traces = dashboard.getTraces();
|
|
2112
|
-
printBanner(config, traces.length, stats);
|
|
2280
|
+
printBanner(config, traces.length, stats, dashboard.getConfigPath());
|
|
2113
2281
|
}, 1500);
|
|
2114
2282
|
} catch (error) {
|
|
2115
2283
|
console.error("\u274C Failed to start dashboard:", error);
|
|
@@ -2118,7 +2286,7 @@ async function startDashboard() {
|
|
|
2118
2286
|
}
|
|
2119
2287
|
function printHelp() {
|
|
2120
2288
|
console.log(`
|
|
2121
|
-
|
|
2289
|
+
AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
|
|
2122
2290
|
|
|
2123
2291
|
Usage:
|
|
2124
2292
|
agentflow-dashboard [options]
|
|
@@ -2129,20 +2297,34 @@ Options:
|
|
|
2129
2297
|
-t, --traces <path> Traces directory (default: ./traces)
|
|
2130
2298
|
-h, --host <address> Host address (default: localhost)
|
|
2131
2299
|
--data-dir <path> Extra data directory for process discovery (repeatable)
|
|
2300
|
+
--config <path> Path to agentflow.config.json (aliases, skip files, etc.)
|
|
2301
|
+
--soma-vault <path> SOMA vault directory for intelligence data
|
|
2132
2302
|
--cors Enable CORS headers
|
|
2303
|
+
--no-collector Disable OTLP trace collector (POST /v1/traces)
|
|
2304
|
+
--collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
|
|
2133
2305
|
--help Show this help message
|
|
2134
2306
|
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2307
|
+
Config file:
|
|
2308
|
+
The dashboard loads agentflow.config.json for agent aliases, skip files,
|
|
2309
|
+
discovery paths, and systemd services. Resolution order:
|
|
2310
|
+
1. --config flag
|
|
2311
|
+
2. AGENTFLOW_CONFIG env var
|
|
2312
|
+
3. ./agentflow.config.json
|
|
2313
|
+
4. ~/.config/agentflow/config.json
|
|
2314
|
+
|
|
2315
|
+
See agentflow.config.example.json for a complete reference.
|
|
2316
|
+
|
|
2317
|
+
Environment:
|
|
2318
|
+
AGENTFLOW_CONFIG Path to config file
|
|
2319
|
+
AGENTFLOW_TRACE_WINDOW_HOURS Max age of traces to load (default: 48)
|
|
2320
|
+
AGENTFLOW_COLLECTOR_TOKEN Auth token for OTLP collector
|
|
2321
|
+
AGENTFLOW_NO_COLLECTOR=true Disable OTLP collector
|
|
2322
|
+
SOMA_VAULT SOMA vault directory
|
|
2139
2323
|
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
\u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
|
|
2145
|
-
\u26A0\uFE0F Errors Failed and hung nodes with metadata
|
|
2324
|
+
Examples:
|
|
2325
|
+
agentflow-dashboard --traces ./traces --host 0.0.0.0
|
|
2326
|
+
agentflow-dashboard --traces ./traces --config ./agentflow.config.json
|
|
2327
|
+
agentflow-dashboard -p 8080 -t /var/log/agentflow --cors
|
|
2146
2328
|
`);
|
|
2147
2329
|
}
|
|
2148
2330
|
|
|
@@ -2165,12 +2347,15 @@ function serializeTrace(trace) {
|
|
|
2165
2347
|
var DashboardServer = class {
|
|
2166
2348
|
constructor(config) {
|
|
2167
2349
|
this.config = config;
|
|
2168
|
-
const
|
|
2169
|
-
|
|
2350
|
+
const { config: userCfg, configPath: cfgPath } = loadConfig(config.configPath);
|
|
2351
|
+
this.userConfig = userCfg;
|
|
2352
|
+
this.configPath = cfgPath;
|
|
2353
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
2354
|
+
const dashConfigPath = path3.join(home, ".agentflow/dashboard-config.json");
|
|
2170
2355
|
if (!config.dataDirs) config.dataDirs = [];
|
|
2171
2356
|
try {
|
|
2172
|
-
if (fs3.existsSync(
|
|
2173
|
-
const saved = JSON.parse(fs3.readFileSync(
|
|
2357
|
+
if (fs3.existsSync(dashConfigPath)) {
|
|
2358
|
+
const saved = JSON.parse(fs3.readFileSync(dashConfigPath, "utf-8"));
|
|
2174
2359
|
const extraDirs = saved.extraDirs ?? [];
|
|
2175
2360
|
for (const d of extraDirs) {
|
|
2176
2361
|
if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
|
|
@@ -2178,21 +2363,15 @@ var DashboardServer = class {
|
|
|
2178
2363
|
}
|
|
2179
2364
|
} catch {
|
|
2180
2365
|
}
|
|
2181
|
-
const
|
|
2182
|
-
path3.join(home, ".openclaw/cron/runs"),
|
|
2183
|
-
path3.join(home, ".openclaw/workspace/traces"),
|
|
2184
|
-
path3.join(home, ".openclaw/subagents"),
|
|
2185
|
-
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2186
|
-
path3.join(home, ".agentflow/traces")
|
|
2187
|
-
];
|
|
2188
|
-
for (const p of autoDiscoverPaths) {
|
|
2366
|
+
for (const p of getDiscoveryPaths(this.userConfig)) {
|
|
2189
2367
|
if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
|
|
2190
2368
|
config.dataDirs.push(p);
|
|
2191
2369
|
}
|
|
2192
2370
|
}
|
|
2193
2371
|
this.watcher = new TraceWatcher({
|
|
2194
2372
|
tracesDir: config.tracesDir,
|
|
2195
|
-
dataDirs: config.dataDirs
|
|
2373
|
+
dataDirs: config.dataDirs,
|
|
2374
|
+
userConfig: this.userConfig
|
|
2196
2375
|
});
|
|
2197
2376
|
this.stats = new AgentStats();
|
|
2198
2377
|
this.knowledgeStore = (0, import_agentflow_core3.createKnowledgeStore)({
|
|
@@ -2227,6 +2406,8 @@ var DashboardServer = class {
|
|
|
2227
2406
|
ts: 0
|
|
2228
2407
|
};
|
|
2229
2408
|
knowledgeStore;
|
|
2409
|
+
userConfig;
|
|
2410
|
+
configPath;
|
|
2230
2411
|
setupExpress() {
|
|
2231
2412
|
if (this.config.enableCors) {
|
|
2232
2413
|
this.app.use((_req, res, next) => {
|
|
@@ -2238,18 +2419,35 @@ var DashboardServer = class {
|
|
|
2238
2419
|
next();
|
|
2239
2420
|
});
|
|
2240
2421
|
}
|
|
2241
|
-
const
|
|
2422
|
+
const pkgDir = path3.join(__dirname, "..");
|
|
2423
|
+
const clientDir = path3.join(pkgDir, "dist/client");
|
|
2424
|
+
const clientIndex = path3.join(clientDir, "index.html");
|
|
2425
|
+
const srcDir = path3.join(pkgDir, "src/client");
|
|
2426
|
+
const needsBuild = !fs3.existsSync(clientIndex) || fs3.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
|
|
2427
|
+
if (needsBuild) {
|
|
2428
|
+
try {
|
|
2429
|
+
console.log("Building dashboard client...");
|
|
2430
|
+
(0, import_node_child_process.execSync)("npm run build:client", { cwd: pkgDir, stdio: "inherit", timeout: 3e4 });
|
|
2431
|
+
} catch (err) {
|
|
2432
|
+
console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2242
2435
|
if (fs3.existsSync(clientDir)) {
|
|
2243
2436
|
this.app.use(import_express.default.static(clientDir));
|
|
2244
2437
|
}
|
|
2245
|
-
|
|
2246
|
-
if (fs3.existsSync(publicDir)) {
|
|
2247
|
-
this.app.use("/v1", import_express.default.static(publicDir));
|
|
2248
|
-
}
|
|
2249
|
-
this.app.get("/api/traces", (_req, res) => {
|
|
2438
|
+
this.app.get("/api/traces", (req, res) => {
|
|
2250
2439
|
try {
|
|
2251
|
-
const
|
|
2252
|
-
|
|
2440
|
+
const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
|
|
2441
|
+
const cursor = req.query.cursor ? parseFloat(req.query.cursor) : void 0;
|
|
2442
|
+
let allTraces = this.watcher.getAllTraces();
|
|
2443
|
+
if (cursor) {
|
|
2444
|
+
allTraces = allTraces.filter((t) => (t.lastModified || t.startTime) < cursor);
|
|
2445
|
+
}
|
|
2446
|
+
const page = allTraces.slice(0, limit);
|
|
2447
|
+
const serialized = page.map(serializeTrace);
|
|
2448
|
+
const lastTrace = page[page.length - 1];
|
|
2449
|
+
const nextCursor = page.length === limit && lastTrace ? lastTrace.lastModified || lastTrace.startTime : null;
|
|
2450
|
+
res.json({ traces: serialized, nextCursor });
|
|
2253
2451
|
} catch (_error) {
|
|
2254
2452
|
res.status(500).json({ error: "Failed to load traces" });
|
|
2255
2453
|
}
|
|
@@ -2540,6 +2738,102 @@ var DashboardServer = class {
|
|
|
2540
2738
|
res.status(500).json({ error: "Failed to load agent statistics" });
|
|
2541
2739
|
}
|
|
2542
2740
|
});
|
|
2741
|
+
this.app.get("/api/soma/report", (_req, res) => {
|
|
2742
|
+
const somaVault = this.config.somaVault;
|
|
2743
|
+
if (!somaVault) {
|
|
2744
|
+
return res.json({ available: false, teaser: true });
|
|
2745
|
+
}
|
|
2746
|
+
try {
|
|
2747
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2748
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2749
|
+
return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
|
|
2750
|
+
}
|
|
2751
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2752
|
+
res.json(report);
|
|
2753
|
+
} catch (error) {
|
|
2754
|
+
console.error("Soma report error:", error);
|
|
2755
|
+
res.json({ available: false, teaser: false, message: "Failed to read report" });
|
|
2756
|
+
}
|
|
2757
|
+
});
|
|
2758
|
+
this.app.get("/api/soma/governance", (_req, res) => {
|
|
2759
|
+
const somaVault = this.config.somaVault;
|
|
2760
|
+
if (!somaVault) {
|
|
2761
|
+
return res.json({ available: false });
|
|
2762
|
+
}
|
|
2763
|
+
try {
|
|
2764
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2765
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2766
|
+
return res.json({ available: false, message: "No report file. Run soma report." });
|
|
2767
|
+
}
|
|
2768
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2769
|
+
res.json({
|
|
2770
|
+
available: true,
|
|
2771
|
+
layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
|
|
2772
|
+
governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
|
|
2773
|
+
insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
|
|
2774
|
+
canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
|
|
2775
|
+
generatedAt: report.generatedAt
|
|
2776
|
+
});
|
|
2777
|
+
} catch (error) {
|
|
2778
|
+
console.error("Soma governance error:", error);
|
|
2779
|
+
res.status(500).json({ available: false, message: "Failed to read governance data" });
|
|
2780
|
+
}
|
|
2781
|
+
});
|
|
2782
|
+
const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
|
|
2783
|
+
const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
|
|
2784
|
+
this.app.post("/api/soma/governance/promote", (req, res) => {
|
|
2785
|
+
var _a;
|
|
2786
|
+
const somaVault = this.config.somaVault;
|
|
2787
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2788
|
+
const { entryId } = req.body ?? {};
|
|
2789
|
+
if (!entryId) return res.status(400).json({ error: "entryId required" });
|
|
2790
|
+
try {
|
|
2791
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2792
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2793
|
+
const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
|
|
2794
|
+
encoding: "utf-8",
|
|
2795
|
+
timeout: 1e4
|
|
2796
|
+
});
|
|
2797
|
+
res.json({ success: true, message: result.trim() });
|
|
2798
|
+
} catch (error) {
|
|
2799
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2800
|
+
}
|
|
2801
|
+
});
|
|
2802
|
+
this.app.post("/api/soma/governance/reject", (req, res) => {
|
|
2803
|
+
var _a;
|
|
2804
|
+
const somaVault = this.config.somaVault;
|
|
2805
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2806
|
+
const { entryId, reason } = req.body ?? {};
|
|
2807
|
+
if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
|
|
2808
|
+
try {
|
|
2809
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2810
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2811
|
+
const safeReason = sanitizeReason(String(reason));
|
|
2812
|
+
const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
|
|
2813
|
+
encoding: "utf-8",
|
|
2814
|
+
timeout: 1e4
|
|
2815
|
+
});
|
|
2816
|
+
res.json({ success: true, message: result.trim() });
|
|
2817
|
+
} catch (error) {
|
|
2818
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2819
|
+
}
|
|
2820
|
+
});
|
|
2821
|
+
this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
|
|
2822
|
+
var _a;
|
|
2823
|
+
const somaVault = this.config.somaVault;
|
|
2824
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2825
|
+
try {
|
|
2826
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2827
|
+
const safeId = sanitizeArg(String(req.params.id));
|
|
2828
|
+
const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
|
|
2829
|
+
encoding: "utf-8",
|
|
2830
|
+
timeout: 1e4
|
|
2831
|
+
});
|
|
2832
|
+
res.json({ available: true, output: result.trim() });
|
|
2833
|
+
} catch (error) {
|
|
2834
|
+
res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2835
|
+
}
|
|
2836
|
+
});
|
|
2543
2837
|
this.app.get("/api/process-health", (_req, res) => {
|
|
2544
2838
|
var _a, _b;
|
|
2545
2839
|
try {
|
|
@@ -2552,7 +2846,14 @@ var DashboardServer = class {
|
|
|
2552
2846
|
path3.dirname(this.config.tracesDir),
|
|
2553
2847
|
...this.config.dataDirs || []
|
|
2554
2848
|
];
|
|
2555
|
-
|
|
2849
|
+
let configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
|
|
2850
|
+
const pref = getProcessPreference(this.userConfig);
|
|
2851
|
+
if (pref) {
|
|
2852
|
+
const hasPreferred = configs.some((c) => c.processName === pref.prefer);
|
|
2853
|
+
if (hasPreferred) {
|
|
2854
|
+
configs = configs.filter((c) => c.processName !== pref.over);
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2556
2857
|
if (configs.length === 0) {
|
|
2557
2858
|
return res.json(null);
|
|
2558
2859
|
}
|
|
@@ -2642,29 +2943,26 @@ var DashboardServer = class {
|
|
|
2642
2943
|
...extraDirs
|
|
2643
2944
|
];
|
|
2644
2945
|
const discovered = [];
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
const
|
|
2655
|
-
if (
|
|
2946
|
+
const svcNames = getSystemdServices(this.userConfig);
|
|
2947
|
+
if (svcNames.length > 0) {
|
|
2948
|
+
try {
|
|
2949
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2950
|
+
const raw = execSync2(
|
|
2951
|
+
`systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
|
|
2952
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
2953
|
+
);
|
|
2954
|
+
for (const line of raw.split("\n")) {
|
|
2955
|
+
const match = line.match(/path=([^\s;]+)/);
|
|
2956
|
+
if (match == null ? void 0 : match[1]) {
|
|
2957
|
+
const dir = path3.dirname(match[1]);
|
|
2958
|
+
if (fs3.existsSync(dir)) discovered.push(dir);
|
|
2959
|
+
}
|
|
2656
2960
|
}
|
|
2961
|
+
} catch {
|
|
2657
2962
|
}
|
|
2658
|
-
} catch {
|
|
2659
2963
|
}
|
|
2660
2964
|
const commonPaths = [
|
|
2661
|
-
|
|
2662
|
-
path3.join(home, ".alfred/data"),
|
|
2663
|
-
path3.join(home, ".openclaw/workspace/traces"),
|
|
2664
|
-
path3.join(home, ".openclaw/subagents"),
|
|
2665
|
-
path3.join(home, ".openclaw/cron/runs"),
|
|
2666
|
-
path3.join(home, ".openclaw/cron"),
|
|
2667
|
-
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2965
|
+
...getDiscoveryPaths(this.userConfig),
|
|
2668
2966
|
path3.join(home, ".agentflow/traces")
|
|
2669
2967
|
];
|
|
2670
2968
|
for (const p of commonPaths) {
|
|
@@ -2709,46 +3007,54 @@ var DashboardServer = class {
|
|
|
2709
3007
|
res.status(500).json({ error: "Failed to update directory config" });
|
|
2710
3008
|
}
|
|
2711
3009
|
});
|
|
2712
|
-
this.
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
3010
|
+
if (this.config.enableCollector !== false) {
|
|
3011
|
+
this.app.post("/v1/traces", import_express.default.json({ limit: "10mb" }), (req, res) => {
|
|
3012
|
+
try {
|
|
3013
|
+
if (this.config.collectorAuthToken) {
|
|
3014
|
+
const auth = req.headers.authorization;
|
|
3015
|
+
if (!auth || auth !== `Bearer ${this.config.collectorAuthToken}`) {
|
|
3016
|
+
return res.status(401).json({ error: "Unauthorized \u2014 provide Authorization: Bearer <token>" });
|
|
3017
|
+
}
|
|
2720
3018
|
}
|
|
2721
|
-
const
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
3019
|
+
const traces = parseOtlpPayload(req.body);
|
|
3020
|
+
let ingested = 0;
|
|
3021
|
+
for (const trace of traces) {
|
|
3022
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
3023
|
+
for (const [id, node] of Object.entries(trace.nodes)) {
|
|
3024
|
+
nodes.set(id, { ...node, state: {} });
|
|
3025
|
+
}
|
|
3026
|
+
const watched = {
|
|
3027
|
+
id: trace.id,
|
|
3028
|
+
rootNodeId: Object.keys(trace.nodes)[0] ?? "",
|
|
3029
|
+
agentId: trace.agentId,
|
|
3030
|
+
name: trace.name,
|
|
3031
|
+
trigger: trace.trigger,
|
|
3032
|
+
startTime: trace.startTime,
|
|
3033
|
+
endTime: trace.endTime,
|
|
3034
|
+
status: trace.status,
|
|
3035
|
+
nodes,
|
|
3036
|
+
edges: [],
|
|
3037
|
+
events: [],
|
|
3038
|
+
metadata: { ...trace.metadata, adapterSource: "otel" },
|
|
3039
|
+
sessionEvents: [],
|
|
3040
|
+
sourceType: "session",
|
|
3041
|
+
filename: `otel-${trace.id}`,
|
|
3042
|
+
lastModified: Date.now(),
|
|
3043
|
+
sourceDir: "http-collector"
|
|
3044
|
+
};
|
|
3045
|
+
this.watcher.traces.set(`otel:${trace.id}`, watched);
|
|
3046
|
+
ingested++;
|
|
3047
|
+
}
|
|
3048
|
+
if (ingested > 0) {
|
|
3049
|
+
this.broadcast({ type: "traces-updated", count: ingested });
|
|
3050
|
+
}
|
|
3051
|
+
res.json({ ok: true, tracesIngested: ingested });
|
|
3052
|
+
} catch (error) {
|
|
3053
|
+
console.error("OTLP collector error:", error);
|
|
3054
|
+
res.status(400).json({ error: "Failed to parse OTLP payload" });
|
|
2745
3055
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
console.error("OTLP collector error:", error);
|
|
2749
|
-
res.status(400).json({ error: "Failed to parse OTLP payload" });
|
|
2750
|
-
}
|
|
2751
|
-
});
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
2752
3058
|
this.app.get("/health", (_req, res) => {
|
|
2753
3059
|
res.json({
|
|
2754
3060
|
status: "ok",
|
|
@@ -2760,18 +3066,10 @@ var DashboardServer = class {
|
|
|
2760
3066
|
this.app.get("/ready", (_req, res) => {
|
|
2761
3067
|
res.json({ status: "ready" });
|
|
2762
3068
|
});
|
|
2763
|
-
this.app.get("/v1/*", (_req, res) => {
|
|
2764
|
-
const legacyIndex = path3.join(__dirname, "../public/index.html");
|
|
2765
|
-
if (fs3.existsSync(legacyIndex)) {
|
|
2766
|
-
res.sendFile(legacyIndex);
|
|
2767
|
-
} else {
|
|
2768
|
-
res.status(404).send("Legacy dashboard not found");
|
|
2769
|
-
}
|
|
2770
|
-
});
|
|
2771
3069
|
this.app.get("*", (_req, res) => {
|
|
2772
|
-
const
|
|
2773
|
-
if (fs3.existsSync(
|
|
2774
|
-
res.sendFile(
|
|
3070
|
+
const clientIndex2 = path3.join(__dirname, "../dist/client/index.html");
|
|
3071
|
+
if (fs3.existsSync(clientIndex2)) {
|
|
3072
|
+
res.sendFile(clientIndex2);
|
|
2775
3073
|
} else {
|
|
2776
3074
|
res.status(404).send("Dashboard not found - public files may not be built");
|
|
2777
3075
|
}
|
|
@@ -3018,24 +3316,49 @@ var DashboardServer = class {
|
|
|
3018
3316
|
});
|
|
3019
3317
|
}
|
|
3020
3318
|
async start() {
|
|
3021
|
-
return new Promise((
|
|
3319
|
+
return new Promise((resolve5) => {
|
|
3022
3320
|
const host = this.config.host || "localhost";
|
|
3023
3321
|
this.server.listen(this.config.port, host, () => {
|
|
3024
3322
|
console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
|
|
3025
3323
|
console.log(`Watching traces in: ${this.config.tracesDir}`);
|
|
3026
|
-
|
|
3324
|
+
resolve5();
|
|
3027
3325
|
});
|
|
3028
3326
|
});
|
|
3029
3327
|
}
|
|
3328
|
+
/** Check if any src/client file is newer than the built bundle. */
|
|
3329
|
+
isClientStale(srcDir, distDir) {
|
|
3330
|
+
try {
|
|
3331
|
+
const distIndex = path3.join(distDir, "index.html");
|
|
3332
|
+
if (!fs3.existsSync(distIndex)) return true;
|
|
3333
|
+
const distMtime = fs3.statSync(distIndex).mtimeMs;
|
|
3334
|
+
const check = (dir) => {
|
|
3335
|
+
for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
|
|
3336
|
+
const full = path3.join(dir, entry.name);
|
|
3337
|
+
if (entry.isDirectory()) {
|
|
3338
|
+
if (check(full)) return true;
|
|
3339
|
+
} else if (fs3.statSync(full).mtimeMs > distMtime) {
|
|
3340
|
+
return true;
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
return false;
|
|
3344
|
+
};
|
|
3345
|
+
return check(srcDir);
|
|
3346
|
+
} catch {
|
|
3347
|
+
return false;
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3030
3350
|
async stop() {
|
|
3031
|
-
return new Promise((
|
|
3351
|
+
return new Promise((resolve5) => {
|
|
3032
3352
|
this.watcher.stop();
|
|
3033
3353
|
this.server.close(() => {
|
|
3034
3354
|
console.log("Dashboard server stopped");
|
|
3035
|
-
|
|
3355
|
+
resolve5();
|
|
3036
3356
|
});
|
|
3037
3357
|
});
|
|
3038
3358
|
}
|
|
3359
|
+
getConfigPath() {
|
|
3360
|
+
return this.configPath;
|
|
3361
|
+
}
|
|
3039
3362
|
getStats() {
|
|
3040
3363
|
return this.stats.getGlobalStats();
|
|
3041
3364
|
}
|