agentflow-dashboard 0.8.3 → 0.8.4

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.
@@ -1,16 +1,23 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
1
  // src/server.ts
9
- import { execSync } from "child_process";
2
+ import { execFileSync, execSync } from "child_process";
10
3
  import * as fs3 from "fs";
11
4
  import { createServer } from "http";
12
5
  import * as path3 from "path";
13
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
+ import {
8
+ auditProcesses,
9
+ createExecutionEvent,
10
+ createKnowledgeStore,
11
+ discoverAllProcessConfigs,
12
+ discoverProcess,
13
+ findVariants,
14
+ getBottlenecks,
15
+ loadGraph as loadGraph2
16
+ } from "agentflow-core";
17
+ import chokidar2 from "chokidar";
18
+ import express from "express";
19
+ import rateLimit from "express-rate-limit";
20
+ import { WebSocketServer } from "ws";
14
21
 
15
22
  // src/config.ts
16
23
  import { existsSync, readFileSync } from "fs";
@@ -83,21 +90,6 @@ function getProcessPreference(config) {
83
90
  return config.processPreference ?? null;
84
91
  }
85
92
 
86
- // src/server.ts
87
- import {
88
- auditProcesses,
89
- createExecutionEvent,
90
- createKnowledgeStore,
91
- discoverAllProcessConfigs,
92
- discoverProcess,
93
- findVariants,
94
- getBottlenecks,
95
- loadGraph as loadGraph2
96
- } from "agentflow-core";
97
- import chokidar2 from "chokidar";
98
- import express from "express";
99
- import { WebSocketServer } from "ws";
100
-
101
93
  // src/adapters/agentflow.ts
102
94
  var SKIP_FILES = /* @__PURE__ */ new Set([
103
95
  "workers.json",
@@ -109,7 +101,14 @@ var SKIP_FILES = /* @__PURE__ */ new Set([
109
101
  "models.json",
110
102
  "config.json"
111
103
  ]);
112
- var SKIP_SUFFIXES = ["-state.json", "-config.json", "-watch-state.json", ".tmp", ".bak", ".backup"];
104
+ var SKIP_SUFFIXES = [
105
+ "-state.json",
106
+ "-config.json",
107
+ "-watch-state.json",
108
+ ".tmp",
109
+ ".bak",
110
+ ".backup"
111
+ ];
113
112
  var AgentFlowAdapter = class {
114
113
  name = "agentflow";
115
114
  detect(_dirPath) {
@@ -234,7 +233,7 @@ var OpenClawAdapter = class {
234
233
  };
235
234
 
236
235
  // src/adapters/otel.ts
237
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
236
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
238
237
  import { join as join3 } from "path";
239
238
  var SPAN_TYPE_MAP = {
240
239
  "gen_ai.chat": "llm",
@@ -386,8 +385,14 @@ registerAdapter(new AgentFlowAdapter());
386
385
  var PURPOSE_KEYWORDS = [
387
386
  { keywords: ["email", "mail", "inbox", "smtp"], group: "Email Processors" },
388
387
  { keywords: ["monitor", "watch", "alert", "surveillance"], group: "Monitors" },
389
- { keywords: ["digest", "newsletter", "summary", "report", "briefing"], group: "Digests & Reports" },
390
- { keywords: ["curator", "janitor", "distiller", "surveyor", "worker", "indexer"], group: "Workers" },
388
+ {
389
+ keywords: ["digest", "newsletter", "summary", "report", "briefing"],
390
+ group: "Digests & Reports"
391
+ },
392
+ {
393
+ keywords: ["curator", "janitor", "distiller", "surveyor", "worker", "indexer"],
394
+ group: "Workers"
395
+ },
391
396
  { keywords: ["cron", "schedule", "timer", "periodic"], group: "Scheduled Jobs" },
392
397
  { keywords: ["search", "scrape", "crawl", "fetch"], group: "Data Collection" },
393
398
  { keywords: ["embed", "vector", "index"], group: "Embeddings" }
@@ -419,6 +424,7 @@ function capitalize(s) {
419
424
  return s.charAt(0).toUpperCase() + s.slice(1);
420
425
  }
421
426
  function deduplicateAgents(agents) {
427
+ var _a, _b, _c, _d;
422
428
  const tagged = agents.map((a) => ({
423
429
  ...a,
424
430
  ...extractSource(a.agentId)
@@ -435,14 +441,14 @@ function deduplicateAgents(agents) {
435
441
  const mergedIds = /* @__PURE__ */ new Set();
436
442
  const mergedAgents = [];
437
443
  for (const [_key, group] of suffixGroups) {
438
- const suffix = extractSuffix(group[0].localId);
444
+ const suffix = extractSuffix((_a = group[0]) == null ? void 0 : _a.localId);
439
445
  if (group.length < 2) continue;
440
446
  const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
441
447
  if (prefixes.size < 2) continue;
442
448
  const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
443
449
  if (longPrefixes.length >= 2) continue;
444
450
  const merged = {
445
- agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
451
+ agentId: ((_b = group[0]) == null ? void 0 : _b.source) === "agentflow" ? suffix : `${(_c = group[0]) == null ? void 0 : _c.source}:${suffix}`,
446
452
  displayName: suffix,
447
453
  totalExecutions: group.reduce((s, a) => s + a.totalExecutions, 0),
448
454
  successfulExecutions: group.reduce((s, a) => s + a.successfulExecutions, 0),
@@ -453,7 +459,7 @@ function deduplicateAgents(agents) {
453
459
  triggers: {},
454
460
  recentActivity: group.flatMap((a) => a.recentActivity).sort((a, b) => b.timestamp - a.timestamp).slice(0, 50),
455
461
  sources: group.map((a) => a.agentId),
456
- adapterSource: group[0].source
462
+ adapterSource: (_d = group[0]) == null ? void 0 : _d.source
457
463
  };
458
464
  merged.successRate = merged.totalExecutions > 0 ? merged.successfulExecutions / merged.totalExecutions * 100 : 0;
459
465
  const totalExecTime = group.reduce((s, a) => s + a.avgExecutionTime * a.totalExecutions, 0);
@@ -869,7 +875,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
869
875
  ...getSkipFiles(this.userConfig)
870
876
  ]);
871
877
  this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
872
- this.allWatchDirs = [...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))];
878
+ this.allWatchDirs = [
879
+ ...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))
880
+ ];
873
881
  this.ensureTracesDir();
874
882
  this.loadExistingFiles();
875
883
  this.archiveOldTraces();
@@ -879,7 +887,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
879
887
  /** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
880
888
  archiveOldTraces() {
881
889
  const cutoff = Date.now() - this.maxAgeMs;
882
- let archived = 0;
890
+ const _archived = 0;
883
891
  for (const dir of this.allWatchDirs) {
884
892
  if (!fs.existsSync(dir)) continue;
885
893
  try {
@@ -896,7 +904,8 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
896
904
  try {
897
905
  const entries = fs.readdirSync(dir, { withFileTypes: true });
898
906
  for (const entry of entries) {
899
- if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
907
+ if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name))
908
+ continue;
900
909
  const fullPath = path.join(dir, entry.name);
901
910
  if (entry.isDirectory()) {
902
911
  archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
@@ -2142,7 +2151,9 @@ import * as os from "os";
2142
2151
  import * as path2 from "path";
2143
2152
  import { fileURLToPath } from "url";
2144
2153
  var __cliDirname = path2.dirname(fileURLToPath(import.meta.url));
2145
- var VERSION = JSON.parse(fs2.readFileSync(path2.resolve(__cliDirname, "../package.json"), "utf-8")).version;
2154
+ var VERSION = JSON.parse(
2155
+ fs2.readFileSync(path2.resolve(__cliDirname, "../package.json"), "utf-8")
2156
+ ).version;
2146
2157
  function getLanAddress() {
2147
2158
  const interfaces = os.networkInterfaces();
2148
2159
  for (const name of Object.keys(interfaces)) {
@@ -2399,6 +2410,17 @@ var DashboardServer = class {
2399
2410
  userConfig;
2400
2411
  configPath;
2401
2412
  setupExpress() {
2413
+ this.app.use(
2414
+ "/api/",
2415
+ rateLimit({
2416
+ windowMs: 60 * 1e3,
2417
+ // 1 minute
2418
+ max: 300,
2419
+ // 300 requests per minute per IP
2420
+ standardHeaders: true,
2421
+ legacyHeaders: false
2422
+ })
2423
+ );
2402
2424
  if (this.config.enableCors) {
2403
2425
  this.app.use((_req, res, next) => {
2404
2426
  res.header("Access-Control-Allow-Origin", "*");
@@ -2641,6 +2663,7 @@ var DashboardServer = class {
2641
2663
  }
2642
2664
  });
2643
2665
  this.app.get("/api/process-model/:agentId", (req, res) => {
2666
+ var _a, _b;
2644
2667
  try {
2645
2668
  const agentId = req.params.agentId;
2646
2669
  const allTraces = this.watcher.getTracesByAgent(agentId);
@@ -2658,8 +2681,8 @@ var DashboardServer = class {
2658
2681
  const nodeArr = Object.values(nodes);
2659
2682
  const sorted = nodeArr.filter((n) => n.name && typeof n.startTime === "number" && n.startTime > 0).sort((a, b) => (a.startTime ?? 0) - (b.startTime ?? 0));
2660
2683
  for (let i = 0; i < sorted.length - 1; i++) {
2661
- const from = sorted[i].name;
2662
- const to = sorted[i + 1].name;
2684
+ const from = (_a = sorted[i]) == null ? void 0 : _a.name;
2685
+ const to = (_b = sorted[i + 1]) == null ? void 0 : _b.name;
2663
2686
  const key = `${from}|||${to}`;
2664
2687
  transMap.set(key, (transMap.get(key) ?? 0) + 1);
2665
2688
  }
@@ -2758,7 +2781,11 @@ var DashboardServer = class {
2758
2781
  try {
2759
2782
  const reportPath = path3.join(somaVault, "..", "soma-report.json");
2760
2783
  if (!fs3.existsSync(reportPath)) {
2761
- return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
2784
+ return res.json({
2785
+ available: false,
2786
+ teaser: false,
2787
+ message: "No report file yet. Run soma watch."
2788
+ });
2762
2789
  }
2763
2790
  const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2764
2791
  res.json(report);
@@ -2782,7 +2809,9 @@ var DashboardServer = class {
2782
2809
  available: true,
2783
2810
  layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
2784
2811
  governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
2785
- insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
2812
+ insights: (report.insights ?? []).filter(
2813
+ (i) => i.layer === "emerging" && i.proposal_status === "pending"
2814
+ ),
2786
2815
  canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
2787
2816
  generatedAt: report.generatedAt
2788
2817
  });
@@ -2791,21 +2820,23 @@ var DashboardServer = class {
2791
2820
  res.status(500).json({ available: false, message: "Failed to read governance data" });
2792
2821
  }
2793
2822
  });
2794
- const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
2795
- const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
2823
+ const isValidId = (s) => /^[a-zA-Z0-9_\-.:]+$/.test(s);
2796
2824
  this.app.post("/api/soma/governance/promote", (req, res) => {
2797
2825
  var _a;
2798
2826
  const somaVault = this.config.somaVault;
2799
2827
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2800
2828
  const { entryId } = req.body ?? {};
2801
- if (!entryId) return res.status(400).json({ error: "entryId required" });
2829
+ if (!entryId || !isValidId(String(entryId)))
2830
+ return res.status(400).json({ error: "Invalid entryId" });
2802
2831
  try {
2803
- const { execSync: execSync2 } = __require("child_process");
2804
- const safeId = sanitizeArg(String(entryId));
2805
- const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
2806
- encoding: "utf-8",
2807
- timeout: 1e4
2808
- });
2832
+ const result = execFileSync(
2833
+ "npx",
2834
+ ["soma", "governance", "promote", String(entryId), "--vault", somaVault],
2835
+ {
2836
+ encoding: "utf-8",
2837
+ timeout: 1e4
2838
+ }
2839
+ );
2809
2840
  res.json({ success: true, message: result.trim() });
2810
2841
  } catch (error) {
2811
2842
  res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2816,15 +2847,27 @@ var DashboardServer = class {
2816
2847
  const somaVault = this.config.somaVault;
2817
2848
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2818
2849
  const { entryId, reason } = req.body ?? {};
2819
- if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
2850
+ if (!entryId || !isValidId(String(entryId)))
2851
+ return res.status(400).json({ error: "Invalid entryId" });
2852
+ if (!reason || typeof reason !== "string")
2853
+ return res.status(400).json({ error: "reason required" });
2820
2854
  try {
2821
- const { execSync: execSync2 } = __require("child_process");
2822
- const safeId = sanitizeArg(String(entryId));
2823
- const safeReason = sanitizeReason(String(reason));
2824
- const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
2825
- encoding: "utf-8",
2826
- timeout: 1e4
2827
- });
2855
+ const result = execFileSync(
2856
+ "npx",
2857
+ [
2858
+ "soma",
2859
+ "governance",
2860
+ "reject",
2861
+ String(entryId),
2862
+ String(reason).slice(0, 500),
2863
+ "--vault",
2864
+ somaVault
2865
+ ],
2866
+ {
2867
+ encoding: "utf-8",
2868
+ timeout: 1e4
2869
+ }
2870
+ );
2828
2871
  res.json({ success: true, message: result.trim() });
2829
2872
  } catch (error) {
2830
2873
  res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2834,13 +2877,16 @@ var DashboardServer = class {
2834
2877
  var _a;
2835
2878
  const somaVault = this.config.somaVault;
2836
2879
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2880
+ if (!isValidId(String(req.params.id))) return res.status(400).json({ error: "Invalid id" });
2837
2881
  try {
2838
- const { execSync: execSync2 } = __require("child_process");
2839
- const safeId = sanitizeArg(String(req.params.id));
2840
- const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
2841
- encoding: "utf-8",
2842
- timeout: 1e4
2843
- });
2882
+ const result = execFileSync(
2883
+ "npx",
2884
+ ["soma", "governance", "show", String(req.params.id), "--vault", somaVault],
2885
+ {
2886
+ encoding: "utf-8",
2887
+ timeout: 1e4
2888
+ }
2889
+ );
2844
2890
  res.json({ available: true, output: result.trim() });
2845
2891
  } catch (error) {
2846
2892
  res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2863,16 +2909,24 @@ var DashboardServer = class {
2863
2909
  const somaVault = this.config.somaVault;
2864
2910
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2865
2911
  const { name, enforcement, scope, conditions } = req.body ?? {};
2866
- if (!name) return res.status(400).json({ error: "name required" });
2912
+ if (!name || !isValidId(String(name)))
2913
+ return res.status(400).json({ error: "Invalid policy name" });
2914
+ const enf = String(enforcement || "warn");
2915
+ if (!isValidId(enf)) return res.status(400).json({ error: "Invalid enforcement value" });
2867
2916
  try {
2868
- const safeName = sanitizeArg(String(name));
2869
- const safeEnf = sanitizeArg(String(enforcement || "warn"));
2870
- const safeScope = sanitizeReason(String(scope || "all"));
2871
- const safeCond = sanitizeReason(String(conditions || ""));
2872
- const result = execSync(
2873
- `npx soma policy create "${safeName}" --enforcement ${safeEnf} --scope "${safeScope}" --conditions "${safeCond}" --vault "${somaVault}"`,
2874
- { encoding: "utf-8", timeout: 1e4 }
2875
- );
2917
+ const args = [
2918
+ "soma",
2919
+ "policy",
2920
+ "create",
2921
+ String(name),
2922
+ "--enforcement",
2923
+ enf,
2924
+ "--vault",
2925
+ somaVault
2926
+ ];
2927
+ if (scope) args.push("--scope", String(scope).slice(0, 500));
2928
+ if (conditions) args.push("--conditions", String(conditions).slice(0, 500));
2929
+ const result = execFileSync("npx", args, { encoding: "utf-8", timeout: 1e4 });
2876
2930
  res.json({ success: true, message: result.trim() });
2877
2931
  } catch (error) {
2878
2932
  res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2882,11 +2936,16 @@ var DashboardServer = class {
2882
2936
  var _a;
2883
2937
  const somaVault = this.config.somaVault;
2884
2938
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2939
+ if (!isValidId(String(req.params.name)))
2940
+ return res.status(400).json({ error: "Invalid policy name" });
2885
2941
  try {
2886
- const safeName = sanitizeArg(String(req.params.name));
2887
- const result = execSync(
2888
- `npx soma policy delete "${safeName}" --vault "${somaVault}"`,
2889
- { encoding: "utf-8", timeout: 1e4 }
2942
+ const result = execFileSync(
2943
+ "npx",
2944
+ ["soma", "policy", "delete", String(req.params.name), "--vault", somaVault],
2945
+ {
2946
+ encoding: "utf-8",
2947
+ timeout: 1e4
2948
+ }
2890
2949
  );
2891
2950
  res.json({ success: true, message: result.trim() });
2892
2951
  } catch (error) {
@@ -2904,16 +2963,28 @@ var DashboardServer = class {
2904
2963
  ...(report.agents ?? []).map((a) => ({ ...a, type: "agent", id: a.name })),
2905
2964
  ...(report.insights ?? []).map((i, idx) => {
2906
2965
  var _a;
2907
- return { ...i, type: i.type || "insight", id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}` };
2966
+ return {
2967
+ ...i,
2968
+ type: i.type || "insight",
2969
+ id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}`
2970
+ };
2908
2971
  }),
2909
2972
  ...(report.policies ?? []).map((p) => ({ ...p, type: "policy", id: p.name }))
2910
2973
  ];
2911
- const { type, layer, q, limit: limitStr, offset: offsetStr } = req.query;
2974
+ const {
2975
+ type,
2976
+ layer,
2977
+ q,
2978
+ limit: limitStr,
2979
+ offset: offsetStr
2980
+ } = req.query;
2912
2981
  if (type) entities = entities.filter((e) => e.type === type);
2913
2982
  if (layer) entities = entities.filter((e) => e.layer === layer);
2914
2983
  if (q) {
2915
2984
  const lq = q.toLowerCase();
2916
- entities = entities.filter((e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq));
2985
+ entities = entities.filter(
2986
+ (e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq)
2987
+ );
2917
2988
  }
2918
2989
  const total = entities.length;
2919
2990
  const offset = parseInt(offsetStr || "0", 10);
@@ -3004,9 +3075,7 @@ var DashboardServer = class {
3004
3075
  const orphans = uniqueProcesses.filter(
3005
3076
  (p) => !allKnownPids.has(p.pid) && p.pid !== process.pid && p.pid !== process.ppid
3006
3077
  );
3007
- const problems = services.flatMap(
3008
- (s) => s.audit.problems.map((p) => `[${s.name}] ${p}`)
3009
- );
3078
+ const problems = services.flatMap((s) => s.audit.problems.map((p) => `[${s.name}] ${p}`));
3010
3079
  const result = {
3011
3080
  // Backward-compatible fields from primary service
3012
3081
  pidFile: (primary == null ? void 0 : primary.audit.pidFile) ?? null,
@@ -3061,19 +3130,24 @@ var DashboardServer = class {
3061
3130
  }
3062
3131
  } catch {
3063
3132
  }
3064
- const watched = [...new Set([
3065
- this.config.tracesDir,
3066
- ...this.config.dataDirs || [],
3067
- ...extraDirs
3068
- ].map((w) => path3.resolve(w)))];
3133
+ const watched = [
3134
+ ...new Set(
3135
+ [this.config.tracesDir, ...this.config.dataDirs || [], ...extraDirs].map(
3136
+ (w) => path3.resolve(w)
3137
+ )
3138
+ )
3139
+ ];
3069
3140
  const discovered = [];
3070
3141
  const svcNames = getSystemdServices(this.userConfig);
3071
3142
  if (svcNames.length > 0) {
3072
3143
  try {
3073
- const { execSync: execSync2 } = __require("child_process");
3074
- const raw = execSync2(
3075
- `systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
3076
- { encoding: "utf8", timeout: 5e3 }
3144
+ const raw = execFileSync(
3145
+ "systemctl",
3146
+ ["--user", "show", "--property=ExecStart", "--no-pager", ...svcNames],
3147
+ {
3148
+ encoding: "utf8",
3149
+ timeout: 5e3
3150
+ }
3077
3151
  );
3078
3152
  for (const line of raw.split("\n")) {
3079
3153
  const match = line.match(/path=([^\s;]+)/);
@@ -3105,10 +3179,19 @@ var DashboardServer = class {
3105
3179
  this.app.post("/api/directories", express.json(), (req, res) => {
3106
3180
  try {
3107
3181
  const { add, remove } = req.body;
3108
- if (add && !fs3.existsSync(add)) {
3109
- return res.status(400).json({ error: `Directory does not exist: ${add}` });
3182
+ if (add) {
3183
+ const resolved = path3.resolve(add);
3184
+ if (resolved !== add || add.includes("..")) {
3185
+ return res.status(400).json({ error: "Invalid directory path" });
3186
+ }
3187
+ if (!fs3.existsSync(resolved)) {
3188
+ return res.status(400).json({ error: `Directory does not exist: ${add}` });
3189
+ }
3110
3190
  }
3111
- const configPath = path3.join(process.env.HOME ?? "/home/trader", ".agentflow/dashboard-config.json");
3191
+ const configPath = path3.join(
3192
+ process.env.HOME ?? "/home/trader",
3193
+ ".agentflow/dashboard-config.json"
3194
+ );
3112
3195
  let config = {};
3113
3196
  try {
3114
3197
  if (fs3.existsSync(configPath)) {
@@ -3318,13 +3401,31 @@ var DashboardServer = class {
3318
3401
  isVirtual: false
3319
3402
  });
3320
3403
  }
3321
- const rootSteps = new Set(model.steps);
3322
- const childSteps = new Set(model.transitions.map((t) => t.to));
3323
- const leafSteps = new Set(model.steps);
3324
- for (const t of model.transitions) {
3325
- }
3326
- nodes.push({ id: "[START]", label: "[START]", count: model.totalGraphs, frequency: 1, avgDuration: 0, failRate: 0, p95Duration: 0, isVirtual: true });
3327
- nodes.push({ id: "[END]", label: "[END]", count: model.totalGraphs, frequency: 1, avgDuration: 0, failRate: 0, p95Duration: 0, isVirtual: true });
3404
+ const _rootSteps = new Set(model.steps);
3405
+ const _childSteps = new Set(model.transitions.map((t) => t.to));
3406
+ const _leafSteps = new Set(model.steps);
3407
+ for (const _t of model.transitions) {
3408
+ }
3409
+ nodes.push({
3410
+ id: "[START]",
3411
+ label: "[START]",
3412
+ count: model.totalGraphs,
3413
+ frequency: 1,
3414
+ avgDuration: 0,
3415
+ failRate: 0,
3416
+ p95Duration: 0,
3417
+ isVirtual: true
3418
+ });
3419
+ nodes.push({
3420
+ id: "[END]",
3421
+ label: "[END]",
3422
+ count: model.totalGraphs,
3423
+ frequency: 1,
3424
+ avgDuration: 0,
3425
+ failRate: 0,
3426
+ p95Duration: 0,
3427
+ isVirtual: true
3428
+ });
3328
3429
  const edges = model.transitions.map((t) => ({
3329
3430
  source: t.from,
3330
3431
  target: t.to,
@@ -3344,7 +3445,10 @@ var DashboardServer = class {
3344
3445
  }
3345
3446
  }
3346
3447
  const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
3347
- const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
3448
+ const maxNodeCount = Math.max(
3449
+ ...nodes.filter((n) => !n.isVirtual).map((n) => n.count),
3450
+ 1
3451
+ );
3348
3452
  return { agentId, totalTraces: model.totalGraphs, nodes, edges, maxEdgeCount, maxNodeCount };
3349
3453
  }
3350
3454
  /**