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.
@@ -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 = join(openclawDir, "cron", "jobs.json");
135
+ const jobsPath = join2(openclawDir, "cron", "jobs.json");
62
136
  const map = /* @__PURE__ */ new Map();
63
137
  try {
64
- if (existsSync(jobsPath)) {
65
- const data = JSON.parse(readFileSync(jobsPath, "utf-8"));
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 (existsSync(join(dir, "cron", "jobs.json")) || basename(dir) === ".openclaw") {
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 existsSync(join(dirPath, "cron", "jobs.json")) || dirPath.includes(".openclaw") || existsSync(join(dirPath, "cron", "runs"));
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 = readFileSync(filePath, "utf-8");
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 existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
163
- import { join as join2 } from "path";
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 (existsSync2(join2(dirPath, "otel-traces"))) return true;
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 = readFileSync2(filePath, "utf-8");
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
- if (["openclaw", "otel", "langchain", "crewai", "mastra"].includes(prefix)) {
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 group = suffixGroups.get(suffix) ?? [];
429
+ const key = `${a.source}:${suffix}`;
430
+ const group = suffixGroups.get(key) ?? [];
358
431
  group.push(a);
359
- suffixGroups.set(suffix, group);
432
+ suffixGroups.set(key, group);
360
433
  }
361
434
  const mergedIds = /* @__PURE__ */ new Set();
362
435
  const mergedAgents = [];
363
- for (const [suffix, group] of suffixGroups) {
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
- openclaw: "OpenClaw",
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
- /** File names that are config/state, not tracesskip them. */
845
- static SKIP_FILES = /* @__PURE__ */ new Set([
983
+ /** Structural file names that are never trace dataalways 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 (_TraceWatcher.SKIP_FILES.has(filename)) return false;
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
- return _TraceWatcher.AGENT_ALIASES[raw] ?? raw;
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
- const agentId = activity.agent_id;
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
- if (filePath.includes(".alfred/") && !pathAgent.startsWith("alfred-")) {
1201
+ const detection = getAgentDetection(this.userConfig);
1202
+ if (detection.filePatterns) {
1081
1203
  const basename3 = path.basename(filePath, path.extname(filePath));
1082
- if (basename3.match(/^(janitor|curator|distiller|surveyor|alfred)$/)) {
1083
- const raw = basename3 === "alfred" ? "alfred" : `alfred-${basename3}`;
1084
- return this.normaliseAgentId(raw);
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
- if (filePath.includes(".openclaw/")) {
1093
- const agentsIndex = pathParts.lastIndexOf("agents");
1094
- if (agentsIndex !== -1 && agentsIndex + 1 < pathParts.length) {
1095
- return `openclaw-${pathParts[agentsIndex + 1]}`;
1096
- }
1097
- if (filename.startsWith("openclaw-")) {
1098
- return "openclaw-gateway";
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
- if (filePath.includes(".alfred/") || filename.includes("alfred")) {
1103
- return "alfred";
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
- for (const part of pathParts.reverse()) {
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 agentId;
1569
+ let agentName;
1441
1570
  if (parentDir === "sessions" && greatGrandParentDir === "agents") {
1442
- agentId = grandParentDir;
1571
+ agentName = grandParentDir;
1443
1572
  } else if (grandParentDir === "agents") {
1444
- agentId = parentDir;
1445
- } else if (parentDir === "runs" && grandParentDir === "cron") {
1446
- agentId = "openclaw-cron";
1573
+ agentName = parentDir;
1447
1574
  } else {
1448
- agentId = parentDir;
1449
- }
1450
- if (filePath.includes(".alfred/") || filePath.includes("alfred")) {
1451
- if (!agentId.startsWith("alfred-")) {
1452
- agentId = `alfred-${agentId}`;
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) return 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
- return trace;
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) return 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
- return trace;
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 void 0;
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.4.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
- \u{1F4CA} AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
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
- Examples:
2128
- agentflow-dashboard --traces ./traces --host 0.0.0.0 --cors
2129
- agentflow-dashboard -p 8080 -t /var/log/agentflow
2130
- agentflow-dashboard --traces ./traces --data-dir ./workers --data-dir ./cron
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
- Tabs:
2133
- \u{1F3AF} Graph Interactive Cytoscape.js execution graph
2134
- \u23F1\uFE0F Timeline Waterfall view of node durations
2135
- \u{1F4CA} Metrics Success rates, durations, node breakdown
2136
- \u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
2137
- \u26A0\uFE0F Errors Failed and hung nodes with metadata
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 home = process.env.HOME ?? "/home/trader";
2160
- const configPath = path3.join(home, ".agentflow/dashboard-config.json");
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(configPath)) {
2164
- const saved = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
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 autoDiscoverPaths = [
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 clientDir = path3.join(__dirname, "../dist/client");
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
- const publicDir = path3.join(__dirname, "../public");
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 traces = this.watcher.getAllTraces().map(serializeTrace);
2243
- res.json(traces);
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
- const configs = discoverAllProcessConfigs(discoveryDirs);
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
- try {
2637
- const { execSync } = __require("child_process");
2638
- const raw = execSync(
2639
- "systemctl --user show --property=ExecStart --no-pager alfred.service openclaw-gateway.service 2>/dev/null",
2640
- { encoding: "utf8", timeout: 5e3 }
2641
- );
2642
- for (const line of raw.split("\n")) {
2643
- const match = line.match(/path=([^\s;]+)/);
2644
- if (match == null ? void 0 : match[1]) {
2645
- const dir = path3.dirname(match[1]);
2646
- if (fs3.existsSync(dir)) discovered.push(dir);
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
- path3.join(home, ".alfred/traces"),
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 clientIndex = path3.join(__dirname, "../dist/client/index.html");
2772
- if (fs3.existsSync(clientIndex)) {
2773
- res.sendFile(clientIndex);
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((resolve4) => {
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
- resolve4();
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((resolve4) => {
3328
+ return new Promise((resolve5) => {
3031
3329
  this.watcher.stop();
3032
3330
  this.server.close(() => {
3033
3331
  console.log("Dashboard server stopped");
3034
- resolve4();
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
  }