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
|
@@ -6,10 +6,84 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
// src/server.ts
|
|
9
|
+
import { execSync } from "child_process";
|
|
9
10
|
import * as fs3 from "fs";
|
|
10
11
|
import { createServer } from "http";
|
|
11
12
|
import * as path3 from "path";
|
|
12
13
|
import { fileURLToPath } from "url";
|
|
14
|
+
|
|
15
|
+
// src/config.ts
|
|
16
|
+
import { existsSync, readFileSync } from "fs";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import { join, resolve } from "path";
|
|
19
|
+
var EMPTY_CONFIG = {};
|
|
20
|
+
function expandTilde(p) {
|
|
21
|
+
if (p.startsWith("~/") || p === "~") {
|
|
22
|
+
return join(homedir(), p.slice(1));
|
|
23
|
+
}
|
|
24
|
+
return p;
|
|
25
|
+
}
|
|
26
|
+
function loadConfig(explicitPath) {
|
|
27
|
+
const candidates = [];
|
|
28
|
+
if (explicitPath) {
|
|
29
|
+
candidates.push(resolve(explicitPath));
|
|
30
|
+
}
|
|
31
|
+
if (process.env.AGENTFLOW_CONFIG) {
|
|
32
|
+
candidates.push(resolve(process.env.AGENTFLOW_CONFIG));
|
|
33
|
+
}
|
|
34
|
+
candidates.push(resolve("agentflow.config.json"));
|
|
35
|
+
candidates.push(join(homedir(), ".config", "agentflow", "config.json"));
|
|
36
|
+
for (const candidate of candidates) {
|
|
37
|
+
if (!existsSync(candidate)) continue;
|
|
38
|
+
try {
|
|
39
|
+
const raw = readFileSync(candidate, "utf-8");
|
|
40
|
+
const parsed = JSON.parse(raw);
|
|
41
|
+
const cleaned = stripCommentKeys(parsed);
|
|
42
|
+
console.log(`Loaded config: ${candidate}`);
|
|
43
|
+
return { config: cleaned, configPath: candidate };
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.warn(`Warning: Failed to load config from ${candidate}: ${err.message}`);
|
|
46
|
+
console.warn("Continuing with empty defaults.");
|
|
47
|
+
return { config: EMPTY_CONFIG, configPath: null };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { config: EMPTY_CONFIG, configPath: null };
|
|
51
|
+
}
|
|
52
|
+
function stripCommentKeys(obj) {
|
|
53
|
+
if (Array.isArray(obj)) return obj.map(stripCommentKeys);
|
|
54
|
+
if (obj && typeof obj === "object") {
|
|
55
|
+
const result = {};
|
|
56
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
57
|
+
if (key.startsWith("//")) continue;
|
|
58
|
+
result[key] = stripCommentKeys(value);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
return obj;
|
|
63
|
+
}
|
|
64
|
+
function getAliases(config) {
|
|
65
|
+
return config.aliases ?? {};
|
|
66
|
+
}
|
|
67
|
+
function getSkipFiles(config) {
|
|
68
|
+
return config.skipFiles ?? [];
|
|
69
|
+
}
|
|
70
|
+
function getSkipDirectories(config) {
|
|
71
|
+
return config.skipDirectories ?? [];
|
|
72
|
+
}
|
|
73
|
+
function getDiscoveryPaths(config) {
|
|
74
|
+
return (config.discoveryPaths ?? []).map(expandTilde);
|
|
75
|
+
}
|
|
76
|
+
function getSystemdServices(config) {
|
|
77
|
+
return config.systemdServices ?? [];
|
|
78
|
+
}
|
|
79
|
+
function getAgentDetection(config) {
|
|
80
|
+
return config.agentDetection ?? {};
|
|
81
|
+
}
|
|
82
|
+
function getProcessPreference(config) {
|
|
83
|
+
return config.processPreference ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/server.ts
|
|
13
87
|
import {
|
|
14
88
|
auditProcesses,
|
|
15
89
|
createExecutionEvent,
|
|
@@ -52,17 +126,17 @@ var AgentFlowAdapter = class {
|
|
|
52
126
|
};
|
|
53
127
|
|
|
54
128
|
// src/adapters/openclaw.ts
|
|
55
|
-
import { existsSync, readFileSync } from "fs";
|
|
56
|
-
import { basename, dirname, join } from "path";
|
|
129
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
130
|
+
import { basename, dirname, join as join2 } from "path";
|
|
57
131
|
var jobCache = /* @__PURE__ */ new Map();
|
|
58
132
|
function loadJobs(openclawDir) {
|
|
59
133
|
const cached = jobCache.get(openclawDir);
|
|
60
134
|
if (cached) return cached;
|
|
61
|
-
const jobsPath =
|
|
135
|
+
const jobsPath = join2(openclawDir, "cron", "jobs.json");
|
|
62
136
|
const map = /* @__PURE__ */ new Map();
|
|
63
137
|
try {
|
|
64
|
-
if (
|
|
65
|
-
const data = JSON.parse(
|
|
138
|
+
if (existsSync2(jobsPath)) {
|
|
139
|
+
const data = JSON.parse(readFileSync2(jobsPath, "utf-8"));
|
|
66
140
|
const jobs = Array.isArray(data) ? data : data.jobs ?? [];
|
|
67
141
|
for (const job of jobs) {
|
|
68
142
|
if (job.id) map.set(job.id, job);
|
|
@@ -76,7 +150,7 @@ function loadJobs(openclawDir) {
|
|
|
76
150
|
function findOpenClawRoot(filePath) {
|
|
77
151
|
let dir = dirname(filePath);
|
|
78
152
|
for (let i = 0; i < 5; i++) {
|
|
79
|
-
if (
|
|
153
|
+
if (existsSync2(join2(dir, "cron", "jobs.json")) || basename(dir) === ".openclaw") {
|
|
80
154
|
return dir;
|
|
81
155
|
}
|
|
82
156
|
dir = dirname(dir);
|
|
@@ -86,7 +160,7 @@ function findOpenClawRoot(filePath) {
|
|
|
86
160
|
var OpenClawAdapter = class {
|
|
87
161
|
name = "openclaw";
|
|
88
162
|
detect(dirPath) {
|
|
89
|
-
return
|
|
163
|
+
return existsSync2(join2(dirPath, "cron", "jobs.json")) || dirPath.includes(".openclaw") || existsSync2(join2(dirPath, "cron", "runs"));
|
|
90
164
|
}
|
|
91
165
|
canHandle(filePath) {
|
|
92
166
|
if (!filePath.endsWith(".jsonl")) return false;
|
|
@@ -95,7 +169,7 @@ var OpenClawAdapter = class {
|
|
|
95
169
|
parse(filePath) {
|
|
96
170
|
const traces = [];
|
|
97
171
|
try {
|
|
98
|
-
const content =
|
|
172
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
99
173
|
const root = findOpenClawRoot(filePath);
|
|
100
174
|
const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
|
|
101
175
|
for (const line of content.split("\n")) {
|
|
@@ -159,8 +233,8 @@ var OpenClawAdapter = class {
|
|
|
159
233
|
};
|
|
160
234
|
|
|
161
235
|
// src/adapters/otel.ts
|
|
162
|
-
import { existsSync as
|
|
163
|
-
import { join as
|
|
236
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
237
|
+
import { join as join3 } from "path";
|
|
164
238
|
var SPAN_TYPE_MAP = {
|
|
165
239
|
"gen_ai.chat": "llm",
|
|
166
240
|
"gen_ai.completion": "llm",
|
|
@@ -267,7 +341,7 @@ var OTelAdapter = class {
|
|
|
267
341
|
name = "otel";
|
|
268
342
|
detect(dirPath) {
|
|
269
343
|
try {
|
|
270
|
-
if (
|
|
344
|
+
if (existsSync3(join3(dirPath, "otel-traces"))) return true;
|
|
271
345
|
const files = readdirSync(dirPath);
|
|
272
346
|
return files.some((f) => f.endsWith(".otlp.json"));
|
|
273
347
|
} catch {
|
|
@@ -279,7 +353,7 @@ var OTelAdapter = class {
|
|
|
279
353
|
}
|
|
280
354
|
parse(filePath) {
|
|
281
355
|
try {
|
|
282
|
-
const content =
|
|
356
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
283
357
|
const payload = JSON.parse(content);
|
|
284
358
|
const traces = parseOtlpPayload(payload);
|
|
285
359
|
for (const t of traces) t.filePath = filePath;
|
|
@@ -321,9 +395,7 @@ function extractSource(agentId) {
|
|
|
321
395
|
const colonIdx = agentId.indexOf(":");
|
|
322
396
|
if (colonIdx > 0 && colonIdx < 20) {
|
|
323
397
|
const prefix = agentId.slice(0, colonIdx);
|
|
324
|
-
|
|
325
|
-
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
326
|
-
}
|
|
398
|
+
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
327
399
|
}
|
|
328
400
|
return { source: "agentflow", localId: agentId };
|
|
329
401
|
}
|
|
@@ -354,16 +426,20 @@ function deduplicateAgents(agents) {
|
|
|
354
426
|
for (const a of tagged) {
|
|
355
427
|
const suffix = extractSuffix(a.localId);
|
|
356
428
|
if (!suffix) continue;
|
|
357
|
-
const
|
|
429
|
+
const key = `${a.source}:${suffix}`;
|
|
430
|
+
const group = suffixGroups.get(key) ?? [];
|
|
358
431
|
group.push(a);
|
|
359
|
-
suffixGroups.set(
|
|
432
|
+
suffixGroups.set(key, group);
|
|
360
433
|
}
|
|
361
434
|
const mergedIds = /* @__PURE__ */ new Set();
|
|
362
435
|
const mergedAgents = [];
|
|
363
|
-
for (const [
|
|
436
|
+
for (const [_key, group] of suffixGroups) {
|
|
437
|
+
const suffix = extractSuffix(group[0].localId);
|
|
364
438
|
if (group.length < 2) continue;
|
|
365
439
|
const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
|
|
366
440
|
if (prefixes.size < 2) continue;
|
|
441
|
+
const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
|
|
442
|
+
if (longPrefixes.length >= 2) continue;
|
|
367
443
|
const merged = {
|
|
368
444
|
agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
|
|
369
445
|
displayName: suffix,
|
|
@@ -411,10 +487,7 @@ function groupAgents(agents) {
|
|
|
411
487
|
}
|
|
412
488
|
const SOURCE_DISPLAY = {
|
|
413
489
|
agentflow: "AgentFlow",
|
|
414
|
-
|
|
415
|
-
otel: "OpenTelemetry",
|
|
416
|
-
langchain: "LangChain",
|
|
417
|
-
crewai: "CrewAI"
|
|
490
|
+
otel: "OpenTelemetry"
|
|
418
491
|
};
|
|
419
492
|
const groups = [];
|
|
420
493
|
for (const [source, sourceAgents] of sourceMap) {
|
|
@@ -760,10 +833,6 @@ function getUniversalNodeStatus(activity) {
|
|
|
760
833
|
return "completed";
|
|
761
834
|
}
|
|
762
835
|
function openClawSessionIdToAgent(sessionId) {
|
|
763
|
-
if (sessionId.startsWith("janitor-")) return "vault-janitor";
|
|
764
|
-
if (sessionId.startsWith("curator-")) return "vault-curator";
|
|
765
|
-
if (sessionId.startsWith("distiller-")) return "vault-distiller";
|
|
766
|
-
if (sessionId.startsWith("main-")) return "alfred-main";
|
|
767
836
|
const firstSegment = sessionId.split("-")[0];
|
|
768
837
|
if (firstSegment) return firstSegment;
|
|
769
838
|
return "openclaw";
|
|
@@ -776,19 +845,81 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
776
845
|
tracesDir;
|
|
777
846
|
dataDirs;
|
|
778
847
|
allWatchDirs;
|
|
848
|
+
maxAgeMs;
|
|
849
|
+
userConfig;
|
|
779
850
|
constructor(tracesDirOrOptions) {
|
|
780
851
|
super();
|
|
852
|
+
const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
|
|
853
|
+
const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
|
|
854
|
+
const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
|
|
781
855
|
if (typeof tracesDirOrOptions === "string") {
|
|
782
856
|
this.tracesDir = path.resolve(tracesDirOrOptions);
|
|
783
857
|
this.dataDirs = [];
|
|
858
|
+
this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
|
|
859
|
+
this.userConfig = {};
|
|
784
860
|
} else {
|
|
785
861
|
this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
|
|
786
862
|
this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
|
|
787
|
-
|
|
863
|
+
this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
|
|
864
|
+
this.userConfig = tracesDirOrOptions.userConfig ?? {};
|
|
865
|
+
}
|
|
866
|
+
this.skipFiles = /* @__PURE__ */ new Set([
|
|
867
|
+
..._TraceWatcher.STRUCTURAL_SKIP_FILES,
|
|
868
|
+
...getSkipFiles(this.userConfig)
|
|
869
|
+
]);
|
|
870
|
+
this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
|
|
788
871
|
this.allWatchDirs = [this.tracesDir, ...this.dataDirs];
|
|
789
872
|
this.ensureTracesDir();
|
|
790
873
|
this.loadExistingFiles();
|
|
874
|
+
this.archiveOldTraces();
|
|
791
875
|
this.startWatching();
|
|
876
|
+
setInterval(() => this.archiveOldTraces(), 6 * 60 * 60 * 1e3);
|
|
877
|
+
}
|
|
878
|
+
/** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
|
|
879
|
+
archiveOldTraces() {
|
|
880
|
+
const cutoff = Date.now() - this.maxAgeMs;
|
|
881
|
+
let archived = 0;
|
|
882
|
+
for (const dir of this.allWatchDirs) {
|
|
883
|
+
if (!fs.existsSync(dir)) continue;
|
|
884
|
+
try {
|
|
885
|
+
this.archiveDirectory(dir, cutoff, 0);
|
|
886
|
+
} catch (error) {
|
|
887
|
+
console.warn(`Archival error in ${dir}:`, error.message);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
archiveDirectory(dir, cutoff, depth) {
|
|
892
|
+
if (depth > 10) return 0;
|
|
893
|
+
if (path.basename(dir) === "archive") return 0;
|
|
894
|
+
let archived = 0;
|
|
895
|
+
try {
|
|
896
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
897
|
+
for (const entry of entries) {
|
|
898
|
+
if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
|
|
899
|
+
const fullPath = path.join(dir, entry.name);
|
|
900
|
+
if (entry.isDirectory()) {
|
|
901
|
+
archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
|
|
905
|
+
try {
|
|
906
|
+
const stats = fs.statSync(fullPath);
|
|
907
|
+
if (stats.mtimeMs >= cutoff) continue;
|
|
908
|
+
const mtime = new Date(stats.mtimeMs);
|
|
909
|
+
const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
|
|
910
|
+
const archiveDir = path.join(this.tracesDir, "archive", yearMonth);
|
|
911
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
912
|
+
const dest = path.join(archiveDir, entry.name);
|
|
913
|
+
fs.renameSync(fullPath, dest);
|
|
914
|
+
const key = this.traceKey(fullPath);
|
|
915
|
+
this.traces.delete(key);
|
|
916
|
+
archived++;
|
|
917
|
+
} catch {
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
} catch {
|
|
921
|
+
}
|
|
922
|
+
return archived;
|
|
792
923
|
}
|
|
793
924
|
ensureTracesDir() {
|
|
794
925
|
if (!fs.existsSync(this.tracesDir)) {
|
|
@@ -821,9 +952,17 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
821
952
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
822
953
|
for (const entry of entries) {
|
|
823
954
|
if (entry.name.startsWith(".")) continue;
|
|
955
|
+
if (entry.name === "archive") continue;
|
|
956
|
+
if (this.userSkipDirs.has(entry.name)) continue;
|
|
824
957
|
const fullPath = path.join(dir, entry.name);
|
|
825
958
|
if (entry.isFile()) {
|
|
826
959
|
if (this.isSupportedFile(entry.name)) {
|
|
960
|
+
try {
|
|
961
|
+
const mtime = fs.statSync(fullPath).mtimeMs;
|
|
962
|
+
if (Date.now() - mtime > this.maxAgeMs) continue;
|
|
963
|
+
} catch {
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
827
966
|
if (this.loadFile(fullPath)) {
|
|
828
967
|
fileCount++;
|
|
829
968
|
}
|
|
@@ -841,8 +980,8 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
841
980
|
isSupportedFile(filename) {
|
|
842
981
|
return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
|
|
843
982
|
}
|
|
844
|
-
/**
|
|
845
|
-
static
|
|
983
|
+
/** Structural file names that are never trace data — always skipped. */
|
|
984
|
+
static STRUCTURAL_SKIP_FILES = /* @__PURE__ */ new Set([
|
|
846
985
|
"workers.json",
|
|
847
986
|
"package.json",
|
|
848
987
|
"package-lock.json",
|
|
@@ -857,6 +996,10 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
857
996
|
"update-check.json",
|
|
858
997
|
"exec-approvals.json"
|
|
859
998
|
]);
|
|
999
|
+
/** Skip files = structural + user config */
|
|
1000
|
+
skipFiles;
|
|
1001
|
+
/** Skip directories from user config */
|
|
1002
|
+
userSkipDirs;
|
|
860
1003
|
static SKIP_SUFFIXES = [
|
|
861
1004
|
"-state.json",
|
|
862
1005
|
"-config.json",
|
|
@@ -868,7 +1011,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
868
1011
|
/** Load a file using the adapter registry, falling back to built-in parsing. */
|
|
869
1012
|
loadFile(filePath) {
|
|
870
1013
|
const filename = path.basename(filePath);
|
|
871
|
-
if (
|
|
1014
|
+
if (this.skipFiles.has(filename)) return false;
|
|
872
1015
|
if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
|
|
873
1016
|
const adapter = findAdapter(filePath);
|
|
874
1017
|
if (adapter && adapter.name !== "agentflow") {
|
|
@@ -1045,43 +1188,26 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1045
1188
|
}
|
|
1046
1189
|
return traces;
|
|
1047
1190
|
}
|
|
1048
|
-
/**
|
|
1049
|
-
* Normalise agent identifiers so that the same worker is never shown
|
|
1050
|
-
* under two different names (e.g. "vault-curator" vs "openclaw-vault-curator").
|
|
1051
|
-
*
|
|
1052
|
-
* Canonical names: alfred-main, vault-curator, vault-janitor,
|
|
1053
|
-
* vault-distiller, vault-surveyor
|
|
1054
|
-
*/
|
|
1055
|
-
static AGENT_ALIASES = {
|
|
1056
|
-
"openclaw-main": "alfred-main",
|
|
1057
|
-
"openclaw-vault-curator": "vault-curator",
|
|
1058
|
-
"openclaw-vault-janitor": "vault-janitor",
|
|
1059
|
-
"openclaw-vault-distiller": "vault-distiller",
|
|
1060
|
-
"openclaw-vault-surveyor": "vault-surveyor",
|
|
1061
|
-
"alfred-curator": "vault-curator",
|
|
1062
|
-
"alfred-janitor": "vault-janitor",
|
|
1063
|
-
"alfred-distiller": "vault-distiller",
|
|
1064
|
-
"alfred-surveyor": "vault-surveyor",
|
|
1065
|
-
curator: "vault-curator",
|
|
1066
|
-
janitor: "vault-janitor",
|
|
1067
|
-
distiller: "vault-distiller",
|
|
1068
|
-
surveyor: "vault-surveyor"
|
|
1069
|
-
};
|
|
1191
|
+
/** Normalise agent identifiers using config-driven alias map. */
|
|
1070
1192
|
normaliseAgentId(raw) {
|
|
1071
|
-
|
|
1193
|
+
const aliases = getAliases(this.userConfig);
|
|
1194
|
+
return aliases[raw] ?? raw;
|
|
1072
1195
|
}
|
|
1073
1196
|
detectAgentIdentifier(activity, _filename, filePath) {
|
|
1074
1197
|
if (activity.agent_id) {
|
|
1075
|
-
|
|
1076
|
-
if (agentId === "main" && filePath.includes(".alfred/")) return this.normaliseAgentId("alfred-main");
|
|
1077
|
-
return this.normaliseAgentId(agentId);
|
|
1198
|
+
return this.normaliseAgentId(activity.agent_id);
|
|
1078
1199
|
}
|
|
1079
1200
|
const pathAgent = this.extractAgentFromPath(filePath);
|
|
1080
|
-
|
|
1201
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1202
|
+
if (detection.filePatterns) {
|
|
1081
1203
|
const basename3 = path.basename(filePath, path.extname(filePath));
|
|
1082
|
-
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1204
|
+
for (const [pattern, template] of Object.entries(detection.filePatterns)) {
|
|
1205
|
+
const re = new RegExp(`^(${pattern})$`);
|
|
1206
|
+
const match = basename3.match(re);
|
|
1207
|
+
if (match) {
|
|
1208
|
+
const resolved = template.replace("${match}", match[1]);
|
|
1209
|
+
return this.normaliseAgentId(resolved);
|
|
1210
|
+
}
|
|
1085
1211
|
}
|
|
1086
1212
|
}
|
|
1087
1213
|
return this.normaliseAgentId(pathAgent);
|
|
@@ -1089,20 +1215,23 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1089
1215
|
extractAgentFromPath(filePath) {
|
|
1090
1216
|
const filename = path.basename(filePath, path.extname(filePath));
|
|
1091
1217
|
const pathParts = filePath.split(path.sep);
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1218
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1219
|
+
let pathPrefix = "";
|
|
1220
|
+
if (detection.pathPatterns) {
|
|
1221
|
+
for (const [pathSubstring, agentId] of Object.entries(detection.pathPatterns)) {
|
|
1222
|
+
if (filePath.includes(pathSubstring)) {
|
|
1223
|
+
pathPrefix = agentId;
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1099
1226
|
}
|
|
1100
|
-
return "openclaw";
|
|
1101
1227
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1228
|
+
const agentsIndex = pathParts.lastIndexOf("agents");
|
|
1229
|
+
if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
|
|
1230
|
+
const agentName = pathParts[agentsIndex + 1];
|
|
1231
|
+
return pathPrefix ? `${pathPrefix}-${agentName}` : agentName;
|
|
1104
1232
|
}
|
|
1105
|
-
|
|
1233
|
+
if (pathPrefix) return pathPrefix;
|
|
1234
|
+
for (const part of [...pathParts].reverse()) {
|
|
1106
1235
|
if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
|
|
1107
1236
|
return part;
|
|
1108
1237
|
}
|
|
@@ -1437,19 +1566,22 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1437
1566
|
const parentDir = path.basename(path.dirname(filePath));
|
|
1438
1567
|
const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
|
|
1439
1568
|
const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
|
|
1440
|
-
let
|
|
1569
|
+
let agentName;
|
|
1441
1570
|
if (parentDir === "sessions" && greatGrandParentDir === "agents") {
|
|
1442
|
-
|
|
1571
|
+
agentName = grandParentDir;
|
|
1443
1572
|
} else if (grandParentDir === "agents") {
|
|
1444
|
-
|
|
1445
|
-
} else if (parentDir === "runs" && grandParentDir === "cron") {
|
|
1446
|
-
agentId = "openclaw-cron";
|
|
1573
|
+
agentName = parentDir;
|
|
1447
1574
|
} else {
|
|
1448
|
-
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1575
|
+
agentName = parentDir;
|
|
1576
|
+
}
|
|
1577
|
+
let agentId = agentName;
|
|
1578
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1579
|
+
if (detection.pathPatterns) {
|
|
1580
|
+
for (const [pathSubstring, prefix] of Object.entries(detection.pathPatterns)) {
|
|
1581
|
+
if (filePath.includes(pathSubstring)) {
|
|
1582
|
+
agentId = `${prefix}-${agentName}`;
|
|
1583
|
+
break;
|
|
1584
|
+
}
|
|
1453
1585
|
}
|
|
1454
1586
|
}
|
|
1455
1587
|
const modelEvent = rawEvents.find((e) => e.type === "model_change");
|
|
@@ -1738,6 +1870,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1738
1870
|
edges: [],
|
|
1739
1871
|
events: [],
|
|
1740
1872
|
startTime,
|
|
1873
|
+
status,
|
|
1741
1874
|
agentId,
|
|
1742
1875
|
trigger,
|
|
1743
1876
|
name: rootName,
|
|
@@ -1858,8 +1991,12 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1858
1991
|
// Ignore git directories
|
|
1859
1992
|
/\.vscode/,
|
|
1860
1993
|
// Ignore vscode
|
|
1861
|
-
/\.idea
|
|
1994
|
+
/\.idea/,
|
|
1862
1995
|
// Ignore idea
|
|
1996
|
+
/\/archive\//,
|
|
1997
|
+
// Ignore archived trace files
|
|
1998
|
+
// Ignore user-configured skip directories
|
|
1999
|
+
...getSkipDirectories(this.userConfig).map((d) => new RegExp(`/${d}/`))
|
|
1863
2000
|
],
|
|
1864
2001
|
persistent: true,
|
|
1865
2002
|
ignoreInitial: true,
|
|
@@ -1913,29 +2050,42 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1913
2050
|
});
|
|
1914
2051
|
}
|
|
1915
2052
|
getTrace(filename) {
|
|
2053
|
+
const candidates = [];
|
|
1916
2054
|
const exact = this.traces.get(filename);
|
|
1917
|
-
if (exact)
|
|
2055
|
+
if (exact) candidates.push(exact);
|
|
1918
2056
|
if (filename.includes("::")) {
|
|
1919
2057
|
const [fname, startTimeStr] = filename.split("::");
|
|
1920
2058
|
const startTime = Number(startTimeStr);
|
|
1921
2059
|
if (fname && !Number.isNaN(startTime)) {
|
|
1922
2060
|
for (const trace of this.traces.values()) {
|
|
1923
2061
|
if (trace.filename === fname && trace.startTime === startTime) {
|
|
1924
|
-
|
|
2062
|
+
candidates.push(trace);
|
|
1925
2063
|
}
|
|
1926
2064
|
}
|
|
1927
2065
|
}
|
|
1928
2066
|
}
|
|
1929
2067
|
for (const prefix of ["openclaw:", "otel:", ""]) {
|
|
1930
2068
|
const prefixed = this.traces.get(prefix + filename);
|
|
1931
|
-
if (prefixed)
|
|
2069
|
+
if (prefixed) candidates.push(prefixed);
|
|
1932
2070
|
}
|
|
1933
2071
|
for (const [key, trace] of this.traces) {
|
|
1934
2072
|
if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
|
|
1935
|
-
|
|
2073
|
+
candidates.push(trace);
|
|
1936
2074
|
}
|
|
1937
2075
|
}
|
|
1938
|
-
return void 0;
|
|
2076
|
+
if (candidates.length === 0) return void 0;
|
|
2077
|
+
if (candidates.length === 1) return candidates[0];
|
|
2078
|
+
let best = candidates[0];
|
|
2079
|
+
let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
|
|
2080
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
2081
|
+
const c = candidates[i];
|
|
2082
|
+
const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
|
|
2083
|
+
if (nc > bestNodeCount) {
|
|
2084
|
+
best = c;
|
|
2085
|
+
bestNodeCount = nc;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return best;
|
|
1939
2089
|
}
|
|
1940
2090
|
getTracesByAgent(agentId) {
|
|
1941
2091
|
return this.getAllTraces().filter((trace) => trace.agentId === agentId);
|
|
@@ -1982,7 +2132,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1982
2132
|
import * as fs2 from "fs";
|
|
1983
2133
|
import * as os from "os";
|
|
1984
2134
|
import * as path2 from "path";
|
|
1985
|
-
var VERSION = "0.
|
|
2135
|
+
var VERSION = "0.8.0";
|
|
1986
2136
|
function getLanAddress() {
|
|
1987
2137
|
const interfaces = os.networkInterfaces();
|
|
1988
2138
|
for (const name of Object.keys(interfaces)) {
|
|
@@ -1994,7 +2144,7 @@ function getLanAddress() {
|
|
|
1994
2144
|
}
|
|
1995
2145
|
return null;
|
|
1996
2146
|
}
|
|
1997
|
-
function printBanner(config, traceCount, stats) {
|
|
2147
|
+
function printBanner(config, traceCount, stats, configPath) {
|
|
1998
2148
|
var _a;
|
|
1999
2149
|
const lan = getLanAddress();
|
|
2000
2150
|
const host = config.host || "localhost";
|
|
@@ -2010,26 +2160,23 @@ function printBanner(config, traceCount, stats) {
|
|
|
2010
2160
|
|
|
2011
2161
|
See your agents think.
|
|
2012
2162
|
|
|
2013
|
-
\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
|
|
2014
|
-
\u2502 \u{1F916} Agents \u2502 TRACE FILES \u2502 \u{1F4CA} AgentFlow \u2502 SHOWS YOU \u2502 \u{1F310} Your browser \u2502
|
|
2015
|
-
\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
|
|
2016
|
-
\u2502 write JSON \u2502 \u2502 builds graphs, \u2502 \u2502 graph, timeline, \u2502
|
|
2017
|
-
\u2502 trace files. \u2502 \u2502 serves dashboard.\u2502 \u2502 metrics, health. \u2502
|
|
2018
|
-
\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
|
|
2019
|
-
|
|
2020
|
-
Runs locally. Your data never leaves your machine.
|
|
2021
|
-
|
|
2022
|
-
Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
|
|
2023
|
-
|
|
2024
2163
|
Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? `
|
|
2025
2164
|
Data dirs: ${config.dataDirs.join("\n ")}` : ""}
|
|
2026
2165
|
Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
|
|
2027
2166
|
Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
|
|
2167
|
+
Config: ${configPath ?? "none (using defaults)"}
|
|
2028
2168
|
CORS: ${config.enableCors ? "enabled" : "disabled"}
|
|
2029
2169
|
WebSocket: live updates enabled
|
|
2170
|
+
Window: ${process.env.AGENTFLOW_TRACE_WINDOW_HOURS ?? "48"}h (set AGENTFLOW_TRACE_WINDOW_HOURS to change)
|
|
2030
2171
|
|
|
2031
2172
|
\u2192 http://localhost:${port}${isPublic && lan ? `
|
|
2032
2173
|
\u2192 http://${lan}:${port} (LAN)` : ""}
|
|
2174
|
+
|
|
2175
|
+
Views: Agent Profile \xB7 Execution Detail \xB7 Governance
|
|
2176
|
+
Tabs: Flame Chart \xB7 Agent Flow \xB7 Metrics \xB7 Dependencies
|
|
2177
|
+
State Machine \xB7 Summary \xB7 Transcript
|
|
2178
|
+
|
|
2179
|
+
Runs locally. Your data never leaves your machine.
|
|
2033
2180
|
`);
|
|
2034
2181
|
}
|
|
2035
2182
|
async function startDashboard() {
|
|
@@ -2061,11 +2208,32 @@ async function startDashboard() {
|
|
|
2061
2208
|
case "--cors":
|
|
2062
2209
|
config.enableCors = true;
|
|
2063
2210
|
break;
|
|
2211
|
+
case "--no-collector":
|
|
2212
|
+
config.enableCollector = false;
|
|
2213
|
+
break;
|
|
2214
|
+
case "--collector-token":
|
|
2215
|
+
config.collectorAuthToken = args[++i];
|
|
2216
|
+
break;
|
|
2217
|
+
case "--soma-vault":
|
|
2218
|
+
config.somaVault = args[++i];
|
|
2219
|
+
break;
|
|
2220
|
+
case "--config":
|
|
2221
|
+
config.configPath = args[++i];
|
|
2222
|
+
break;
|
|
2064
2223
|
case "--help":
|
|
2065
2224
|
printHelp();
|
|
2066
2225
|
process.exit(0);
|
|
2067
2226
|
}
|
|
2068
2227
|
}
|
|
2228
|
+
if (!config.collectorAuthToken && process.env.AGENTFLOW_COLLECTOR_TOKEN) {
|
|
2229
|
+
config.collectorAuthToken = process.env.AGENTFLOW_COLLECTOR_TOKEN;
|
|
2230
|
+
}
|
|
2231
|
+
if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
|
|
2232
|
+
config.enableCollector = false;
|
|
2233
|
+
}
|
|
2234
|
+
if (!config.somaVault && process.env.SOMA_VAULT) {
|
|
2235
|
+
config.somaVault = process.env.SOMA_VAULT;
|
|
2236
|
+
}
|
|
2069
2237
|
const tracesPath = path2.resolve(config.tracesDir);
|
|
2070
2238
|
if (!fs2.existsSync(tracesPath)) {
|
|
2071
2239
|
fs2.mkdirSync(tracesPath, { recursive: true });
|
|
@@ -2087,7 +2255,7 @@ async function startDashboard() {
|
|
|
2087
2255
|
setTimeout(() => {
|
|
2088
2256
|
const stats = dashboard.getStats();
|
|
2089
2257
|
const traces = dashboard.getTraces();
|
|
2090
|
-
printBanner(config, traces.length, stats);
|
|
2258
|
+
printBanner(config, traces.length, stats, dashboard.getConfigPath());
|
|
2091
2259
|
}, 1500);
|
|
2092
2260
|
} catch (error) {
|
|
2093
2261
|
console.error("\u274C Failed to start dashboard:", error);
|
|
@@ -2096,7 +2264,7 @@ async function startDashboard() {
|
|
|
2096
2264
|
}
|
|
2097
2265
|
function printHelp() {
|
|
2098
2266
|
console.log(`
|
|
2099
|
-
|
|
2267
|
+
AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
|
|
2100
2268
|
|
|
2101
2269
|
Usage:
|
|
2102
2270
|
agentflow-dashboard [options]
|
|
@@ -2107,20 +2275,34 @@ Options:
|
|
|
2107
2275
|
-t, --traces <path> Traces directory (default: ./traces)
|
|
2108
2276
|
-h, --host <address> Host address (default: localhost)
|
|
2109
2277
|
--data-dir <path> Extra data directory for process discovery (repeatable)
|
|
2278
|
+
--config <path> Path to agentflow.config.json (aliases, skip files, etc.)
|
|
2279
|
+
--soma-vault <path> SOMA vault directory for intelligence data
|
|
2110
2280
|
--cors Enable CORS headers
|
|
2281
|
+
--no-collector Disable OTLP trace collector (POST /v1/traces)
|
|
2282
|
+
--collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
|
|
2111
2283
|
--help Show this help message
|
|
2112
2284
|
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2285
|
+
Config file:
|
|
2286
|
+
The dashboard loads agentflow.config.json for agent aliases, skip files,
|
|
2287
|
+
discovery paths, and systemd services. Resolution order:
|
|
2288
|
+
1. --config flag
|
|
2289
|
+
2. AGENTFLOW_CONFIG env var
|
|
2290
|
+
3. ./agentflow.config.json
|
|
2291
|
+
4. ~/.config/agentflow/config.json
|
|
2292
|
+
|
|
2293
|
+
See agentflow.config.example.json for a complete reference.
|
|
2294
|
+
|
|
2295
|
+
Environment:
|
|
2296
|
+
AGENTFLOW_CONFIG Path to config file
|
|
2297
|
+
AGENTFLOW_TRACE_WINDOW_HOURS Max age of traces to load (default: 48)
|
|
2298
|
+
AGENTFLOW_COLLECTOR_TOKEN Auth token for OTLP collector
|
|
2299
|
+
AGENTFLOW_NO_COLLECTOR=true Disable OTLP collector
|
|
2300
|
+
SOMA_VAULT SOMA vault directory
|
|
2117
2301
|
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
\u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
|
|
2123
|
-
\u26A0\uFE0F Errors Failed and hung nodes with metadata
|
|
2302
|
+
Examples:
|
|
2303
|
+
agentflow-dashboard --traces ./traces --host 0.0.0.0
|
|
2304
|
+
agentflow-dashboard --traces ./traces --config ./agentflow.config.json
|
|
2305
|
+
agentflow-dashboard -p 8080 -t /var/log/agentflow --cors
|
|
2124
2306
|
`);
|
|
2125
2307
|
}
|
|
2126
2308
|
|
|
@@ -2142,12 +2324,15 @@ function serializeTrace(trace) {
|
|
|
2142
2324
|
var DashboardServer = class {
|
|
2143
2325
|
constructor(config) {
|
|
2144
2326
|
this.config = config;
|
|
2145
|
-
const
|
|
2146
|
-
|
|
2327
|
+
const { config: userCfg, configPath: cfgPath } = loadConfig(config.configPath);
|
|
2328
|
+
this.userConfig = userCfg;
|
|
2329
|
+
this.configPath = cfgPath;
|
|
2330
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
2331
|
+
const dashConfigPath = path3.join(home, ".agentflow/dashboard-config.json");
|
|
2147
2332
|
if (!config.dataDirs) config.dataDirs = [];
|
|
2148
2333
|
try {
|
|
2149
|
-
if (fs3.existsSync(
|
|
2150
|
-
const saved = JSON.parse(fs3.readFileSync(
|
|
2334
|
+
if (fs3.existsSync(dashConfigPath)) {
|
|
2335
|
+
const saved = JSON.parse(fs3.readFileSync(dashConfigPath, "utf-8"));
|
|
2151
2336
|
const extraDirs = saved.extraDirs ?? [];
|
|
2152
2337
|
for (const d of extraDirs) {
|
|
2153
2338
|
if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
|
|
@@ -2155,21 +2340,15 @@ var DashboardServer = class {
|
|
|
2155
2340
|
}
|
|
2156
2341
|
} catch {
|
|
2157
2342
|
}
|
|
2158
|
-
const
|
|
2159
|
-
path3.join(home, ".openclaw/cron/runs"),
|
|
2160
|
-
path3.join(home, ".openclaw/workspace/traces"),
|
|
2161
|
-
path3.join(home, ".openclaw/subagents"),
|
|
2162
|
-
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2163
|
-
path3.join(home, ".agentflow/traces")
|
|
2164
|
-
];
|
|
2165
|
-
for (const p of autoDiscoverPaths) {
|
|
2343
|
+
for (const p of getDiscoveryPaths(this.userConfig)) {
|
|
2166
2344
|
if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
|
|
2167
2345
|
config.dataDirs.push(p);
|
|
2168
2346
|
}
|
|
2169
2347
|
}
|
|
2170
2348
|
this.watcher = new TraceWatcher({
|
|
2171
2349
|
tracesDir: config.tracesDir,
|
|
2172
|
-
dataDirs: config.dataDirs
|
|
2350
|
+
dataDirs: config.dataDirs,
|
|
2351
|
+
userConfig: this.userConfig
|
|
2173
2352
|
});
|
|
2174
2353
|
this.stats = new AgentStats();
|
|
2175
2354
|
this.knowledgeStore = createKnowledgeStore({
|
|
@@ -2204,6 +2383,8 @@ var DashboardServer = class {
|
|
|
2204
2383
|
ts: 0
|
|
2205
2384
|
};
|
|
2206
2385
|
knowledgeStore;
|
|
2386
|
+
userConfig;
|
|
2387
|
+
configPath;
|
|
2207
2388
|
setupExpress() {
|
|
2208
2389
|
if (this.config.enableCors) {
|
|
2209
2390
|
this.app.use((_req, res, next) => {
|
|
@@ -2215,18 +2396,35 @@ var DashboardServer = class {
|
|
|
2215
2396
|
next();
|
|
2216
2397
|
});
|
|
2217
2398
|
}
|
|
2218
|
-
const
|
|
2399
|
+
const pkgDir = path3.join(__dirname, "..");
|
|
2400
|
+
const clientDir = path3.join(pkgDir, "dist/client");
|
|
2401
|
+
const clientIndex = path3.join(clientDir, "index.html");
|
|
2402
|
+
const srcDir = path3.join(pkgDir, "src/client");
|
|
2403
|
+
const needsBuild = !fs3.existsSync(clientIndex) || fs3.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
|
|
2404
|
+
if (needsBuild) {
|
|
2405
|
+
try {
|
|
2406
|
+
console.log("Building dashboard client...");
|
|
2407
|
+
execSync("npm run build:client", { cwd: pkgDir, stdio: "inherit", timeout: 3e4 });
|
|
2408
|
+
} catch (err) {
|
|
2409
|
+
console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2219
2412
|
if (fs3.existsSync(clientDir)) {
|
|
2220
2413
|
this.app.use(express.static(clientDir));
|
|
2221
2414
|
}
|
|
2222
|
-
|
|
2223
|
-
if (fs3.existsSync(publicDir)) {
|
|
2224
|
-
this.app.use("/v1", express.static(publicDir));
|
|
2225
|
-
}
|
|
2226
|
-
this.app.get("/api/traces", (_req, res) => {
|
|
2415
|
+
this.app.get("/api/traces", (req, res) => {
|
|
2227
2416
|
try {
|
|
2228
|
-
const
|
|
2229
|
-
|
|
2417
|
+
const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
|
|
2418
|
+
const cursor = req.query.cursor ? parseFloat(req.query.cursor) : void 0;
|
|
2419
|
+
let allTraces = this.watcher.getAllTraces();
|
|
2420
|
+
if (cursor) {
|
|
2421
|
+
allTraces = allTraces.filter((t) => (t.lastModified || t.startTime) < cursor);
|
|
2422
|
+
}
|
|
2423
|
+
const page = allTraces.slice(0, limit);
|
|
2424
|
+
const serialized = page.map(serializeTrace);
|
|
2425
|
+
const lastTrace = page[page.length - 1];
|
|
2426
|
+
const nextCursor = page.length === limit && lastTrace ? lastTrace.lastModified || lastTrace.startTime : null;
|
|
2427
|
+
res.json({ traces: serialized, nextCursor });
|
|
2230
2428
|
} catch (_error) {
|
|
2231
2429
|
res.status(500).json({ error: "Failed to load traces" });
|
|
2232
2430
|
}
|
|
@@ -2517,6 +2715,102 @@ var DashboardServer = class {
|
|
|
2517
2715
|
res.status(500).json({ error: "Failed to load agent statistics" });
|
|
2518
2716
|
}
|
|
2519
2717
|
});
|
|
2718
|
+
this.app.get("/api/soma/report", (_req, res) => {
|
|
2719
|
+
const somaVault = this.config.somaVault;
|
|
2720
|
+
if (!somaVault) {
|
|
2721
|
+
return res.json({ available: false, teaser: true });
|
|
2722
|
+
}
|
|
2723
|
+
try {
|
|
2724
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2725
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2726
|
+
return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
|
|
2727
|
+
}
|
|
2728
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2729
|
+
res.json(report);
|
|
2730
|
+
} catch (error) {
|
|
2731
|
+
console.error("Soma report error:", error);
|
|
2732
|
+
res.json({ available: false, teaser: false, message: "Failed to read report" });
|
|
2733
|
+
}
|
|
2734
|
+
});
|
|
2735
|
+
this.app.get("/api/soma/governance", (_req, res) => {
|
|
2736
|
+
const somaVault = this.config.somaVault;
|
|
2737
|
+
if (!somaVault) {
|
|
2738
|
+
return res.json({ available: false });
|
|
2739
|
+
}
|
|
2740
|
+
try {
|
|
2741
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2742
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2743
|
+
return res.json({ available: false, message: "No report file. Run soma report." });
|
|
2744
|
+
}
|
|
2745
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2746
|
+
res.json({
|
|
2747
|
+
available: true,
|
|
2748
|
+
layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
|
|
2749
|
+
governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
|
|
2750
|
+
insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
|
|
2751
|
+
canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
|
|
2752
|
+
generatedAt: report.generatedAt
|
|
2753
|
+
});
|
|
2754
|
+
} catch (error) {
|
|
2755
|
+
console.error("Soma governance error:", error);
|
|
2756
|
+
res.status(500).json({ available: false, message: "Failed to read governance data" });
|
|
2757
|
+
}
|
|
2758
|
+
});
|
|
2759
|
+
const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
|
|
2760
|
+
const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
|
|
2761
|
+
this.app.post("/api/soma/governance/promote", (req, res) => {
|
|
2762
|
+
var _a;
|
|
2763
|
+
const somaVault = this.config.somaVault;
|
|
2764
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2765
|
+
const { entryId } = req.body ?? {};
|
|
2766
|
+
if (!entryId) return res.status(400).json({ error: "entryId required" });
|
|
2767
|
+
try {
|
|
2768
|
+
const { execSync: execSync2 } = __require("child_process");
|
|
2769
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2770
|
+
const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
|
|
2771
|
+
encoding: "utf-8",
|
|
2772
|
+
timeout: 1e4
|
|
2773
|
+
});
|
|
2774
|
+
res.json({ success: true, message: result.trim() });
|
|
2775
|
+
} catch (error) {
|
|
2776
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2777
|
+
}
|
|
2778
|
+
});
|
|
2779
|
+
this.app.post("/api/soma/governance/reject", (req, res) => {
|
|
2780
|
+
var _a;
|
|
2781
|
+
const somaVault = this.config.somaVault;
|
|
2782
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2783
|
+
const { entryId, reason } = req.body ?? {};
|
|
2784
|
+
if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
|
|
2785
|
+
try {
|
|
2786
|
+
const { execSync: execSync2 } = __require("child_process");
|
|
2787
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2788
|
+
const safeReason = sanitizeReason(String(reason));
|
|
2789
|
+
const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
|
|
2790
|
+
encoding: "utf-8",
|
|
2791
|
+
timeout: 1e4
|
|
2792
|
+
});
|
|
2793
|
+
res.json({ success: true, message: result.trim() });
|
|
2794
|
+
} catch (error) {
|
|
2795
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2796
|
+
}
|
|
2797
|
+
});
|
|
2798
|
+
this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
|
|
2799
|
+
var _a;
|
|
2800
|
+
const somaVault = this.config.somaVault;
|
|
2801
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2802
|
+
try {
|
|
2803
|
+
const { execSync: execSync2 } = __require("child_process");
|
|
2804
|
+
const safeId = sanitizeArg(String(req.params.id));
|
|
2805
|
+
const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
|
|
2806
|
+
encoding: "utf-8",
|
|
2807
|
+
timeout: 1e4
|
|
2808
|
+
});
|
|
2809
|
+
res.json({ available: true, output: result.trim() });
|
|
2810
|
+
} catch (error) {
|
|
2811
|
+
res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2520
2814
|
this.app.get("/api/process-health", (_req, res) => {
|
|
2521
2815
|
var _a, _b;
|
|
2522
2816
|
try {
|
|
@@ -2529,7 +2823,14 @@ var DashboardServer = class {
|
|
|
2529
2823
|
path3.dirname(this.config.tracesDir),
|
|
2530
2824
|
...this.config.dataDirs || []
|
|
2531
2825
|
];
|
|
2532
|
-
|
|
2826
|
+
let configs = discoverAllProcessConfigs(discoveryDirs);
|
|
2827
|
+
const pref = getProcessPreference(this.userConfig);
|
|
2828
|
+
if (pref) {
|
|
2829
|
+
const hasPreferred = configs.some((c) => c.processName === pref.prefer);
|
|
2830
|
+
if (hasPreferred) {
|
|
2831
|
+
configs = configs.filter((c) => c.processName !== pref.over);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2533
2834
|
if (configs.length === 0) {
|
|
2534
2835
|
return res.json(null);
|
|
2535
2836
|
}
|
|
@@ -2619,29 +2920,26 @@ var DashboardServer = class {
|
|
|
2619
2920
|
...extraDirs
|
|
2620
2921
|
];
|
|
2621
2922
|
const discovered = [];
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
const
|
|
2632
|
-
if (
|
|
2923
|
+
const svcNames = getSystemdServices(this.userConfig);
|
|
2924
|
+
if (svcNames.length > 0) {
|
|
2925
|
+
try {
|
|
2926
|
+
const { execSync: execSync2 } = __require("child_process");
|
|
2927
|
+
const raw = execSync2(
|
|
2928
|
+
`systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
|
|
2929
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
2930
|
+
);
|
|
2931
|
+
for (const line of raw.split("\n")) {
|
|
2932
|
+
const match = line.match(/path=([^\s;]+)/);
|
|
2933
|
+
if (match == null ? void 0 : match[1]) {
|
|
2934
|
+
const dir = path3.dirname(match[1]);
|
|
2935
|
+
if (fs3.existsSync(dir)) discovered.push(dir);
|
|
2936
|
+
}
|
|
2633
2937
|
}
|
|
2938
|
+
} catch {
|
|
2634
2939
|
}
|
|
2635
|
-
} catch {
|
|
2636
2940
|
}
|
|
2637
2941
|
const commonPaths = [
|
|
2638
|
-
|
|
2639
|
-
path3.join(home, ".alfred/data"),
|
|
2640
|
-
path3.join(home, ".openclaw/workspace/traces"),
|
|
2641
|
-
path3.join(home, ".openclaw/subagents"),
|
|
2642
|
-
path3.join(home, ".openclaw/cron/runs"),
|
|
2643
|
-
path3.join(home, ".openclaw/cron"),
|
|
2644
|
-
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2942
|
+
...getDiscoveryPaths(this.userConfig),
|
|
2645
2943
|
path3.join(home, ".agentflow/traces")
|
|
2646
2944
|
];
|
|
2647
2945
|
for (const p of commonPaths) {
|
|
@@ -2686,46 +2984,54 @@ var DashboardServer = class {
|
|
|
2686
2984
|
res.status(500).json({ error: "Failed to update directory config" });
|
|
2687
2985
|
}
|
|
2688
2986
|
});
|
|
2689
|
-
this.
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2987
|
+
if (this.config.enableCollector !== false) {
|
|
2988
|
+
this.app.post("/v1/traces", express.json({ limit: "10mb" }), (req, res) => {
|
|
2989
|
+
try {
|
|
2990
|
+
if (this.config.collectorAuthToken) {
|
|
2991
|
+
const auth = req.headers.authorization;
|
|
2992
|
+
if (!auth || auth !== `Bearer ${this.config.collectorAuthToken}`) {
|
|
2993
|
+
return res.status(401).json({ error: "Unauthorized \u2014 provide Authorization: Bearer <token>" });
|
|
2994
|
+
}
|
|
2697
2995
|
}
|
|
2698
|
-
const
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2996
|
+
const traces = parseOtlpPayload(req.body);
|
|
2997
|
+
let ingested = 0;
|
|
2998
|
+
for (const trace of traces) {
|
|
2999
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
3000
|
+
for (const [id, node] of Object.entries(trace.nodes)) {
|
|
3001
|
+
nodes.set(id, { ...node, state: {} });
|
|
3002
|
+
}
|
|
3003
|
+
const watched = {
|
|
3004
|
+
id: trace.id,
|
|
3005
|
+
rootNodeId: Object.keys(trace.nodes)[0] ?? "",
|
|
3006
|
+
agentId: trace.agentId,
|
|
3007
|
+
name: trace.name,
|
|
3008
|
+
trigger: trace.trigger,
|
|
3009
|
+
startTime: trace.startTime,
|
|
3010
|
+
endTime: trace.endTime,
|
|
3011
|
+
status: trace.status,
|
|
3012
|
+
nodes,
|
|
3013
|
+
edges: [],
|
|
3014
|
+
events: [],
|
|
3015
|
+
metadata: { ...trace.metadata, adapterSource: "otel" },
|
|
3016
|
+
sessionEvents: [],
|
|
3017
|
+
sourceType: "session",
|
|
3018
|
+
filename: `otel-${trace.id}`,
|
|
3019
|
+
lastModified: Date.now(),
|
|
3020
|
+
sourceDir: "http-collector"
|
|
3021
|
+
};
|
|
3022
|
+
this.watcher.traces.set(`otel:${trace.id}`, watched);
|
|
3023
|
+
ingested++;
|
|
3024
|
+
}
|
|
3025
|
+
if (ingested > 0) {
|
|
3026
|
+
this.broadcast({ type: "traces-updated", count: ingested });
|
|
3027
|
+
}
|
|
3028
|
+
res.json({ ok: true, tracesIngested: ingested });
|
|
3029
|
+
} catch (error) {
|
|
3030
|
+
console.error("OTLP collector error:", error);
|
|
3031
|
+
res.status(400).json({ error: "Failed to parse OTLP payload" });
|
|
2722
3032
|
}
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
console.error("OTLP collector error:", error);
|
|
2726
|
-
res.status(400).json({ error: "Failed to parse OTLP payload" });
|
|
2727
|
-
}
|
|
2728
|
-
});
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
2729
3035
|
this.app.get("/health", (_req, res) => {
|
|
2730
3036
|
res.json({
|
|
2731
3037
|
status: "ok",
|
|
@@ -2737,18 +3043,10 @@ var DashboardServer = class {
|
|
|
2737
3043
|
this.app.get("/ready", (_req, res) => {
|
|
2738
3044
|
res.json({ status: "ready" });
|
|
2739
3045
|
});
|
|
2740
|
-
this.app.get("/v1/*", (_req, res) => {
|
|
2741
|
-
const legacyIndex = path3.join(__dirname, "../public/index.html");
|
|
2742
|
-
if (fs3.existsSync(legacyIndex)) {
|
|
2743
|
-
res.sendFile(legacyIndex);
|
|
2744
|
-
} else {
|
|
2745
|
-
res.status(404).send("Legacy dashboard not found");
|
|
2746
|
-
}
|
|
2747
|
-
});
|
|
2748
3046
|
this.app.get("*", (_req, res) => {
|
|
2749
|
-
const
|
|
2750
|
-
if (fs3.existsSync(
|
|
2751
|
-
res.sendFile(
|
|
3047
|
+
const clientIndex2 = path3.join(__dirname, "../dist/client/index.html");
|
|
3048
|
+
if (fs3.existsSync(clientIndex2)) {
|
|
3049
|
+
res.sendFile(clientIndex2);
|
|
2752
3050
|
} else {
|
|
2753
3051
|
res.status(404).send("Dashboard not found - public files may not be built");
|
|
2754
3052
|
}
|
|
@@ -2995,24 +3293,49 @@ var DashboardServer = class {
|
|
|
2995
3293
|
});
|
|
2996
3294
|
}
|
|
2997
3295
|
async start() {
|
|
2998
|
-
return new Promise((
|
|
3296
|
+
return new Promise((resolve5) => {
|
|
2999
3297
|
const host = this.config.host || "localhost";
|
|
3000
3298
|
this.server.listen(this.config.port, host, () => {
|
|
3001
3299
|
console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
|
|
3002
3300
|
console.log(`Watching traces in: ${this.config.tracesDir}`);
|
|
3003
|
-
|
|
3301
|
+
resolve5();
|
|
3004
3302
|
});
|
|
3005
3303
|
});
|
|
3006
3304
|
}
|
|
3305
|
+
/** Check if any src/client file is newer than the built bundle. */
|
|
3306
|
+
isClientStale(srcDir, distDir) {
|
|
3307
|
+
try {
|
|
3308
|
+
const distIndex = path3.join(distDir, "index.html");
|
|
3309
|
+
if (!fs3.existsSync(distIndex)) return true;
|
|
3310
|
+
const distMtime = fs3.statSync(distIndex).mtimeMs;
|
|
3311
|
+
const check = (dir) => {
|
|
3312
|
+
for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
|
|
3313
|
+
const full = path3.join(dir, entry.name);
|
|
3314
|
+
if (entry.isDirectory()) {
|
|
3315
|
+
if (check(full)) return true;
|
|
3316
|
+
} else if (fs3.statSync(full).mtimeMs > distMtime) {
|
|
3317
|
+
return true;
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
return false;
|
|
3321
|
+
};
|
|
3322
|
+
return check(srcDir);
|
|
3323
|
+
} catch {
|
|
3324
|
+
return false;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3007
3327
|
async stop() {
|
|
3008
|
-
return new Promise((
|
|
3328
|
+
return new Promise((resolve5) => {
|
|
3009
3329
|
this.watcher.stop();
|
|
3010
3330
|
this.server.close(() => {
|
|
3011
3331
|
console.log("Dashboard server stopped");
|
|
3012
|
-
|
|
3332
|
+
resolve5();
|
|
3013
3333
|
});
|
|
3014
3334
|
});
|
|
3015
3335
|
}
|
|
3336
|
+
getConfigPath() {
|
|
3337
|
+
return this.configPath;
|
|
3338
|
+
}
|
|
3016
3339
|
getStats() {
|
|
3017
3340
|
return this.stats.getGlobalStats();
|
|
3018
3341
|
}
|