agentflow-dashboard 0.7.1 → 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/dist/{chunk-3S4AAIPA.js → chunk-NZFXRZYU.js} +469 -168
- package/dist/cli.cjs +473 -172
- 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 +473 -172
- package/dist/index.js +1 -1
- package/dist/server.cjs +473 -172
- 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
|
@@ -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);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
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;
|
|
1936
2086
|
}
|
|
1937
2087
|
}
|
|
1938
|
-
return
|
|
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() {
|
|
@@ -2067,6 +2214,12 @@ async function startDashboard() {
|
|
|
2067
2214
|
case "--collector-token":
|
|
2068
2215
|
config.collectorAuthToken = args[++i];
|
|
2069
2216
|
break;
|
|
2217
|
+
case "--soma-vault":
|
|
2218
|
+
config.somaVault = args[++i];
|
|
2219
|
+
break;
|
|
2220
|
+
case "--config":
|
|
2221
|
+
config.configPath = args[++i];
|
|
2222
|
+
break;
|
|
2070
2223
|
case "--help":
|
|
2071
2224
|
printHelp();
|
|
2072
2225
|
process.exit(0);
|
|
@@ -2078,6 +2231,9 @@ async function startDashboard() {
|
|
|
2078
2231
|
if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
|
|
2079
2232
|
config.enableCollector = false;
|
|
2080
2233
|
}
|
|
2234
|
+
if (!config.somaVault && process.env.SOMA_VAULT) {
|
|
2235
|
+
config.somaVault = process.env.SOMA_VAULT;
|
|
2236
|
+
}
|
|
2081
2237
|
const tracesPath = path2.resolve(config.tracesDir);
|
|
2082
2238
|
if (!fs2.existsSync(tracesPath)) {
|
|
2083
2239
|
fs2.mkdirSync(tracesPath, { recursive: true });
|
|
@@ -2099,7 +2255,7 @@ async function startDashboard() {
|
|
|
2099
2255
|
setTimeout(() => {
|
|
2100
2256
|
const stats = dashboard.getStats();
|
|
2101
2257
|
const traces = dashboard.getTraces();
|
|
2102
|
-
printBanner(config, traces.length, stats);
|
|
2258
|
+
printBanner(config, traces.length, stats, dashboard.getConfigPath());
|
|
2103
2259
|
}, 1500);
|
|
2104
2260
|
} catch (error) {
|
|
2105
2261
|
console.error("\u274C Failed to start dashboard:", error);
|
|
@@ -2108,7 +2264,7 @@ async function startDashboard() {
|
|
|
2108
2264
|
}
|
|
2109
2265
|
function printHelp() {
|
|
2110
2266
|
console.log(`
|
|
2111
|
-
|
|
2267
|
+
AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
|
|
2112
2268
|
|
|
2113
2269
|
Usage:
|
|
2114
2270
|
agentflow-dashboard [options]
|
|
@@ -2119,22 +2275,34 @@ Options:
|
|
|
2119
2275
|
-t, --traces <path> Traces directory (default: ./traces)
|
|
2120
2276
|
-h, --host <address> Host address (default: localhost)
|
|
2121
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
|
|
2122
2280
|
--cors Enable CORS headers
|
|
2123
2281
|
--no-collector Disable OTLP trace collector (POST /v1/traces)
|
|
2124
2282
|
--collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
|
|
2125
2283
|
--help Show this help message
|
|
2126
2284
|
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
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.
|
|
2131
2294
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
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
|
|
2301
|
+
|
|
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
|
|
2138
2306
|
`);
|
|
2139
2307
|
}
|
|
2140
2308
|
|
|
@@ -2156,12 +2324,15 @@ function serializeTrace(trace) {
|
|
|
2156
2324
|
var DashboardServer = class {
|
|
2157
2325
|
constructor(config) {
|
|
2158
2326
|
this.config = config;
|
|
2159
|
-
const
|
|
2160
|
-
|
|
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");
|
|
2161
2332
|
if (!config.dataDirs) config.dataDirs = [];
|
|
2162
2333
|
try {
|
|
2163
|
-
if (fs3.existsSync(
|
|
2164
|
-
const saved = JSON.parse(fs3.readFileSync(
|
|
2334
|
+
if (fs3.existsSync(dashConfigPath)) {
|
|
2335
|
+
const saved = JSON.parse(fs3.readFileSync(dashConfigPath, "utf-8"));
|
|
2165
2336
|
const extraDirs = saved.extraDirs ?? [];
|
|
2166
2337
|
for (const d of extraDirs) {
|
|
2167
2338
|
if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
|
|
@@ -2169,21 +2340,15 @@ var DashboardServer = class {
|
|
|
2169
2340
|
}
|
|
2170
2341
|
} catch {
|
|
2171
2342
|
}
|
|
2172
|
-
const
|
|
2173
|
-
path3.join(home, ".openclaw/cron/runs"),
|
|
2174
|
-
path3.join(home, ".openclaw/workspace/traces"),
|
|
2175
|
-
path3.join(home, ".openclaw/subagents"),
|
|
2176
|
-
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2177
|
-
path3.join(home, ".agentflow/traces")
|
|
2178
|
-
];
|
|
2179
|
-
for (const p of autoDiscoverPaths) {
|
|
2343
|
+
for (const p of getDiscoveryPaths(this.userConfig)) {
|
|
2180
2344
|
if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
|
|
2181
2345
|
config.dataDirs.push(p);
|
|
2182
2346
|
}
|
|
2183
2347
|
}
|
|
2184
2348
|
this.watcher = new TraceWatcher({
|
|
2185
2349
|
tracesDir: config.tracesDir,
|
|
2186
|
-
dataDirs: config.dataDirs
|
|
2350
|
+
dataDirs: config.dataDirs,
|
|
2351
|
+
userConfig: this.userConfig
|
|
2187
2352
|
});
|
|
2188
2353
|
this.stats = new AgentStats();
|
|
2189
2354
|
this.knowledgeStore = createKnowledgeStore({
|
|
@@ -2218,6 +2383,8 @@ var DashboardServer = class {
|
|
|
2218
2383
|
ts: 0
|
|
2219
2384
|
};
|
|
2220
2385
|
knowledgeStore;
|
|
2386
|
+
userConfig;
|
|
2387
|
+
configPath;
|
|
2221
2388
|
setupExpress() {
|
|
2222
2389
|
if (this.config.enableCors) {
|
|
2223
2390
|
this.app.use((_req, res, next) => {
|
|
@@ -2229,18 +2396,35 @@ var DashboardServer = class {
|
|
|
2229
2396
|
next();
|
|
2230
2397
|
});
|
|
2231
2398
|
}
|
|
2232
|
-
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
|
+
}
|
|
2233
2412
|
if (fs3.existsSync(clientDir)) {
|
|
2234
2413
|
this.app.use(express.static(clientDir));
|
|
2235
2414
|
}
|
|
2236
|
-
|
|
2237
|
-
if (fs3.existsSync(publicDir)) {
|
|
2238
|
-
this.app.use("/v1", express.static(publicDir));
|
|
2239
|
-
}
|
|
2240
|
-
this.app.get("/api/traces", (_req, res) => {
|
|
2415
|
+
this.app.get("/api/traces", (req, res) => {
|
|
2241
2416
|
try {
|
|
2242
|
-
const
|
|
2243
|
-
|
|
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 });
|
|
2244
2428
|
} catch (_error) {
|
|
2245
2429
|
res.status(500).json({ error: "Failed to load traces" });
|
|
2246
2430
|
}
|
|
@@ -2531,6 +2715,102 @@ var DashboardServer = class {
|
|
|
2531
2715
|
res.status(500).json({ error: "Failed to load agent statistics" });
|
|
2532
2716
|
}
|
|
2533
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
|
+
});
|
|
2534
2814
|
this.app.get("/api/process-health", (_req, res) => {
|
|
2535
2815
|
var _a, _b;
|
|
2536
2816
|
try {
|
|
@@ -2543,7 +2823,14 @@ var DashboardServer = class {
|
|
|
2543
2823
|
path3.dirname(this.config.tracesDir),
|
|
2544
2824
|
...this.config.dataDirs || []
|
|
2545
2825
|
];
|
|
2546
|
-
|
|
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
|
+
}
|
|
2547
2834
|
if (configs.length === 0) {
|
|
2548
2835
|
return res.json(null);
|
|
2549
2836
|
}
|
|
@@ -2633,29 +2920,26 @@ var DashboardServer = class {
|
|
|
2633
2920
|
...extraDirs
|
|
2634
2921
|
];
|
|
2635
2922
|
const discovered = [];
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
const
|
|
2646
|
-
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
|
+
}
|
|
2647
2937
|
}
|
|
2938
|
+
} catch {
|
|
2648
2939
|
}
|
|
2649
|
-
} catch {
|
|
2650
2940
|
}
|
|
2651
2941
|
const commonPaths = [
|
|
2652
|
-
|
|
2653
|
-
path3.join(home, ".alfred/data"),
|
|
2654
|
-
path3.join(home, ".openclaw/workspace/traces"),
|
|
2655
|
-
path3.join(home, ".openclaw/subagents"),
|
|
2656
|
-
path3.join(home, ".openclaw/cron/runs"),
|
|
2657
|
-
path3.join(home, ".openclaw/cron"),
|
|
2658
|
-
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2942
|
+
...getDiscoveryPaths(this.userConfig),
|
|
2659
2943
|
path3.join(home, ".agentflow/traces")
|
|
2660
2944
|
];
|
|
2661
2945
|
for (const p of commonPaths) {
|
|
@@ -2759,18 +3043,10 @@ var DashboardServer = class {
|
|
|
2759
3043
|
this.app.get("/ready", (_req, res) => {
|
|
2760
3044
|
res.json({ status: "ready" });
|
|
2761
3045
|
});
|
|
2762
|
-
this.app.get("/v1/*", (_req, res) => {
|
|
2763
|
-
const legacyIndex = path3.join(__dirname, "../public/index.html");
|
|
2764
|
-
if (fs3.existsSync(legacyIndex)) {
|
|
2765
|
-
res.sendFile(legacyIndex);
|
|
2766
|
-
} else {
|
|
2767
|
-
res.status(404).send("Legacy dashboard not found");
|
|
2768
|
-
}
|
|
2769
|
-
});
|
|
2770
3046
|
this.app.get("*", (_req, res) => {
|
|
2771
|
-
const
|
|
2772
|
-
if (fs3.existsSync(
|
|
2773
|
-
res.sendFile(
|
|
3047
|
+
const clientIndex2 = path3.join(__dirname, "../dist/client/index.html");
|
|
3048
|
+
if (fs3.existsSync(clientIndex2)) {
|
|
3049
|
+
res.sendFile(clientIndex2);
|
|
2774
3050
|
} else {
|
|
2775
3051
|
res.status(404).send("Dashboard not found - public files may not be built");
|
|
2776
3052
|
}
|
|
@@ -3017,24 +3293,49 @@ var DashboardServer = class {
|
|
|
3017
3293
|
});
|
|
3018
3294
|
}
|
|
3019
3295
|
async start() {
|
|
3020
|
-
return new Promise((
|
|
3296
|
+
return new Promise((resolve5) => {
|
|
3021
3297
|
const host = this.config.host || "localhost";
|
|
3022
3298
|
this.server.listen(this.config.port, host, () => {
|
|
3023
3299
|
console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
|
|
3024
3300
|
console.log(`Watching traces in: ${this.config.tracesDir}`);
|
|
3025
|
-
|
|
3301
|
+
resolve5();
|
|
3026
3302
|
});
|
|
3027
3303
|
});
|
|
3028
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
|
+
}
|
|
3029
3327
|
async stop() {
|
|
3030
|
-
return new Promise((
|
|
3328
|
+
return new Promise((resolve5) => {
|
|
3031
3329
|
this.watcher.stop();
|
|
3032
3330
|
this.server.close(() => {
|
|
3033
3331
|
console.log("Dashboard server stopped");
|
|
3034
|
-
|
|
3332
|
+
resolve5();
|
|
3035
3333
|
});
|
|
3036
3334
|
});
|
|
3037
3335
|
}
|
|
3336
|
+
getConfigPath() {
|
|
3337
|
+
return this.configPath;
|
|
3338
|
+
}
|
|
3038
3339
|
getStats() {
|
|
3039
3340
|
return this.stats.getGlobalStats();
|
|
3040
3341
|
}
|