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/cli.cjs
CHANGED
|
@@ -37,11 +37,86 @@ var os = __toESM(require("os"), 1);
|
|
|
37
37
|
var path3 = __toESM(require("path"), 1);
|
|
38
38
|
|
|
39
39
|
// src/server.ts
|
|
40
|
+
var import_node_child_process = require("child_process");
|
|
40
41
|
var fs2 = __toESM(require("fs"), 1);
|
|
41
42
|
var import_node_http = require("http");
|
|
42
43
|
var path2 = __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);
|
|
@@ -2019,12 +2170,15 @@ function serializeTrace(trace) {
|
|
|
2019
2170
|
var DashboardServer = class {
|
|
2020
2171
|
constructor(config) {
|
|
2021
2172
|
this.config = config;
|
|
2022
|
-
const
|
|
2023
|
-
|
|
2173
|
+
const { config: userCfg, configPath: cfgPath } = loadConfig(config.configPath);
|
|
2174
|
+
this.userConfig = userCfg;
|
|
2175
|
+
this.configPath = cfgPath;
|
|
2176
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
2177
|
+
const dashConfigPath = path2.join(home, ".agentflow/dashboard-config.json");
|
|
2024
2178
|
if (!config.dataDirs) config.dataDirs = [];
|
|
2025
2179
|
try {
|
|
2026
|
-
if (fs2.existsSync(
|
|
2027
|
-
const saved = JSON.parse(fs2.readFileSync(
|
|
2180
|
+
if (fs2.existsSync(dashConfigPath)) {
|
|
2181
|
+
const saved = JSON.parse(fs2.readFileSync(dashConfigPath, "utf-8"));
|
|
2028
2182
|
const extraDirs = saved.extraDirs ?? [];
|
|
2029
2183
|
for (const d of extraDirs) {
|
|
2030
2184
|
if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
|
|
@@ -2032,21 +2186,15 @@ var DashboardServer = class {
|
|
|
2032
2186
|
}
|
|
2033
2187
|
} catch {
|
|
2034
2188
|
}
|
|
2035
|
-
const
|
|
2036
|
-
path2.join(home, ".openclaw/cron/runs"),
|
|
2037
|
-
path2.join(home, ".openclaw/workspace/traces"),
|
|
2038
|
-
path2.join(home, ".openclaw/subagents"),
|
|
2039
|
-
path2.join(home, ".openclaw/agents/main/sessions"),
|
|
2040
|
-
path2.join(home, ".agentflow/traces")
|
|
2041
|
-
];
|
|
2042
|
-
for (const p of autoDiscoverPaths) {
|
|
2189
|
+
for (const p of getDiscoveryPaths(this.userConfig)) {
|
|
2043
2190
|
if (fs2.existsSync(p) && !config.dataDirs.includes(p)) {
|
|
2044
2191
|
config.dataDirs.push(p);
|
|
2045
2192
|
}
|
|
2046
2193
|
}
|
|
2047
2194
|
this.watcher = new TraceWatcher({
|
|
2048
2195
|
tracesDir: config.tracesDir,
|
|
2049
|
-
dataDirs: config.dataDirs
|
|
2196
|
+
dataDirs: config.dataDirs,
|
|
2197
|
+
userConfig: this.userConfig
|
|
2050
2198
|
});
|
|
2051
2199
|
this.stats = new AgentStats();
|
|
2052
2200
|
this.knowledgeStore = (0, import_agentflow_core3.createKnowledgeStore)({
|
|
@@ -2055,6 +2203,7 @@ var DashboardServer = class {
|
|
|
2055
2203
|
this.setupExpress();
|
|
2056
2204
|
this.setupWebSocket();
|
|
2057
2205
|
this.setupTraceWatcher();
|
|
2206
|
+
this.setupSomaReportWatcher();
|
|
2058
2207
|
let knowledgeCount = 0;
|
|
2059
2208
|
for (const trace of this.watcher.getAllTraces()) {
|
|
2060
2209
|
this.stats.processTrace(trace);
|
|
@@ -2081,6 +2230,8 @@ var DashboardServer = class {
|
|
|
2081
2230
|
ts: 0
|
|
2082
2231
|
};
|
|
2083
2232
|
knowledgeStore;
|
|
2233
|
+
userConfig;
|
|
2234
|
+
configPath;
|
|
2084
2235
|
setupExpress() {
|
|
2085
2236
|
if (this.config.enableCors) {
|
|
2086
2237
|
this.app.use((_req, res, next) => {
|
|
@@ -2092,18 +2243,35 @@ var DashboardServer = class {
|
|
|
2092
2243
|
next();
|
|
2093
2244
|
});
|
|
2094
2245
|
}
|
|
2095
|
-
const
|
|
2246
|
+
const pkgDir = path2.join(__dirname, "..");
|
|
2247
|
+
const clientDir = path2.join(pkgDir, "dist/client");
|
|
2248
|
+
const clientIndex = path2.join(clientDir, "index.html");
|
|
2249
|
+
const srcDir = path2.join(pkgDir, "src/client");
|
|
2250
|
+
const needsBuild = !fs2.existsSync(clientIndex) || fs2.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
|
|
2251
|
+
if (needsBuild) {
|
|
2252
|
+
try {
|
|
2253
|
+
console.log("Building dashboard client...");
|
|
2254
|
+
(0, import_node_child_process.execSync)("npm run build:client", { cwd: pkgDir, stdio: "inherit", timeout: 3e4 });
|
|
2255
|
+
} catch (err) {
|
|
2256
|
+
console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2096
2259
|
if (fs2.existsSync(clientDir)) {
|
|
2097
2260
|
this.app.use(import_express.default.static(clientDir));
|
|
2098
2261
|
}
|
|
2099
|
-
|
|
2100
|
-
if (fs2.existsSync(publicDir)) {
|
|
2101
|
-
this.app.use("/v1", import_express.default.static(publicDir));
|
|
2102
|
-
}
|
|
2103
|
-
this.app.get("/api/traces", (_req, res) => {
|
|
2262
|
+
this.app.get("/api/traces", (req, res) => {
|
|
2104
2263
|
try {
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2264
|
+
const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
|
|
2265
|
+
const cursor = req.query.cursor ? parseFloat(req.query.cursor) : void 0;
|
|
2266
|
+
let allTraces = this.watcher.getAllTraces();
|
|
2267
|
+
if (cursor) {
|
|
2268
|
+
allTraces = allTraces.filter((t) => (t.lastModified || t.startTime) < cursor);
|
|
2269
|
+
}
|
|
2270
|
+
const page = allTraces.slice(0, limit);
|
|
2271
|
+
const serialized = page.map(serializeTrace);
|
|
2272
|
+
const lastTrace = page[page.length - 1];
|
|
2273
|
+
const nextCursor = page.length === limit && lastTrace ? lastTrace.lastModified || lastTrace.startTime : null;
|
|
2274
|
+
res.json({ traces: serialized, nextCursor });
|
|
2107
2275
|
} catch (_error) {
|
|
2108
2276
|
res.status(500).json({ error: "Failed to load traces" });
|
|
2109
2277
|
}
|
|
@@ -2394,6 +2562,235 @@ var DashboardServer = class {
|
|
|
2394
2562
|
res.status(500).json({ error: "Failed to load agent statistics" });
|
|
2395
2563
|
}
|
|
2396
2564
|
});
|
|
2565
|
+
this.app.get("/api/soma/tier", (_req, res) => {
|
|
2566
|
+
const somaVault = this.config.somaVault;
|
|
2567
|
+
if (!somaVault) {
|
|
2568
|
+
return res.json({ tier: "teaser", somaVault: false, governanceAvailable: false });
|
|
2569
|
+
}
|
|
2570
|
+
try {
|
|
2571
|
+
const reportPath = path2.join(somaVault, "..", "soma-report.json");
|
|
2572
|
+
if (!fs2.existsSync(reportPath)) {
|
|
2573
|
+
return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
|
|
2574
|
+
}
|
|
2575
|
+
const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
|
|
2576
|
+
const hasGovernance = report.governance && typeof report.governance.pending === "number";
|
|
2577
|
+
return res.json({
|
|
2578
|
+
tier: hasGovernance ? "pro" : "free",
|
|
2579
|
+
somaVault: true,
|
|
2580
|
+
governanceAvailable: !!hasGovernance
|
|
2581
|
+
});
|
|
2582
|
+
} catch {
|
|
2583
|
+
return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2586
|
+
this.app.get("/api/soma/report", (_req, res) => {
|
|
2587
|
+
const somaVault = this.config.somaVault;
|
|
2588
|
+
if (!somaVault) {
|
|
2589
|
+
return res.json({ available: false, teaser: true });
|
|
2590
|
+
}
|
|
2591
|
+
try {
|
|
2592
|
+
const reportPath = path2.join(somaVault, "..", "soma-report.json");
|
|
2593
|
+
if (!fs2.existsSync(reportPath)) {
|
|
2594
|
+
return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
|
|
2595
|
+
}
|
|
2596
|
+
const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
|
|
2597
|
+
res.json(report);
|
|
2598
|
+
} catch (error) {
|
|
2599
|
+
console.error("Soma report error:", error);
|
|
2600
|
+
res.json({ available: false, teaser: false, message: "Failed to read report" });
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
this.app.get("/api/soma/governance", (_req, res) => {
|
|
2604
|
+
const somaVault = this.config.somaVault;
|
|
2605
|
+
if (!somaVault) {
|
|
2606
|
+
return res.json({ available: false });
|
|
2607
|
+
}
|
|
2608
|
+
try {
|
|
2609
|
+
const reportPath = path2.join(somaVault, "..", "soma-report.json");
|
|
2610
|
+
if (!fs2.existsSync(reportPath)) {
|
|
2611
|
+
return res.json({ available: false, message: "No report file. Run soma report." });
|
|
2612
|
+
}
|
|
2613
|
+
const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
|
|
2614
|
+
res.json({
|
|
2615
|
+
available: true,
|
|
2616
|
+
layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
|
|
2617
|
+
governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
|
|
2618
|
+
insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
|
|
2619
|
+
canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
|
|
2620
|
+
generatedAt: report.generatedAt
|
|
2621
|
+
});
|
|
2622
|
+
} catch (error) {
|
|
2623
|
+
console.error("Soma governance error:", error);
|
|
2624
|
+
res.status(500).json({ available: false, message: "Failed to read governance data" });
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
|
|
2628
|
+
const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
|
|
2629
|
+
this.app.post("/api/soma/governance/promote", (req, res) => {
|
|
2630
|
+
var _a;
|
|
2631
|
+
const somaVault = this.config.somaVault;
|
|
2632
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2633
|
+
const { entryId } = req.body ?? {};
|
|
2634
|
+
if (!entryId) return res.status(400).json({ error: "entryId required" });
|
|
2635
|
+
try {
|
|
2636
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2637
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2638
|
+
const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
|
|
2639
|
+
encoding: "utf-8",
|
|
2640
|
+
timeout: 1e4
|
|
2641
|
+
});
|
|
2642
|
+
res.json({ success: true, message: result.trim() });
|
|
2643
|
+
} catch (error) {
|
|
2644
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
this.app.post("/api/soma/governance/reject", (req, res) => {
|
|
2648
|
+
var _a;
|
|
2649
|
+
const somaVault = this.config.somaVault;
|
|
2650
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2651
|
+
const { entryId, reason } = req.body ?? {};
|
|
2652
|
+
if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
|
|
2653
|
+
try {
|
|
2654
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2655
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2656
|
+
const safeReason = sanitizeReason(String(reason));
|
|
2657
|
+
const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
|
|
2658
|
+
encoding: "utf-8",
|
|
2659
|
+
timeout: 1e4
|
|
2660
|
+
});
|
|
2661
|
+
res.json({ success: true, message: result.trim() });
|
|
2662
|
+
} catch (error) {
|
|
2663
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
|
|
2667
|
+
var _a;
|
|
2668
|
+
const somaVault = this.config.somaVault;
|
|
2669
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2670
|
+
try {
|
|
2671
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2672
|
+
const safeId = sanitizeArg(String(req.params.id));
|
|
2673
|
+
const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
|
|
2674
|
+
encoding: "utf-8",
|
|
2675
|
+
timeout: 1e4
|
|
2676
|
+
});
|
|
2677
|
+
res.json({ available: true, output: result.trim() });
|
|
2678
|
+
} catch (error) {
|
|
2679
|
+
res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2680
|
+
}
|
|
2681
|
+
});
|
|
2682
|
+
this.app.get("/api/soma/policies", (_req, res) => {
|
|
2683
|
+
const somaVault = this.config.somaVault;
|
|
2684
|
+
if (!somaVault) return res.json({ policies: [] });
|
|
2685
|
+
try {
|
|
2686
|
+
const reportPath = path2.join(somaVault, "..", "soma-report.json");
|
|
2687
|
+
if (!fs2.existsSync(reportPath)) return res.json({ policies: [] });
|
|
2688
|
+
const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
|
|
2689
|
+
res.json({ policies: report.policies ?? [] });
|
|
2690
|
+
} catch {
|
|
2691
|
+
res.json({ policies: [] });
|
|
2692
|
+
}
|
|
2693
|
+
});
|
|
2694
|
+
this.app.post("/api/soma/policies", import_express.default.json(), (req, res) => {
|
|
2695
|
+
var _a;
|
|
2696
|
+
const somaVault = this.config.somaVault;
|
|
2697
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2698
|
+
const { name, enforcement, scope, conditions } = req.body ?? {};
|
|
2699
|
+
if (!name) return res.status(400).json({ error: "name required" });
|
|
2700
|
+
try {
|
|
2701
|
+
const safeName = sanitizeArg(String(name));
|
|
2702
|
+
const safeEnf = sanitizeArg(String(enforcement || "warn"));
|
|
2703
|
+
const safeScope = sanitizeReason(String(scope || "all"));
|
|
2704
|
+
const safeCond = sanitizeReason(String(conditions || ""));
|
|
2705
|
+
const result = (0, import_node_child_process.execSync)(
|
|
2706
|
+
`npx soma policy create "${safeName}" --enforcement ${safeEnf} --scope "${safeScope}" --conditions "${safeCond}" --vault "${somaVault}"`,
|
|
2707
|
+
{ encoding: "utf-8", timeout: 1e4 }
|
|
2708
|
+
);
|
|
2709
|
+
res.json({ success: true, message: result.trim() });
|
|
2710
|
+
} catch (error) {
|
|
2711
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
this.app.delete("/api/soma/policies/:name", (req, res) => {
|
|
2715
|
+
var _a;
|
|
2716
|
+
const somaVault = this.config.somaVault;
|
|
2717
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2718
|
+
try {
|
|
2719
|
+
const safeName = sanitizeArg(String(req.params.name));
|
|
2720
|
+
const result = (0, import_node_child_process.execSync)(
|
|
2721
|
+
`npx soma policy delete "${safeName}" --vault "${somaVault}"`,
|
|
2722
|
+
{ encoding: "utf-8", timeout: 1e4 }
|
|
2723
|
+
);
|
|
2724
|
+
res.json({ success: true, message: result.trim() });
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2727
|
+
}
|
|
2728
|
+
});
|
|
2729
|
+
this.app.get("/api/soma/vault/entities", (req, res) => {
|
|
2730
|
+
const somaVault = this.config.somaVault;
|
|
2731
|
+
if (!somaVault) return res.json({ entities: [], total: 0 });
|
|
2732
|
+
try {
|
|
2733
|
+
const reportPath = path2.join(somaVault, "..", "soma-report.json");
|
|
2734
|
+
if (!fs2.existsSync(reportPath)) return res.json({ entities: [], total: 0 });
|
|
2735
|
+
const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
|
|
2736
|
+
let entities = [
|
|
2737
|
+
...(report.agents ?? []).map((a) => ({ ...a, type: "agent", id: a.name })),
|
|
2738
|
+
...(report.insights ?? []).map((i, idx) => {
|
|
2739
|
+
var _a;
|
|
2740
|
+
return { ...i, type: i.type || "insight", id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}` };
|
|
2741
|
+
}),
|
|
2742
|
+
...(report.policies ?? []).map((p) => ({ ...p, type: "policy", id: p.name }))
|
|
2743
|
+
];
|
|
2744
|
+
const { type, layer, q, limit: limitStr, offset: offsetStr } = req.query;
|
|
2745
|
+
if (type) entities = entities.filter((e) => e.type === type);
|
|
2746
|
+
if (layer) entities = entities.filter((e) => e.layer === layer);
|
|
2747
|
+
if (q) {
|
|
2748
|
+
const lq = q.toLowerCase();
|
|
2749
|
+
entities = entities.filter((e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq));
|
|
2750
|
+
}
|
|
2751
|
+
const total = entities.length;
|
|
2752
|
+
const offset = parseInt(offsetStr || "0", 10);
|
|
2753
|
+
const limit = Math.min(parseInt(limitStr || "50", 10), 200);
|
|
2754
|
+
entities = entities.slice(offset, offset + limit);
|
|
2755
|
+
res.json({ entities, total });
|
|
2756
|
+
} catch (error) {
|
|
2757
|
+
console.error("Vault entities error:", error);
|
|
2758
|
+
res.json({ entities: [], total: 0 });
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
this.app.get("/api/soma/vault/entities/:type/:id", (req, res) => {
|
|
2762
|
+
const somaVault = this.config.somaVault;
|
|
2763
|
+
if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
|
|
2764
|
+
try {
|
|
2765
|
+
const reportPath = path2.join(somaVault, "..", "soma-report.json");
|
|
2766
|
+
const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
|
|
2767
|
+
const { type, id } = req.params;
|
|
2768
|
+
let entity = null;
|
|
2769
|
+
if (type === "agent") {
|
|
2770
|
+
entity = (report.agents ?? []).find((a) => a.name === id);
|
|
2771
|
+
} else if (type === "policy") {
|
|
2772
|
+
entity = (report.policies ?? []).find((p) => p.name === id);
|
|
2773
|
+
} else {
|
|
2774
|
+
entity = (report.insights ?? []).find(
|
|
2775
|
+
(i) => {
|
|
2776
|
+
var _a;
|
|
2777
|
+
return (((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || "") === id || i.title === id;
|
|
2778
|
+
}
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
if (!entity) return res.status(404).json({ error: "Entity not found" });
|
|
2782
|
+
res.json({
|
|
2783
|
+
...entity,
|
|
2784
|
+
type,
|
|
2785
|
+
id,
|
|
2786
|
+
body: entity.claim || entity.conditions || "",
|
|
2787
|
+
tags: entity.tags ?? [],
|
|
2788
|
+
related: entity.related ?? []
|
|
2789
|
+
});
|
|
2790
|
+
} catch {
|
|
2791
|
+
res.status(404).json({ error: "Entity not found" });
|
|
2792
|
+
}
|
|
2793
|
+
});
|
|
2397
2794
|
this.app.get("/api/process-health", (_req, res) => {
|
|
2398
2795
|
var _a, _b;
|
|
2399
2796
|
try {
|
|
@@ -2406,7 +2803,14 @@ var DashboardServer = class {
|
|
|
2406
2803
|
path2.dirname(this.config.tracesDir),
|
|
2407
2804
|
...this.config.dataDirs || []
|
|
2408
2805
|
];
|
|
2409
|
-
|
|
2806
|
+
let configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
|
|
2807
|
+
const pref = getProcessPreference(this.userConfig);
|
|
2808
|
+
if (pref) {
|
|
2809
|
+
const hasPreferred = configs.some((c) => c.processName === pref.prefer);
|
|
2810
|
+
if (hasPreferred) {
|
|
2811
|
+
configs = configs.filter((c) => c.processName !== pref.over);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2410
2814
|
if (configs.length === 0) {
|
|
2411
2815
|
return res.json(null);
|
|
2412
2816
|
}
|
|
@@ -2490,35 +2894,32 @@ var DashboardServer = class {
|
|
|
2490
2894
|
}
|
|
2491
2895
|
} catch {
|
|
2492
2896
|
}
|
|
2493
|
-
const watched = [
|
|
2897
|
+
const watched = [...new Set([
|
|
2494
2898
|
this.config.tracesDir,
|
|
2495
2899
|
...this.config.dataDirs || [],
|
|
2496
2900
|
...extraDirs
|
|
2497
|
-
];
|
|
2901
|
+
].map((w) => path2.resolve(w)))];
|
|
2498
2902
|
const discovered = [];
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
const
|
|
2509
|
-
if (
|
|
2903
|
+
const svcNames = getSystemdServices(this.userConfig);
|
|
2904
|
+
if (svcNames.length > 0) {
|
|
2905
|
+
try {
|
|
2906
|
+
const { execSync: execSync2 } = require("child_process");
|
|
2907
|
+
const raw = execSync2(
|
|
2908
|
+
`systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
|
|
2909
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
2910
|
+
);
|
|
2911
|
+
for (const line of raw.split("\n")) {
|
|
2912
|
+
const match = line.match(/path=([^\s;]+)/);
|
|
2913
|
+
if (match == null ? void 0 : match[1]) {
|
|
2914
|
+
const dir = path2.dirname(match[1]);
|
|
2915
|
+
if (fs2.existsSync(dir)) discovered.push(dir);
|
|
2916
|
+
}
|
|
2510
2917
|
}
|
|
2918
|
+
} catch {
|
|
2511
2919
|
}
|
|
2512
|
-
} catch {
|
|
2513
2920
|
}
|
|
2514
2921
|
const commonPaths = [
|
|
2515
|
-
|
|
2516
|
-
path2.join(home, ".alfred/data"),
|
|
2517
|
-
path2.join(home, ".openclaw/workspace/traces"),
|
|
2518
|
-
path2.join(home, ".openclaw/subagents"),
|
|
2519
|
-
path2.join(home, ".openclaw/cron/runs"),
|
|
2520
|
-
path2.join(home, ".openclaw/cron"),
|
|
2521
|
-
path2.join(home, ".openclaw/agents/main/sessions"),
|
|
2922
|
+
...getDiscoveryPaths(this.userConfig),
|
|
2522
2923
|
path2.join(home, ".agentflow/traces")
|
|
2523
2924
|
];
|
|
2524
2925
|
for (const p of commonPaths) {
|
|
@@ -2622,18 +3023,10 @@ var DashboardServer = class {
|
|
|
2622
3023
|
this.app.get("/ready", (_req, res) => {
|
|
2623
3024
|
res.json({ status: "ready" });
|
|
2624
3025
|
});
|
|
2625
|
-
this.app.get("/v1/*", (_req, res) => {
|
|
2626
|
-
const legacyIndex = path2.join(__dirname, "../public/index.html");
|
|
2627
|
-
if (fs2.existsSync(legacyIndex)) {
|
|
2628
|
-
res.sendFile(legacyIndex);
|
|
2629
|
-
} else {
|
|
2630
|
-
res.status(404).send("Legacy dashboard not found");
|
|
2631
|
-
}
|
|
2632
|
-
});
|
|
2633
3026
|
this.app.get("*", (_req, res) => {
|
|
2634
|
-
const
|
|
2635
|
-
if (fs2.existsSync(
|
|
2636
|
-
res.sendFile(
|
|
3027
|
+
const clientIndex2 = path2.join(__dirname, "../dist/client/index.html");
|
|
3028
|
+
if (fs2.existsSync(clientIndex2)) {
|
|
3029
|
+
res.sendFile(clientIndex2);
|
|
2637
3030
|
} else {
|
|
2638
3031
|
res.status(404).send("Dashboard not found - public files may not be built");
|
|
2639
3032
|
}
|
|
@@ -2659,6 +3052,41 @@ var DashboardServer = class {
|
|
|
2659
3052
|
});
|
|
2660
3053
|
});
|
|
2661
3054
|
}
|
|
3055
|
+
/** Watch soma-report.json for changes and broadcast updates via WebSocket. */
|
|
3056
|
+
setupSomaReportWatcher() {
|
|
3057
|
+
const somaVault = this.config.somaVault;
|
|
3058
|
+
if (!somaVault) return;
|
|
3059
|
+
const reportPath = path2.join(somaVault, "..", "soma-report.json");
|
|
3060
|
+
const reportDir = path2.dirname(reportPath);
|
|
3061
|
+
if (!fs2.existsSync(reportDir)) return;
|
|
3062
|
+
let debounceTimer = null;
|
|
3063
|
+
const watcher = import_chokidar2.default.watch(reportPath, {
|
|
3064
|
+
ignoreInitial: true,
|
|
3065
|
+
persistent: true,
|
|
3066
|
+
awaitWriteFinish: { stabilityThreshold: 500 }
|
|
3067
|
+
});
|
|
3068
|
+
watcher.on("change", () => {
|
|
3069
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3070
|
+
debounceTimer = setTimeout(() => {
|
|
3071
|
+
var _a, _b;
|
|
3072
|
+
try {
|
|
3073
|
+
const report = JSON.parse(fs2.readFileSync(reportPath, "utf-8"));
|
|
3074
|
+
this.broadcast({ type: "soma-report-updated", data: report });
|
|
3075
|
+
if (report.generatedAt) {
|
|
3076
|
+
this.broadcast({
|
|
3077
|
+
type: "soma-activity",
|
|
3078
|
+
data: {
|
|
3079
|
+
action: "report-updated",
|
|
3080
|
+
description: `Report updated: ${((_a = report.totals) == null ? void 0 : _a.agents) ?? 0} agents, ${((_b = report.totals) == null ? void 0 : _b.insights) ?? 0} insights`,
|
|
3081
|
+
timestamp: report.generatedAt
|
|
3082
|
+
}
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
} catch {
|
|
3086
|
+
}
|
|
3087
|
+
}, 500);
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
2662
3090
|
/**
|
|
2663
3091
|
* Filter an agent's traces to valid ExecutionGraphs and convert via loadGraph().
|
|
2664
3092
|
* Returns only traces with proper nodes (Map or non-empty object), skipping session-only traces.
|
|
@@ -2880,24 +3308,49 @@ var DashboardServer = class {
|
|
|
2880
3308
|
});
|
|
2881
3309
|
}
|
|
2882
3310
|
async start() {
|
|
2883
|
-
return new Promise((
|
|
3311
|
+
return new Promise((resolve5) => {
|
|
2884
3312
|
const host = this.config.host || "localhost";
|
|
2885
3313
|
this.server.listen(this.config.port, host, () => {
|
|
2886
3314
|
console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
|
|
2887
3315
|
console.log(`Watching traces in: ${this.config.tracesDir}`);
|
|
2888
|
-
|
|
3316
|
+
resolve5();
|
|
2889
3317
|
});
|
|
2890
3318
|
});
|
|
2891
3319
|
}
|
|
3320
|
+
/** Check if any src/client file is newer than the built bundle. */
|
|
3321
|
+
isClientStale(srcDir, distDir) {
|
|
3322
|
+
try {
|
|
3323
|
+
const distIndex = path2.join(distDir, "index.html");
|
|
3324
|
+
if (!fs2.existsSync(distIndex)) return true;
|
|
3325
|
+
const distMtime = fs2.statSync(distIndex).mtimeMs;
|
|
3326
|
+
const check = (dir) => {
|
|
3327
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
3328
|
+
const full = path2.join(dir, entry.name);
|
|
3329
|
+
if (entry.isDirectory()) {
|
|
3330
|
+
if (check(full)) return true;
|
|
3331
|
+
} else if (fs2.statSync(full).mtimeMs > distMtime) {
|
|
3332
|
+
return true;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
return false;
|
|
3336
|
+
};
|
|
3337
|
+
return check(srcDir);
|
|
3338
|
+
} catch {
|
|
3339
|
+
return false;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
2892
3342
|
async stop() {
|
|
2893
|
-
return new Promise((
|
|
3343
|
+
return new Promise((resolve5) => {
|
|
2894
3344
|
this.watcher.stop();
|
|
2895
3345
|
this.server.close(() => {
|
|
2896
3346
|
console.log("Dashboard server stopped");
|
|
2897
|
-
|
|
3347
|
+
resolve5();
|
|
2898
3348
|
});
|
|
2899
3349
|
});
|
|
2900
3350
|
}
|
|
3351
|
+
getConfigPath() {
|
|
3352
|
+
return this.configPath;
|
|
3353
|
+
}
|
|
2901
3354
|
getStats() {
|
|
2902
3355
|
return this.stats.getGlobalStats();
|
|
2903
3356
|
}
|
|
@@ -2910,7 +3363,7 @@ if (import_meta.url === `file://${process.argv[1]}`) {
|
|
|
2910
3363
|
}
|
|
2911
3364
|
|
|
2912
3365
|
// src/cli.ts
|
|
2913
|
-
var VERSION = "0.
|
|
3366
|
+
var VERSION = "0.8.0";
|
|
2914
3367
|
function getLanAddress() {
|
|
2915
3368
|
const interfaces = os.networkInterfaces();
|
|
2916
3369
|
for (const name of Object.keys(interfaces)) {
|
|
@@ -2922,7 +3375,7 @@ function getLanAddress() {
|
|
|
2922
3375
|
}
|
|
2923
3376
|
return null;
|
|
2924
3377
|
}
|
|
2925
|
-
function printBanner(config, traceCount, stats) {
|
|
3378
|
+
function printBanner(config, traceCount, stats, configPath) {
|
|
2926
3379
|
var _a;
|
|
2927
3380
|
const lan = getLanAddress();
|
|
2928
3381
|
const host = config.host || "localhost";
|
|
@@ -2938,26 +3391,25 @@ function printBanner(config, traceCount, stats) {
|
|
|
2938
3391
|
|
|
2939
3392
|
See your agents think.
|
|
2940
3393
|
|
|
2941
|
-
\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
|
|
2942
|
-
\u2502 \u{1F916} Agents \u2502 TRACE FILES \u2502 \u{1F4CA} AgentFlow \u2502 SHOWS YOU \u2502 \u{1F310} Your browser \u2502
|
|
2943
|
-
\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
|
|
2944
|
-
\u2502 write JSON \u2502 \u2502 builds graphs, \u2502 \u2502 graph, timeline, \u2502
|
|
2945
|
-
\u2502 trace files. \u2502 \u2502 serves dashboard.\u2502 \u2502 metrics, health. \u2502
|
|
2946
|
-
\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
|
|
2947
|
-
|
|
2948
|
-
Runs locally. Your data never leaves your machine.
|
|
2949
|
-
|
|
2950
|
-
Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
|
|
2951
|
-
|
|
2952
3394
|
Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? `
|
|
2953
3395
|
Data dirs: ${config.dataDirs.join("\n ")}` : ""}
|
|
2954
3396
|
Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
|
|
2955
3397
|
Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
|
|
3398
|
+
Config: ${configPath ?? "none (using defaults)"}
|
|
2956
3399
|
CORS: ${config.enableCors ? "enabled" : "disabled"}
|
|
2957
3400
|
WebSocket: live updates enabled
|
|
3401
|
+
Window: ${process.env.AGENTFLOW_TRACE_WINDOW_HOURS ?? "48"}h (set AGENTFLOW_TRACE_WINDOW_HOURS to change)
|
|
2958
3402
|
|
|
2959
3403
|
\u2192 http://localhost:${port}${isPublic && lan ? `
|
|
2960
3404
|
\u2192 http://${lan}:${port} (LAN)` : ""}
|
|
3405
|
+
|
|
3406
|
+
Pages: Agents \xB7 SOMA
|
|
3407
|
+
Agent: Profile \xB7 Execution Detail
|
|
3408
|
+
SOMA: Intelligence \xB7 Review \xB7 Policies \xB7 Knowledge \xB7 Activity
|
|
3409
|
+
Tabs: Flame Chart \xB7 Agent Flow \xB7 Metrics \xB7 Dependencies
|
|
3410
|
+
State Machine \xB7 Summary \xB7 Transcript
|
|
3411
|
+
|
|
3412
|
+
Runs locally. Your data never leaves your machine.
|
|
2961
3413
|
`);
|
|
2962
3414
|
}
|
|
2963
3415
|
async function startDashboard() {
|
|
@@ -2995,6 +3447,12 @@ async function startDashboard() {
|
|
|
2995
3447
|
case "--collector-token":
|
|
2996
3448
|
config.collectorAuthToken = args[++i];
|
|
2997
3449
|
break;
|
|
3450
|
+
case "--soma-vault":
|
|
3451
|
+
config.somaVault = args[++i];
|
|
3452
|
+
break;
|
|
3453
|
+
case "--config":
|
|
3454
|
+
config.configPath = args[++i];
|
|
3455
|
+
break;
|
|
2998
3456
|
case "--help":
|
|
2999
3457
|
printHelp();
|
|
3000
3458
|
process.exit(0);
|
|
@@ -3006,6 +3464,9 @@ async function startDashboard() {
|
|
|
3006
3464
|
if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
|
|
3007
3465
|
config.enableCollector = false;
|
|
3008
3466
|
}
|
|
3467
|
+
if (!config.somaVault && process.env.SOMA_VAULT) {
|
|
3468
|
+
config.somaVault = process.env.SOMA_VAULT;
|
|
3469
|
+
}
|
|
3009
3470
|
const tracesPath = path3.resolve(config.tracesDir);
|
|
3010
3471
|
if (!fs3.existsSync(tracesPath)) {
|
|
3011
3472
|
fs3.mkdirSync(tracesPath, { recursive: true });
|
|
@@ -3027,7 +3488,7 @@ async function startDashboard() {
|
|
|
3027
3488
|
setTimeout(() => {
|
|
3028
3489
|
const stats = dashboard.getStats();
|
|
3029
3490
|
const traces = dashboard.getTraces();
|
|
3030
|
-
printBanner(config, traces.length, stats);
|
|
3491
|
+
printBanner(config, traces.length, stats, dashboard.getConfigPath());
|
|
3031
3492
|
}, 1500);
|
|
3032
3493
|
} catch (error) {
|
|
3033
3494
|
console.error("\u274C Failed to start dashboard:", error);
|
|
@@ -3036,7 +3497,7 @@ async function startDashboard() {
|
|
|
3036
3497
|
}
|
|
3037
3498
|
function printHelp() {
|
|
3038
3499
|
console.log(`
|
|
3039
|
-
|
|
3500
|
+
AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
|
|
3040
3501
|
|
|
3041
3502
|
Usage:
|
|
3042
3503
|
agentflow-dashboard [options]
|
|
@@ -3047,22 +3508,34 @@ Options:
|
|
|
3047
3508
|
-t, --traces <path> Traces directory (default: ./traces)
|
|
3048
3509
|
-h, --host <address> Host address (default: localhost)
|
|
3049
3510
|
--data-dir <path> Extra data directory for process discovery (repeatable)
|
|
3511
|
+
--config <path> Path to agentflow.config.json (aliases, skip files, etc.)
|
|
3512
|
+
--soma-vault <path> SOMA vault directory for intelligence data
|
|
3050
3513
|
--cors Enable CORS headers
|
|
3051
3514
|
--no-collector Disable OTLP trace collector (POST /v1/traces)
|
|
3052
3515
|
--collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
|
|
3053
3516
|
--help Show this help message
|
|
3054
3517
|
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3518
|
+
Config file:
|
|
3519
|
+
The dashboard loads agentflow.config.json for agent aliases, skip files,
|
|
3520
|
+
discovery paths, and systemd services. Resolution order:
|
|
3521
|
+
1. --config flag
|
|
3522
|
+
2. AGENTFLOW_CONFIG env var
|
|
3523
|
+
3. ./agentflow.config.json
|
|
3524
|
+
4. ~/.config/agentflow/config.json
|
|
3525
|
+
|
|
3526
|
+
See agentflow.config.example.json for a complete reference.
|
|
3059
3527
|
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3528
|
+
Environment:
|
|
3529
|
+
AGENTFLOW_CONFIG Path to config file
|
|
3530
|
+
AGENTFLOW_TRACE_WINDOW_HOURS Max age of traces to load (default: 48)
|
|
3531
|
+
AGENTFLOW_COLLECTOR_TOKEN Auth token for OTLP collector
|
|
3532
|
+
AGENTFLOW_NO_COLLECTOR=true Disable OTLP collector
|
|
3533
|
+
SOMA_VAULT SOMA vault directory
|
|
3534
|
+
|
|
3535
|
+
Examples:
|
|
3536
|
+
agentflow-dashboard --traces ./traces --host 0.0.0.0
|
|
3537
|
+
agentflow-dashboard --traces ./traces --config ./agentflow.config.json
|
|
3538
|
+
agentflow-dashboard -p 8080 -t /var/log/agentflow --cors
|
|
3066
3539
|
`);
|
|
3067
3540
|
}
|
|
3068
3541
|
// Annotate the CommonJS export names for ESM import in node:
|