agentflow-dashboard 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-3S4AAIPA.js → chunk-JRVE5NM3.js} +644 -171
- package/dist/cli.cjs +648 -175
- package/dist/cli.js +1 -1
- package/dist/client/assets/index-CyQ7qX-x.js +50 -0
- package/dist/client/assets/{index-Ds_npIxI.css → index-DHcSpTgM.css} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/index.cjs +648 -175
- package/dist/index.js +1 -1
- package/dist/server.cjs +648 -175
- package/dist/server.js +1 -1
- package/package.json +3 -5
- package/dist/client/assets/index-DSuI0NgP.js +0 -50
- package/dist/client/dashboard.js +0 -3113
- package/dist/public/dashboard.js +0 -3113
- package/dist/public/index.html +0 -1385
- package/public/dashboard.js +0 -3113
- package/public/index.html +0 -1385
|
@@ -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,
|
|
@@ -20,6 +94,7 @@ import {
|
|
|
20
94
|
getBottlenecks,
|
|
21
95
|
loadGraph as loadGraph2
|
|
22
96
|
} from "agentflow-core";
|
|
97
|
+
import chokidar2 from "chokidar";
|
|
23
98
|
import express from "express";
|
|
24
99
|
import { WebSocketServer } from "ws";
|
|
25
100
|
|
|
@@ -52,17 +127,17 @@ var AgentFlowAdapter = class {
|
|
|
52
127
|
};
|
|
53
128
|
|
|
54
129
|
// src/adapters/openclaw.ts
|
|
55
|
-
import { existsSync, readFileSync } from "fs";
|
|
56
|
-
import { basename, dirname, join } from "path";
|
|
130
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
131
|
+
import { basename, dirname, join as join2 } from "path";
|
|
57
132
|
var jobCache = /* @__PURE__ */ new Map();
|
|
58
133
|
function loadJobs(openclawDir) {
|
|
59
134
|
const cached = jobCache.get(openclawDir);
|
|
60
135
|
if (cached) return cached;
|
|
61
|
-
const jobsPath =
|
|
136
|
+
const jobsPath = join2(openclawDir, "cron", "jobs.json");
|
|
62
137
|
const map = /* @__PURE__ */ new Map();
|
|
63
138
|
try {
|
|
64
|
-
if (
|
|
65
|
-
const data = JSON.parse(
|
|
139
|
+
if (existsSync2(jobsPath)) {
|
|
140
|
+
const data = JSON.parse(readFileSync2(jobsPath, "utf-8"));
|
|
66
141
|
const jobs = Array.isArray(data) ? data : data.jobs ?? [];
|
|
67
142
|
for (const job of jobs) {
|
|
68
143
|
if (job.id) map.set(job.id, job);
|
|
@@ -76,7 +151,7 @@ function loadJobs(openclawDir) {
|
|
|
76
151
|
function findOpenClawRoot(filePath) {
|
|
77
152
|
let dir = dirname(filePath);
|
|
78
153
|
for (let i = 0; i < 5; i++) {
|
|
79
|
-
if (
|
|
154
|
+
if (existsSync2(join2(dir, "cron", "jobs.json")) || basename(dir) === ".openclaw") {
|
|
80
155
|
return dir;
|
|
81
156
|
}
|
|
82
157
|
dir = dirname(dir);
|
|
@@ -86,7 +161,7 @@ function findOpenClawRoot(filePath) {
|
|
|
86
161
|
var OpenClawAdapter = class {
|
|
87
162
|
name = "openclaw";
|
|
88
163
|
detect(dirPath) {
|
|
89
|
-
return
|
|
164
|
+
return existsSync2(join2(dirPath, "cron", "jobs.json")) || dirPath.includes(".openclaw") || existsSync2(join2(dirPath, "cron", "runs"));
|
|
90
165
|
}
|
|
91
166
|
canHandle(filePath) {
|
|
92
167
|
if (!filePath.endsWith(".jsonl")) return false;
|
|
@@ -95,7 +170,7 @@ var OpenClawAdapter = class {
|
|
|
95
170
|
parse(filePath) {
|
|
96
171
|
const traces = [];
|
|
97
172
|
try {
|
|
98
|
-
const content =
|
|
173
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
99
174
|
const root = findOpenClawRoot(filePath);
|
|
100
175
|
const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
|
|
101
176
|
for (const line of content.split("\n")) {
|
|
@@ -159,8 +234,8 @@ var OpenClawAdapter = class {
|
|
|
159
234
|
};
|
|
160
235
|
|
|
161
236
|
// src/adapters/otel.ts
|
|
162
|
-
import { existsSync as
|
|
163
|
-
import { join as
|
|
237
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
238
|
+
import { join as join3 } from "path";
|
|
164
239
|
var SPAN_TYPE_MAP = {
|
|
165
240
|
"gen_ai.chat": "llm",
|
|
166
241
|
"gen_ai.completion": "llm",
|
|
@@ -267,7 +342,7 @@ var OTelAdapter = class {
|
|
|
267
342
|
name = "otel";
|
|
268
343
|
detect(dirPath) {
|
|
269
344
|
try {
|
|
270
|
-
if (
|
|
345
|
+
if (existsSync3(join3(dirPath, "otel-traces"))) return true;
|
|
271
346
|
const files = readdirSync(dirPath);
|
|
272
347
|
return files.some((f) => f.endsWith(".otlp.json"));
|
|
273
348
|
} catch {
|
|
@@ -279,7 +354,7 @@ var OTelAdapter = class {
|
|
|
279
354
|
}
|
|
280
355
|
parse(filePath) {
|
|
281
356
|
try {
|
|
282
|
-
const content =
|
|
357
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
283
358
|
const payload = JSON.parse(content);
|
|
284
359
|
const traces = parseOtlpPayload(payload);
|
|
285
360
|
for (const t of traces) t.filePath = filePath;
|
|
@@ -321,9 +396,7 @@ function extractSource(agentId) {
|
|
|
321
396
|
const colonIdx = agentId.indexOf(":");
|
|
322
397
|
if (colonIdx > 0 && colonIdx < 20) {
|
|
323
398
|
const prefix = agentId.slice(0, colonIdx);
|
|
324
|
-
|
|
325
|
-
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
326
|
-
}
|
|
399
|
+
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
327
400
|
}
|
|
328
401
|
return { source: "agentflow", localId: agentId };
|
|
329
402
|
}
|
|
@@ -354,16 +427,20 @@ function deduplicateAgents(agents) {
|
|
|
354
427
|
for (const a of tagged) {
|
|
355
428
|
const suffix = extractSuffix(a.localId);
|
|
356
429
|
if (!suffix) continue;
|
|
357
|
-
const
|
|
430
|
+
const key = `${a.source}:${suffix}`;
|
|
431
|
+
const group = suffixGroups.get(key) ?? [];
|
|
358
432
|
group.push(a);
|
|
359
|
-
suffixGroups.set(
|
|
433
|
+
suffixGroups.set(key, group);
|
|
360
434
|
}
|
|
361
435
|
const mergedIds = /* @__PURE__ */ new Set();
|
|
362
436
|
const mergedAgents = [];
|
|
363
|
-
for (const [
|
|
437
|
+
for (const [_key, group] of suffixGroups) {
|
|
438
|
+
const suffix = extractSuffix(group[0].localId);
|
|
364
439
|
if (group.length < 2) continue;
|
|
365
440
|
const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
|
|
366
441
|
if (prefixes.size < 2) continue;
|
|
442
|
+
const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
|
|
443
|
+
if (longPrefixes.length >= 2) continue;
|
|
367
444
|
const merged = {
|
|
368
445
|
agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
|
|
369
446
|
displayName: suffix,
|
|
@@ -411,10 +488,7 @@ function groupAgents(agents) {
|
|
|
411
488
|
}
|
|
412
489
|
const SOURCE_DISPLAY = {
|
|
413
490
|
agentflow: "AgentFlow",
|
|
414
|
-
|
|
415
|
-
otel: "OpenTelemetry",
|
|
416
|
-
langchain: "LangChain",
|
|
417
|
-
crewai: "CrewAI"
|
|
491
|
+
otel: "OpenTelemetry"
|
|
418
492
|
};
|
|
419
493
|
const groups = [];
|
|
420
494
|
for (const [source, sourceAgents] of sourceMap) {
|
|
@@ -760,10 +834,6 @@ function getUniversalNodeStatus(activity) {
|
|
|
760
834
|
return "completed";
|
|
761
835
|
}
|
|
762
836
|
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
837
|
const firstSegment = sessionId.split("-")[0];
|
|
768
838
|
if (firstSegment) return firstSegment;
|
|
769
839
|
return "openclaw";
|
|
@@ -776,19 +846,81 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
776
846
|
tracesDir;
|
|
777
847
|
dataDirs;
|
|
778
848
|
allWatchDirs;
|
|
849
|
+
maxAgeMs;
|
|
850
|
+
userConfig;
|
|
779
851
|
constructor(tracesDirOrOptions) {
|
|
780
852
|
super();
|
|
853
|
+
const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
|
|
854
|
+
const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
|
|
855
|
+
const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
|
|
781
856
|
if (typeof tracesDirOrOptions === "string") {
|
|
782
857
|
this.tracesDir = path.resolve(tracesDirOrOptions);
|
|
783
858
|
this.dataDirs = [];
|
|
859
|
+
this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
|
|
860
|
+
this.userConfig = {};
|
|
784
861
|
} else {
|
|
785
862
|
this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
|
|
786
863
|
this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
|
|
787
|
-
|
|
788
|
-
|
|
864
|
+
this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
|
|
865
|
+
this.userConfig = tracesDirOrOptions.userConfig ?? {};
|
|
866
|
+
}
|
|
867
|
+
this.skipFiles = /* @__PURE__ */ new Set([
|
|
868
|
+
..._TraceWatcher.STRUCTURAL_SKIP_FILES,
|
|
869
|
+
...getSkipFiles(this.userConfig)
|
|
870
|
+
]);
|
|
871
|
+
this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
|
|
872
|
+
this.allWatchDirs = [...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))];
|
|
789
873
|
this.ensureTracesDir();
|
|
790
874
|
this.loadExistingFiles();
|
|
875
|
+
this.archiveOldTraces();
|
|
791
876
|
this.startWatching();
|
|
877
|
+
setInterval(() => this.archiveOldTraces(), 6 * 60 * 60 * 1e3);
|
|
878
|
+
}
|
|
879
|
+
/** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
|
|
880
|
+
archiveOldTraces() {
|
|
881
|
+
const cutoff = Date.now() - this.maxAgeMs;
|
|
882
|
+
let archived = 0;
|
|
883
|
+
for (const dir of this.allWatchDirs) {
|
|
884
|
+
if (!fs.existsSync(dir)) continue;
|
|
885
|
+
try {
|
|
886
|
+
this.archiveDirectory(dir, cutoff, 0);
|
|
887
|
+
} catch (error) {
|
|
888
|
+
console.warn(`Archival error in ${dir}:`, error.message);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
archiveDirectory(dir, cutoff, depth) {
|
|
893
|
+
if (depth > 10) return 0;
|
|
894
|
+
if (path.basename(dir) === "archive") return 0;
|
|
895
|
+
let archived = 0;
|
|
896
|
+
try {
|
|
897
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
898
|
+
for (const entry of entries) {
|
|
899
|
+
if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
|
|
900
|
+
const fullPath = path.join(dir, entry.name);
|
|
901
|
+
if (entry.isDirectory()) {
|
|
902
|
+
archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
|
|
906
|
+
try {
|
|
907
|
+
const stats = fs.statSync(fullPath);
|
|
908
|
+
if (stats.mtimeMs >= cutoff) continue;
|
|
909
|
+
const mtime = new Date(stats.mtimeMs);
|
|
910
|
+
const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
|
|
911
|
+
const archiveDir = path.join(this.tracesDir, "archive", yearMonth);
|
|
912
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
913
|
+
const dest = path.join(archiveDir, entry.name);
|
|
914
|
+
fs.renameSync(fullPath, dest);
|
|
915
|
+
const key = this.traceKey(fullPath);
|
|
916
|
+
this.traces.delete(key);
|
|
917
|
+
archived++;
|
|
918
|
+
} catch {
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
} catch {
|
|
922
|
+
}
|
|
923
|
+
return archived;
|
|
792
924
|
}
|
|
793
925
|
ensureTracesDir() {
|
|
794
926
|
if (!fs.existsSync(this.tracesDir)) {
|
|
@@ -821,9 +953,17 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
821
953
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
822
954
|
for (const entry of entries) {
|
|
823
955
|
if (entry.name.startsWith(".")) continue;
|
|
956
|
+
if (entry.name === "archive") continue;
|
|
957
|
+
if (this.userSkipDirs.has(entry.name)) continue;
|
|
824
958
|
const fullPath = path.join(dir, entry.name);
|
|
825
959
|
if (entry.isFile()) {
|
|
826
960
|
if (this.isSupportedFile(entry.name)) {
|
|
961
|
+
try {
|
|
962
|
+
const mtime = fs.statSync(fullPath).mtimeMs;
|
|
963
|
+
if (Date.now() - mtime > this.maxAgeMs) continue;
|
|
964
|
+
} catch {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
827
967
|
if (this.loadFile(fullPath)) {
|
|
828
968
|
fileCount++;
|
|
829
969
|
}
|
|
@@ -841,8 +981,8 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
841
981
|
isSupportedFile(filename) {
|
|
842
982
|
return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
|
|
843
983
|
}
|
|
844
|
-
/**
|
|
845
|
-
static
|
|
984
|
+
/** Structural file names that are never trace data — always skipped. */
|
|
985
|
+
static STRUCTURAL_SKIP_FILES = /* @__PURE__ */ new Set([
|
|
846
986
|
"workers.json",
|
|
847
987
|
"package.json",
|
|
848
988
|
"package-lock.json",
|
|
@@ -857,6 +997,10 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
857
997
|
"update-check.json",
|
|
858
998
|
"exec-approvals.json"
|
|
859
999
|
]);
|
|
1000
|
+
/** Skip files = structural + user config */
|
|
1001
|
+
skipFiles;
|
|
1002
|
+
/** Skip directories from user config */
|
|
1003
|
+
userSkipDirs;
|
|
860
1004
|
static SKIP_SUFFIXES = [
|
|
861
1005
|
"-state.json",
|
|
862
1006
|
"-config.json",
|
|
@@ -868,7 +1012,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
868
1012
|
/** Load a file using the adapter registry, falling back to built-in parsing. */
|
|
869
1013
|
loadFile(filePath) {
|
|
870
1014
|
const filename = path.basename(filePath);
|
|
871
|
-
if (
|
|
1015
|
+
if (this.skipFiles.has(filename)) return false;
|
|
872
1016
|
if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
|
|
873
1017
|
const adapter = findAdapter(filePath);
|
|
874
1018
|
if (adapter && adapter.name !== "agentflow") {
|
|
@@ -1045,43 +1189,26 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1045
1189
|
}
|
|
1046
1190
|
return traces;
|
|
1047
1191
|
}
|
|
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
|
-
};
|
|
1192
|
+
/** Normalise agent identifiers using config-driven alias map. */
|
|
1070
1193
|
normaliseAgentId(raw) {
|
|
1071
|
-
|
|
1194
|
+
const aliases = getAliases(this.userConfig);
|
|
1195
|
+
return aliases[raw] ?? raw;
|
|
1072
1196
|
}
|
|
1073
1197
|
detectAgentIdentifier(activity, _filename, filePath) {
|
|
1074
1198
|
if (activity.agent_id) {
|
|
1075
|
-
|
|
1076
|
-
if (agentId === "main" && filePath.includes(".alfred/")) return this.normaliseAgentId("alfred-main");
|
|
1077
|
-
return this.normaliseAgentId(agentId);
|
|
1199
|
+
return this.normaliseAgentId(activity.agent_id);
|
|
1078
1200
|
}
|
|
1079
1201
|
const pathAgent = this.extractAgentFromPath(filePath);
|
|
1080
|
-
|
|
1202
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1203
|
+
if (detection.filePatterns) {
|
|
1081
1204
|
const basename3 = path.basename(filePath, path.extname(filePath));
|
|
1082
|
-
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1205
|
+
for (const [pattern, template] of Object.entries(detection.filePatterns)) {
|
|
1206
|
+
const re = new RegExp(`^(${pattern})$`);
|
|
1207
|
+
const match = basename3.match(re);
|
|
1208
|
+
if (match) {
|
|
1209
|
+
const resolved = template.replace("${match}", match[1]);
|
|
1210
|
+
return this.normaliseAgentId(resolved);
|
|
1211
|
+
}
|
|
1085
1212
|
}
|
|
1086
1213
|
}
|
|
1087
1214
|
return this.normaliseAgentId(pathAgent);
|
|
@@ -1089,20 +1216,23 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1089
1216
|
extractAgentFromPath(filePath) {
|
|
1090
1217
|
const filename = path.basename(filePath, path.extname(filePath));
|
|
1091
1218
|
const pathParts = filePath.split(path.sep);
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1219
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1220
|
+
let pathPrefix = "";
|
|
1221
|
+
if (detection.pathPatterns) {
|
|
1222
|
+
for (const [pathSubstring, agentId] of Object.entries(detection.pathPatterns)) {
|
|
1223
|
+
if (filePath.includes(pathSubstring)) {
|
|
1224
|
+
pathPrefix = agentId;
|
|
1225
|
+
break;
|
|
1226
|
+
}
|
|
1099
1227
|
}
|
|
1100
|
-
return "openclaw";
|
|
1101
1228
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1229
|
+
const agentsIndex = pathParts.lastIndexOf("agents");
|
|
1230
|
+
if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
|
|
1231
|
+
const agentName = pathParts[agentsIndex + 1];
|
|
1232
|
+
return pathPrefix ? `${pathPrefix}-${agentName}` : agentName;
|
|
1104
1233
|
}
|
|
1105
|
-
|
|
1234
|
+
if (pathPrefix) return pathPrefix;
|
|
1235
|
+
for (const part of [...pathParts].reverse()) {
|
|
1106
1236
|
if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
|
|
1107
1237
|
return part;
|
|
1108
1238
|
}
|
|
@@ -1437,19 +1567,22 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1437
1567
|
const parentDir = path.basename(path.dirname(filePath));
|
|
1438
1568
|
const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
|
|
1439
1569
|
const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
|
|
1440
|
-
let
|
|
1570
|
+
let agentName;
|
|
1441
1571
|
if (parentDir === "sessions" && greatGrandParentDir === "agents") {
|
|
1442
|
-
|
|
1572
|
+
agentName = grandParentDir;
|
|
1443
1573
|
} else if (grandParentDir === "agents") {
|
|
1444
|
-
|
|
1445
|
-
} else if (parentDir === "runs" && grandParentDir === "cron") {
|
|
1446
|
-
agentId = "openclaw-cron";
|
|
1574
|
+
agentName = parentDir;
|
|
1447
1575
|
} else {
|
|
1448
|
-
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1576
|
+
agentName = parentDir;
|
|
1577
|
+
}
|
|
1578
|
+
let agentId = agentName;
|
|
1579
|
+
const detection = getAgentDetection(this.userConfig);
|
|
1580
|
+
if (detection.pathPatterns) {
|
|
1581
|
+
for (const [pathSubstring, prefix] of Object.entries(detection.pathPatterns)) {
|
|
1582
|
+
if (filePath.includes(pathSubstring)) {
|
|
1583
|
+
agentId = `${prefix}-${agentName}`;
|
|
1584
|
+
break;
|
|
1585
|
+
}
|
|
1453
1586
|
}
|
|
1454
1587
|
}
|
|
1455
1588
|
const modelEvent = rawEvents.find((e) => e.type === "model_change");
|
|
@@ -1738,6 +1871,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1738
1871
|
edges: [],
|
|
1739
1872
|
events: [],
|
|
1740
1873
|
startTime,
|
|
1874
|
+
status,
|
|
1741
1875
|
agentId,
|
|
1742
1876
|
trigger,
|
|
1743
1877
|
name: rootName,
|
|
@@ -1858,8 +1992,12 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1858
1992
|
// Ignore git directories
|
|
1859
1993
|
/\.vscode/,
|
|
1860
1994
|
// Ignore vscode
|
|
1861
|
-
/\.idea
|
|
1995
|
+
/\.idea/,
|
|
1862
1996
|
// Ignore idea
|
|
1997
|
+
/\/archive\//,
|
|
1998
|
+
// Ignore archived trace files
|
|
1999
|
+
// Ignore user-configured skip directories
|
|
2000
|
+
...getSkipDirectories(this.userConfig).map((d) => new RegExp(`/${d}/`))
|
|
1863
2001
|
],
|
|
1864
2002
|
persistent: true,
|
|
1865
2003
|
ignoreInitial: true,
|
|
@@ -1913,29 +2051,42 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1913
2051
|
});
|
|
1914
2052
|
}
|
|
1915
2053
|
getTrace(filename) {
|
|
2054
|
+
const candidates = [];
|
|
1916
2055
|
const exact = this.traces.get(filename);
|
|
1917
|
-
if (exact)
|
|
2056
|
+
if (exact) candidates.push(exact);
|
|
1918
2057
|
if (filename.includes("::")) {
|
|
1919
2058
|
const [fname, startTimeStr] = filename.split("::");
|
|
1920
2059
|
const startTime = Number(startTimeStr);
|
|
1921
2060
|
if (fname && !Number.isNaN(startTime)) {
|
|
1922
2061
|
for (const trace of this.traces.values()) {
|
|
1923
2062
|
if (trace.filename === fname && trace.startTime === startTime) {
|
|
1924
|
-
|
|
2063
|
+
candidates.push(trace);
|
|
1925
2064
|
}
|
|
1926
2065
|
}
|
|
1927
2066
|
}
|
|
1928
2067
|
}
|
|
1929
2068
|
for (const prefix of ["openclaw:", "otel:", ""]) {
|
|
1930
2069
|
const prefixed = this.traces.get(prefix + filename);
|
|
1931
|
-
if (prefixed)
|
|
2070
|
+
if (prefixed) candidates.push(prefixed);
|
|
1932
2071
|
}
|
|
1933
2072
|
for (const [key, trace] of this.traces) {
|
|
1934
2073
|
if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
|
|
1935
|
-
|
|
2074
|
+
candidates.push(trace);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
if (candidates.length === 0) return void 0;
|
|
2078
|
+
if (candidates.length === 1) return candidates[0];
|
|
2079
|
+
let best = candidates[0];
|
|
2080
|
+
let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
|
|
2081
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
2082
|
+
const c = candidates[i];
|
|
2083
|
+
const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
|
|
2084
|
+
if (nc > bestNodeCount) {
|
|
2085
|
+
best = c;
|
|
2086
|
+
bestNodeCount = nc;
|
|
1936
2087
|
}
|
|
1937
2088
|
}
|
|
1938
|
-
return
|
|
2089
|
+
return best;
|
|
1939
2090
|
}
|
|
1940
2091
|
getTracesByAgent(agentId) {
|
|
1941
2092
|
return this.getAllTraces().filter((trace) => trace.agentId === agentId);
|
|
@@ -1982,7 +2133,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1982
2133
|
import * as fs2 from "fs";
|
|
1983
2134
|
import * as os from "os";
|
|
1984
2135
|
import * as path2 from "path";
|
|
1985
|
-
var VERSION = "0.
|
|
2136
|
+
var VERSION = "0.8.0";
|
|
1986
2137
|
function getLanAddress() {
|
|
1987
2138
|
const interfaces = os.networkInterfaces();
|
|
1988
2139
|
for (const name of Object.keys(interfaces)) {
|
|
@@ -1994,7 +2145,7 @@ function getLanAddress() {
|
|
|
1994
2145
|
}
|
|
1995
2146
|
return null;
|
|
1996
2147
|
}
|
|
1997
|
-
function printBanner(config, traceCount, stats) {
|
|
2148
|
+
function printBanner(config, traceCount, stats, configPath) {
|
|
1998
2149
|
var _a;
|
|
1999
2150
|
const lan = getLanAddress();
|
|
2000
2151
|
const host = config.host || "localhost";
|
|
@@ -2010,26 +2161,25 @@ function printBanner(config, traceCount, stats) {
|
|
|
2010
2161
|
|
|
2011
2162
|
See your agents think.
|
|
2012
2163
|
|
|
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
2164
|
Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? `
|
|
2025
2165
|
Data dirs: ${config.dataDirs.join("\n ")}` : ""}
|
|
2026
2166
|
Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
|
|
2027
2167
|
Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
|
|
2168
|
+
Config: ${configPath ?? "none (using defaults)"}
|
|
2028
2169
|
CORS: ${config.enableCors ? "enabled" : "disabled"}
|
|
2029
2170
|
WebSocket: live updates enabled
|
|
2171
|
+
Window: ${process.env.AGENTFLOW_TRACE_WINDOW_HOURS ?? "48"}h (set AGENTFLOW_TRACE_WINDOW_HOURS to change)
|
|
2030
2172
|
|
|
2031
2173
|
\u2192 http://localhost:${port}${isPublic && lan ? `
|
|
2032
2174
|
\u2192 http://${lan}:${port} (LAN)` : ""}
|
|
2175
|
+
|
|
2176
|
+
Pages: Agents \xB7 SOMA
|
|
2177
|
+
Agent: Profile \xB7 Execution Detail
|
|
2178
|
+
SOMA: Intelligence \xB7 Review \xB7 Policies \xB7 Knowledge \xB7 Activity
|
|
2179
|
+
Tabs: Flame Chart \xB7 Agent Flow \xB7 Metrics \xB7 Dependencies
|
|
2180
|
+
State Machine \xB7 Summary \xB7 Transcript
|
|
2181
|
+
|
|
2182
|
+
Runs locally. Your data never leaves your machine.
|
|
2033
2183
|
`);
|
|
2034
2184
|
}
|
|
2035
2185
|
async function startDashboard() {
|
|
@@ -2067,6 +2217,12 @@ async function startDashboard() {
|
|
|
2067
2217
|
case "--collector-token":
|
|
2068
2218
|
config.collectorAuthToken = args[++i];
|
|
2069
2219
|
break;
|
|
2220
|
+
case "--soma-vault":
|
|
2221
|
+
config.somaVault = args[++i];
|
|
2222
|
+
break;
|
|
2223
|
+
case "--config":
|
|
2224
|
+
config.configPath = args[++i];
|
|
2225
|
+
break;
|
|
2070
2226
|
case "--help":
|
|
2071
2227
|
printHelp();
|
|
2072
2228
|
process.exit(0);
|
|
@@ -2078,6 +2234,9 @@ async function startDashboard() {
|
|
|
2078
2234
|
if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
|
|
2079
2235
|
config.enableCollector = false;
|
|
2080
2236
|
}
|
|
2237
|
+
if (!config.somaVault && process.env.SOMA_VAULT) {
|
|
2238
|
+
config.somaVault = process.env.SOMA_VAULT;
|
|
2239
|
+
}
|
|
2081
2240
|
const tracesPath = path2.resolve(config.tracesDir);
|
|
2082
2241
|
if (!fs2.existsSync(tracesPath)) {
|
|
2083
2242
|
fs2.mkdirSync(tracesPath, { recursive: true });
|
|
@@ -2099,7 +2258,7 @@ async function startDashboard() {
|
|
|
2099
2258
|
setTimeout(() => {
|
|
2100
2259
|
const stats = dashboard.getStats();
|
|
2101
2260
|
const traces = dashboard.getTraces();
|
|
2102
|
-
printBanner(config, traces.length, stats);
|
|
2261
|
+
printBanner(config, traces.length, stats, dashboard.getConfigPath());
|
|
2103
2262
|
}, 1500);
|
|
2104
2263
|
} catch (error) {
|
|
2105
2264
|
console.error("\u274C Failed to start dashboard:", error);
|
|
@@ -2108,7 +2267,7 @@ async function startDashboard() {
|
|
|
2108
2267
|
}
|
|
2109
2268
|
function printHelp() {
|
|
2110
2269
|
console.log(`
|
|
2111
|
-
|
|
2270
|
+
AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
|
|
2112
2271
|
|
|
2113
2272
|
Usage:
|
|
2114
2273
|
agentflow-dashboard [options]
|
|
@@ -2119,22 +2278,34 @@ Options:
|
|
|
2119
2278
|
-t, --traces <path> Traces directory (default: ./traces)
|
|
2120
2279
|
-h, --host <address> Host address (default: localhost)
|
|
2121
2280
|
--data-dir <path> Extra data directory for process discovery (repeatable)
|
|
2281
|
+
--config <path> Path to agentflow.config.json (aliases, skip files, etc.)
|
|
2282
|
+
--soma-vault <path> SOMA vault directory for intelligence data
|
|
2122
2283
|
--cors Enable CORS headers
|
|
2123
2284
|
--no-collector Disable OTLP trace collector (POST /v1/traces)
|
|
2124
2285
|
--collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
|
|
2125
2286
|
--help Show this help message
|
|
2126
2287
|
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2288
|
+
Config file:
|
|
2289
|
+
The dashboard loads agentflow.config.json for agent aliases, skip files,
|
|
2290
|
+
discovery paths, and systemd services. Resolution order:
|
|
2291
|
+
1. --config flag
|
|
2292
|
+
2. AGENTFLOW_CONFIG env var
|
|
2293
|
+
3. ./agentflow.config.json
|
|
2294
|
+
4. ~/.config/agentflow/config.json
|
|
2131
2295
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2296
|
+
See agentflow.config.example.json for a complete reference.
|
|
2297
|
+
|
|
2298
|
+
Environment:
|
|
2299
|
+
AGENTFLOW_CONFIG Path to config file
|
|
2300
|
+
AGENTFLOW_TRACE_WINDOW_HOURS Max age of traces to load (default: 48)
|
|
2301
|
+
AGENTFLOW_COLLECTOR_TOKEN Auth token for OTLP collector
|
|
2302
|
+
AGENTFLOW_NO_COLLECTOR=true Disable OTLP collector
|
|
2303
|
+
SOMA_VAULT SOMA vault directory
|
|
2304
|
+
|
|
2305
|
+
Examples:
|
|
2306
|
+
agentflow-dashboard --traces ./traces --host 0.0.0.0
|
|
2307
|
+
agentflow-dashboard --traces ./traces --config ./agentflow.config.json
|
|
2308
|
+
agentflow-dashboard -p 8080 -t /var/log/agentflow --cors
|
|
2138
2309
|
`);
|
|
2139
2310
|
}
|
|
2140
2311
|
|
|
@@ -2156,12 +2327,15 @@ function serializeTrace(trace) {
|
|
|
2156
2327
|
var DashboardServer = class {
|
|
2157
2328
|
constructor(config) {
|
|
2158
2329
|
this.config = config;
|
|
2159
|
-
const
|
|
2160
|
-
|
|
2330
|
+
const { config: userCfg, configPath: cfgPath } = loadConfig(config.configPath);
|
|
2331
|
+
this.userConfig = userCfg;
|
|
2332
|
+
this.configPath = cfgPath;
|
|
2333
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
2334
|
+
const dashConfigPath = path3.join(home, ".agentflow/dashboard-config.json");
|
|
2161
2335
|
if (!config.dataDirs) config.dataDirs = [];
|
|
2162
2336
|
try {
|
|
2163
|
-
if (fs3.existsSync(
|
|
2164
|
-
const saved = JSON.parse(fs3.readFileSync(
|
|
2337
|
+
if (fs3.existsSync(dashConfigPath)) {
|
|
2338
|
+
const saved = JSON.parse(fs3.readFileSync(dashConfigPath, "utf-8"));
|
|
2165
2339
|
const extraDirs = saved.extraDirs ?? [];
|
|
2166
2340
|
for (const d of extraDirs) {
|
|
2167
2341
|
if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
|
|
@@ -2169,21 +2343,15 @@ var DashboardServer = class {
|
|
|
2169
2343
|
}
|
|
2170
2344
|
} catch {
|
|
2171
2345
|
}
|
|
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) {
|
|
2346
|
+
for (const p of getDiscoveryPaths(this.userConfig)) {
|
|
2180
2347
|
if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
|
|
2181
2348
|
config.dataDirs.push(p);
|
|
2182
2349
|
}
|
|
2183
2350
|
}
|
|
2184
2351
|
this.watcher = new TraceWatcher({
|
|
2185
2352
|
tracesDir: config.tracesDir,
|
|
2186
|
-
dataDirs: config.dataDirs
|
|
2353
|
+
dataDirs: config.dataDirs,
|
|
2354
|
+
userConfig: this.userConfig
|
|
2187
2355
|
});
|
|
2188
2356
|
this.stats = new AgentStats();
|
|
2189
2357
|
this.knowledgeStore = createKnowledgeStore({
|
|
@@ -2192,6 +2360,7 @@ var DashboardServer = class {
|
|
|
2192
2360
|
this.setupExpress();
|
|
2193
2361
|
this.setupWebSocket();
|
|
2194
2362
|
this.setupTraceWatcher();
|
|
2363
|
+
this.setupSomaReportWatcher();
|
|
2195
2364
|
let knowledgeCount = 0;
|
|
2196
2365
|
for (const trace of this.watcher.getAllTraces()) {
|
|
2197
2366
|
this.stats.processTrace(trace);
|
|
@@ -2218,6 +2387,8 @@ var DashboardServer = class {
|
|
|
2218
2387
|
ts: 0
|
|
2219
2388
|
};
|
|
2220
2389
|
knowledgeStore;
|
|
2390
|
+
userConfig;
|
|
2391
|
+
configPath;
|
|
2221
2392
|
setupExpress() {
|
|
2222
2393
|
if (this.config.enableCors) {
|
|
2223
2394
|
this.app.use((_req, res, next) => {
|
|
@@ -2229,18 +2400,35 @@ var DashboardServer = class {
|
|
|
2229
2400
|
next();
|
|
2230
2401
|
});
|
|
2231
2402
|
}
|
|
2232
|
-
const
|
|
2403
|
+
const pkgDir = path3.join(__dirname, "..");
|
|
2404
|
+
const clientDir = path3.join(pkgDir, "dist/client");
|
|
2405
|
+
const clientIndex = path3.join(clientDir, "index.html");
|
|
2406
|
+
const srcDir = path3.join(pkgDir, "src/client");
|
|
2407
|
+
const needsBuild = !fs3.existsSync(clientIndex) || fs3.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
|
|
2408
|
+
if (needsBuild) {
|
|
2409
|
+
try {
|
|
2410
|
+
console.log("Building dashboard client...");
|
|
2411
|
+
execSync("npm run build:client", { cwd: pkgDir, stdio: "inherit", timeout: 3e4 });
|
|
2412
|
+
} catch (err) {
|
|
2413
|
+
console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2233
2416
|
if (fs3.existsSync(clientDir)) {
|
|
2234
2417
|
this.app.use(express.static(clientDir));
|
|
2235
2418
|
}
|
|
2236
|
-
|
|
2237
|
-
if (fs3.existsSync(publicDir)) {
|
|
2238
|
-
this.app.use("/v1", express.static(publicDir));
|
|
2239
|
-
}
|
|
2240
|
-
this.app.get("/api/traces", (_req, res) => {
|
|
2419
|
+
this.app.get("/api/traces", (req, res) => {
|
|
2241
2420
|
try {
|
|
2242
|
-
const
|
|
2243
|
-
|
|
2421
|
+
const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
|
|
2422
|
+
const cursor = req.query.cursor ? parseFloat(req.query.cursor) : void 0;
|
|
2423
|
+
let allTraces = this.watcher.getAllTraces();
|
|
2424
|
+
if (cursor) {
|
|
2425
|
+
allTraces = allTraces.filter((t) => (t.lastModified || t.startTime) < cursor);
|
|
2426
|
+
}
|
|
2427
|
+
const page = allTraces.slice(0, limit);
|
|
2428
|
+
const serialized = page.map(serializeTrace);
|
|
2429
|
+
const lastTrace = page[page.length - 1];
|
|
2430
|
+
const nextCursor = page.length === limit && lastTrace ? lastTrace.lastModified || lastTrace.startTime : null;
|
|
2431
|
+
res.json({ traces: serialized, nextCursor });
|
|
2244
2432
|
} catch (_error) {
|
|
2245
2433
|
res.status(500).json({ error: "Failed to load traces" });
|
|
2246
2434
|
}
|
|
@@ -2531,6 +2719,235 @@ var DashboardServer = class {
|
|
|
2531
2719
|
res.status(500).json({ error: "Failed to load agent statistics" });
|
|
2532
2720
|
}
|
|
2533
2721
|
});
|
|
2722
|
+
this.app.get("/api/soma/tier", (_req, res) => {
|
|
2723
|
+
const somaVault = this.config.somaVault;
|
|
2724
|
+
if (!somaVault) {
|
|
2725
|
+
return res.json({ tier: "teaser", somaVault: false, governanceAvailable: false });
|
|
2726
|
+
}
|
|
2727
|
+
try {
|
|
2728
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2729
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2730
|
+
return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
|
|
2731
|
+
}
|
|
2732
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2733
|
+
const hasGovernance = report.governance && typeof report.governance.pending === "number";
|
|
2734
|
+
return res.json({
|
|
2735
|
+
tier: hasGovernance ? "pro" : "free",
|
|
2736
|
+
somaVault: true,
|
|
2737
|
+
governanceAvailable: !!hasGovernance
|
|
2738
|
+
});
|
|
2739
|
+
} catch {
|
|
2740
|
+
return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
|
|
2741
|
+
}
|
|
2742
|
+
});
|
|
2743
|
+
this.app.get("/api/soma/report", (_req, res) => {
|
|
2744
|
+
const somaVault = this.config.somaVault;
|
|
2745
|
+
if (!somaVault) {
|
|
2746
|
+
return res.json({ available: false, teaser: true });
|
|
2747
|
+
}
|
|
2748
|
+
try {
|
|
2749
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2750
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2751
|
+
return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
|
|
2752
|
+
}
|
|
2753
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2754
|
+
res.json(report);
|
|
2755
|
+
} catch (error) {
|
|
2756
|
+
console.error("Soma report error:", error);
|
|
2757
|
+
res.json({ available: false, teaser: false, message: "Failed to read report" });
|
|
2758
|
+
}
|
|
2759
|
+
});
|
|
2760
|
+
this.app.get("/api/soma/governance", (_req, res) => {
|
|
2761
|
+
const somaVault = this.config.somaVault;
|
|
2762
|
+
if (!somaVault) {
|
|
2763
|
+
return res.json({ available: false });
|
|
2764
|
+
}
|
|
2765
|
+
try {
|
|
2766
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2767
|
+
if (!fs3.existsSync(reportPath)) {
|
|
2768
|
+
return res.json({ available: false, message: "No report file. Run soma report." });
|
|
2769
|
+
}
|
|
2770
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2771
|
+
res.json({
|
|
2772
|
+
available: true,
|
|
2773
|
+
layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
|
|
2774
|
+
governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
|
|
2775
|
+
insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
|
|
2776
|
+
canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
|
|
2777
|
+
generatedAt: report.generatedAt
|
|
2778
|
+
});
|
|
2779
|
+
} catch (error) {
|
|
2780
|
+
console.error("Soma governance error:", error);
|
|
2781
|
+
res.status(500).json({ available: false, message: "Failed to read governance data" });
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
|
|
2785
|
+
const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
|
|
2786
|
+
this.app.post("/api/soma/governance/promote", (req, res) => {
|
|
2787
|
+
var _a;
|
|
2788
|
+
const somaVault = this.config.somaVault;
|
|
2789
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2790
|
+
const { entryId } = req.body ?? {};
|
|
2791
|
+
if (!entryId) return res.status(400).json({ error: "entryId required" });
|
|
2792
|
+
try {
|
|
2793
|
+
const { execSync: execSync2 } = __require("child_process");
|
|
2794
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2795
|
+
const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
|
|
2796
|
+
encoding: "utf-8",
|
|
2797
|
+
timeout: 1e4
|
|
2798
|
+
});
|
|
2799
|
+
res.json({ success: true, message: result.trim() });
|
|
2800
|
+
} catch (error) {
|
|
2801
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2802
|
+
}
|
|
2803
|
+
});
|
|
2804
|
+
this.app.post("/api/soma/governance/reject", (req, res) => {
|
|
2805
|
+
var _a;
|
|
2806
|
+
const somaVault = this.config.somaVault;
|
|
2807
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2808
|
+
const { entryId, reason } = req.body ?? {};
|
|
2809
|
+
if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
|
|
2810
|
+
try {
|
|
2811
|
+
const { execSync: execSync2 } = __require("child_process");
|
|
2812
|
+
const safeId = sanitizeArg(String(entryId));
|
|
2813
|
+
const safeReason = sanitizeReason(String(reason));
|
|
2814
|
+
const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
|
|
2815
|
+
encoding: "utf-8",
|
|
2816
|
+
timeout: 1e4
|
|
2817
|
+
});
|
|
2818
|
+
res.json({ success: true, message: result.trim() });
|
|
2819
|
+
} catch (error) {
|
|
2820
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2821
|
+
}
|
|
2822
|
+
});
|
|
2823
|
+
this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
|
|
2824
|
+
var _a;
|
|
2825
|
+
const somaVault = this.config.somaVault;
|
|
2826
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2827
|
+
try {
|
|
2828
|
+
const { execSync: execSync2 } = __require("child_process");
|
|
2829
|
+
const safeId = sanitizeArg(String(req.params.id));
|
|
2830
|
+
const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
|
|
2831
|
+
encoding: "utf-8",
|
|
2832
|
+
timeout: 1e4
|
|
2833
|
+
});
|
|
2834
|
+
res.json({ available: true, output: result.trim() });
|
|
2835
|
+
} catch (error) {
|
|
2836
|
+
res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2837
|
+
}
|
|
2838
|
+
});
|
|
2839
|
+
this.app.get("/api/soma/policies", (_req, res) => {
|
|
2840
|
+
const somaVault = this.config.somaVault;
|
|
2841
|
+
if (!somaVault) return res.json({ policies: [] });
|
|
2842
|
+
try {
|
|
2843
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2844
|
+
if (!fs3.existsSync(reportPath)) return res.json({ policies: [] });
|
|
2845
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2846
|
+
res.json({ policies: report.policies ?? [] });
|
|
2847
|
+
} catch {
|
|
2848
|
+
res.json({ policies: [] });
|
|
2849
|
+
}
|
|
2850
|
+
});
|
|
2851
|
+
this.app.post("/api/soma/policies", express.json(), (req, res) => {
|
|
2852
|
+
var _a;
|
|
2853
|
+
const somaVault = this.config.somaVault;
|
|
2854
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2855
|
+
const { name, enforcement, scope, conditions } = req.body ?? {};
|
|
2856
|
+
if (!name) return res.status(400).json({ error: "name required" });
|
|
2857
|
+
try {
|
|
2858
|
+
const safeName = sanitizeArg(String(name));
|
|
2859
|
+
const safeEnf = sanitizeArg(String(enforcement || "warn"));
|
|
2860
|
+
const safeScope = sanitizeReason(String(scope || "all"));
|
|
2861
|
+
const safeCond = sanitizeReason(String(conditions || ""));
|
|
2862
|
+
const result = execSync(
|
|
2863
|
+
`npx soma policy create "${safeName}" --enforcement ${safeEnf} --scope "${safeScope}" --conditions "${safeCond}" --vault "${somaVault}"`,
|
|
2864
|
+
{ encoding: "utf-8", timeout: 1e4 }
|
|
2865
|
+
);
|
|
2866
|
+
res.json({ success: true, message: result.trim() });
|
|
2867
|
+
} catch (error) {
|
|
2868
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
this.app.delete("/api/soma/policies/:name", (req, res) => {
|
|
2872
|
+
var _a;
|
|
2873
|
+
const somaVault = this.config.somaVault;
|
|
2874
|
+
if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
|
|
2875
|
+
try {
|
|
2876
|
+
const safeName = sanitizeArg(String(req.params.name));
|
|
2877
|
+
const result = execSync(
|
|
2878
|
+
`npx soma policy delete "${safeName}" --vault "${somaVault}"`,
|
|
2879
|
+
{ encoding: "utf-8", timeout: 1e4 }
|
|
2880
|
+
);
|
|
2881
|
+
res.json({ success: true, message: result.trim() });
|
|
2882
|
+
} catch (error) {
|
|
2883
|
+
res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
|
|
2884
|
+
}
|
|
2885
|
+
});
|
|
2886
|
+
this.app.get("/api/soma/vault/entities", (req, res) => {
|
|
2887
|
+
const somaVault = this.config.somaVault;
|
|
2888
|
+
if (!somaVault) return res.json({ entities: [], total: 0 });
|
|
2889
|
+
try {
|
|
2890
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2891
|
+
if (!fs3.existsSync(reportPath)) return res.json({ entities: [], total: 0 });
|
|
2892
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2893
|
+
let entities = [
|
|
2894
|
+
...(report.agents ?? []).map((a) => ({ ...a, type: "agent", id: a.name })),
|
|
2895
|
+
...(report.insights ?? []).map((i, idx) => {
|
|
2896
|
+
var _a;
|
|
2897
|
+
return { ...i, type: i.type || "insight", id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}` };
|
|
2898
|
+
}),
|
|
2899
|
+
...(report.policies ?? []).map((p) => ({ ...p, type: "policy", id: p.name }))
|
|
2900
|
+
];
|
|
2901
|
+
const { type, layer, q, limit: limitStr, offset: offsetStr } = req.query;
|
|
2902
|
+
if (type) entities = entities.filter((e) => e.type === type);
|
|
2903
|
+
if (layer) entities = entities.filter((e) => e.layer === layer);
|
|
2904
|
+
if (q) {
|
|
2905
|
+
const lq = q.toLowerCase();
|
|
2906
|
+
entities = entities.filter((e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq));
|
|
2907
|
+
}
|
|
2908
|
+
const total = entities.length;
|
|
2909
|
+
const offset = parseInt(offsetStr || "0", 10);
|
|
2910
|
+
const limit = Math.min(parseInt(limitStr || "50", 10), 200);
|
|
2911
|
+
entities = entities.slice(offset, offset + limit);
|
|
2912
|
+
res.json({ entities, total });
|
|
2913
|
+
} catch (error) {
|
|
2914
|
+
console.error("Vault entities error:", error);
|
|
2915
|
+
res.json({ entities: [], total: 0 });
|
|
2916
|
+
}
|
|
2917
|
+
});
|
|
2918
|
+
this.app.get("/api/soma/vault/entities/:type/:id", (req, res) => {
|
|
2919
|
+
const somaVault = this.config.somaVault;
|
|
2920
|
+
if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
|
|
2921
|
+
try {
|
|
2922
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
2923
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
2924
|
+
const { type, id } = req.params;
|
|
2925
|
+
let entity = null;
|
|
2926
|
+
if (type === "agent") {
|
|
2927
|
+
entity = (report.agents ?? []).find((a) => a.name === id);
|
|
2928
|
+
} else if (type === "policy") {
|
|
2929
|
+
entity = (report.policies ?? []).find((p) => p.name === id);
|
|
2930
|
+
} else {
|
|
2931
|
+
entity = (report.insights ?? []).find(
|
|
2932
|
+
(i) => {
|
|
2933
|
+
var _a;
|
|
2934
|
+
return (((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || "") === id || i.title === id;
|
|
2935
|
+
}
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
if (!entity) return res.status(404).json({ error: "Entity not found" });
|
|
2939
|
+
res.json({
|
|
2940
|
+
...entity,
|
|
2941
|
+
type,
|
|
2942
|
+
id,
|
|
2943
|
+
body: entity.claim || entity.conditions || "",
|
|
2944
|
+
tags: entity.tags ?? [],
|
|
2945
|
+
related: entity.related ?? []
|
|
2946
|
+
});
|
|
2947
|
+
} catch {
|
|
2948
|
+
res.status(404).json({ error: "Entity not found" });
|
|
2949
|
+
}
|
|
2950
|
+
});
|
|
2534
2951
|
this.app.get("/api/process-health", (_req, res) => {
|
|
2535
2952
|
var _a, _b;
|
|
2536
2953
|
try {
|
|
@@ -2543,7 +2960,14 @@ var DashboardServer = class {
|
|
|
2543
2960
|
path3.dirname(this.config.tracesDir),
|
|
2544
2961
|
...this.config.dataDirs || []
|
|
2545
2962
|
];
|
|
2546
|
-
|
|
2963
|
+
let configs = discoverAllProcessConfigs(discoveryDirs);
|
|
2964
|
+
const pref = getProcessPreference(this.userConfig);
|
|
2965
|
+
if (pref) {
|
|
2966
|
+
const hasPreferred = configs.some((c) => c.processName === pref.prefer);
|
|
2967
|
+
if (hasPreferred) {
|
|
2968
|
+
configs = configs.filter((c) => c.processName !== pref.over);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2547
2971
|
if (configs.length === 0) {
|
|
2548
2972
|
return res.json(null);
|
|
2549
2973
|
}
|
|
@@ -2627,35 +3051,32 @@ var DashboardServer = class {
|
|
|
2627
3051
|
}
|
|
2628
3052
|
} catch {
|
|
2629
3053
|
}
|
|
2630
|
-
const watched = [
|
|
3054
|
+
const watched = [...new Set([
|
|
2631
3055
|
this.config.tracesDir,
|
|
2632
3056
|
...this.config.dataDirs || [],
|
|
2633
3057
|
...extraDirs
|
|
2634
|
-
];
|
|
3058
|
+
].map((w) => path3.resolve(w)))];
|
|
2635
3059
|
const discovered = [];
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
const
|
|
2646
|
-
if (
|
|
3060
|
+
const svcNames = getSystemdServices(this.userConfig);
|
|
3061
|
+
if (svcNames.length > 0) {
|
|
3062
|
+
try {
|
|
3063
|
+
const { execSync: execSync2 } = __require("child_process");
|
|
3064
|
+
const raw = execSync2(
|
|
3065
|
+
`systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
|
|
3066
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
3067
|
+
);
|
|
3068
|
+
for (const line of raw.split("\n")) {
|
|
3069
|
+
const match = line.match(/path=([^\s;]+)/);
|
|
3070
|
+
if (match == null ? void 0 : match[1]) {
|
|
3071
|
+
const dir = path3.dirname(match[1]);
|
|
3072
|
+
if (fs3.existsSync(dir)) discovered.push(dir);
|
|
3073
|
+
}
|
|
2647
3074
|
}
|
|
3075
|
+
} catch {
|
|
2648
3076
|
}
|
|
2649
|
-
} catch {
|
|
2650
3077
|
}
|
|
2651
3078
|
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"),
|
|
3079
|
+
...getDiscoveryPaths(this.userConfig),
|
|
2659
3080
|
path3.join(home, ".agentflow/traces")
|
|
2660
3081
|
];
|
|
2661
3082
|
for (const p of commonPaths) {
|
|
@@ -2759,18 +3180,10 @@ var DashboardServer = class {
|
|
|
2759
3180
|
this.app.get("/ready", (_req, res) => {
|
|
2760
3181
|
res.json({ status: "ready" });
|
|
2761
3182
|
});
|
|
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
3183
|
this.app.get("*", (_req, res) => {
|
|
2771
|
-
const
|
|
2772
|
-
if (fs3.existsSync(
|
|
2773
|
-
res.sendFile(
|
|
3184
|
+
const clientIndex2 = path3.join(__dirname, "../dist/client/index.html");
|
|
3185
|
+
if (fs3.existsSync(clientIndex2)) {
|
|
3186
|
+
res.sendFile(clientIndex2);
|
|
2774
3187
|
} else {
|
|
2775
3188
|
res.status(404).send("Dashboard not found - public files may not be built");
|
|
2776
3189
|
}
|
|
@@ -2796,6 +3209,41 @@ var DashboardServer = class {
|
|
|
2796
3209
|
});
|
|
2797
3210
|
});
|
|
2798
3211
|
}
|
|
3212
|
+
/** Watch soma-report.json for changes and broadcast updates via WebSocket. */
|
|
3213
|
+
setupSomaReportWatcher() {
|
|
3214
|
+
const somaVault = this.config.somaVault;
|
|
3215
|
+
if (!somaVault) return;
|
|
3216
|
+
const reportPath = path3.join(somaVault, "..", "soma-report.json");
|
|
3217
|
+
const reportDir = path3.dirname(reportPath);
|
|
3218
|
+
if (!fs3.existsSync(reportDir)) return;
|
|
3219
|
+
let debounceTimer = null;
|
|
3220
|
+
const watcher = chokidar2.watch(reportPath, {
|
|
3221
|
+
ignoreInitial: true,
|
|
3222
|
+
persistent: true,
|
|
3223
|
+
awaitWriteFinish: { stabilityThreshold: 500 }
|
|
3224
|
+
});
|
|
3225
|
+
watcher.on("change", () => {
|
|
3226
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
3227
|
+
debounceTimer = setTimeout(() => {
|
|
3228
|
+
var _a, _b;
|
|
3229
|
+
try {
|
|
3230
|
+
const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
|
|
3231
|
+
this.broadcast({ type: "soma-report-updated", data: report });
|
|
3232
|
+
if (report.generatedAt) {
|
|
3233
|
+
this.broadcast({
|
|
3234
|
+
type: "soma-activity",
|
|
3235
|
+
data: {
|
|
3236
|
+
action: "report-updated",
|
|
3237
|
+
description: `Report updated: ${((_a = report.totals) == null ? void 0 : _a.agents) ?? 0} agents, ${((_b = report.totals) == null ? void 0 : _b.insights) ?? 0} insights`,
|
|
3238
|
+
timestamp: report.generatedAt
|
|
3239
|
+
}
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
} catch {
|
|
3243
|
+
}
|
|
3244
|
+
}, 500);
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
2799
3247
|
/**
|
|
2800
3248
|
* Filter an agent's traces to valid ExecutionGraphs and convert via loadGraph().
|
|
2801
3249
|
* Returns only traces with proper nodes (Map or non-empty object), skipping session-only traces.
|
|
@@ -3017,24 +3465,49 @@ var DashboardServer = class {
|
|
|
3017
3465
|
});
|
|
3018
3466
|
}
|
|
3019
3467
|
async start() {
|
|
3020
|
-
return new Promise((
|
|
3468
|
+
return new Promise((resolve5) => {
|
|
3021
3469
|
const host = this.config.host || "localhost";
|
|
3022
3470
|
this.server.listen(this.config.port, host, () => {
|
|
3023
3471
|
console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
|
|
3024
3472
|
console.log(`Watching traces in: ${this.config.tracesDir}`);
|
|
3025
|
-
|
|
3473
|
+
resolve5();
|
|
3026
3474
|
});
|
|
3027
3475
|
});
|
|
3028
3476
|
}
|
|
3477
|
+
/** Check if any src/client file is newer than the built bundle. */
|
|
3478
|
+
isClientStale(srcDir, distDir) {
|
|
3479
|
+
try {
|
|
3480
|
+
const distIndex = path3.join(distDir, "index.html");
|
|
3481
|
+
if (!fs3.existsSync(distIndex)) return true;
|
|
3482
|
+
const distMtime = fs3.statSync(distIndex).mtimeMs;
|
|
3483
|
+
const check = (dir) => {
|
|
3484
|
+
for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
|
|
3485
|
+
const full = path3.join(dir, entry.name);
|
|
3486
|
+
if (entry.isDirectory()) {
|
|
3487
|
+
if (check(full)) return true;
|
|
3488
|
+
} else if (fs3.statSync(full).mtimeMs > distMtime) {
|
|
3489
|
+
return true;
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
return false;
|
|
3493
|
+
};
|
|
3494
|
+
return check(srcDir);
|
|
3495
|
+
} catch {
|
|
3496
|
+
return false;
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3029
3499
|
async stop() {
|
|
3030
|
-
return new Promise((
|
|
3500
|
+
return new Promise((resolve5) => {
|
|
3031
3501
|
this.watcher.stop();
|
|
3032
3502
|
this.server.close(() => {
|
|
3033
3503
|
console.log("Dashboard server stopped");
|
|
3034
|
-
|
|
3504
|
+
resolve5();
|
|
3035
3505
|
});
|
|
3036
3506
|
});
|
|
3037
3507
|
}
|
|
3508
|
+
getConfigPath() {
|
|
3509
|
+
return this.configPath;
|
|
3510
|
+
}
|
|
3038
3511
|
getStats() {
|
|
3039
3512
|
return this.stats.getGlobalStats();
|
|
3040
3513
|
}
|