agentflow-dashboard 0.8.2 → 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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>AgentFlow Dashboard</title>
7
- <script type="module" crossorigin src="/assets/index-DCSWGDzI.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-DHcSpTgM.css">
7
+ <script type="module" crossorigin src="/assets/index-DA9m90ZC.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-BQBa4cES.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/dist/index.cjs CHANGED
@@ -42,6 +42,11 @@ var fs3 = __toESM(require("fs"), 1);
42
42
  var import_node_http = require("http");
43
43
  var path3 = __toESM(require("path"), 1);
44
44
  var import_node_url2 = require("url");
45
+ var import_agentflow_core3 = require("agentflow-core");
46
+ var import_chokidar2 = __toESM(require("chokidar"), 1);
47
+ var import_express = __toESM(require("express"), 1);
48
+ var import_express_rate_limit = __toESM(require("express-rate-limit"), 1);
49
+ var import_ws = require("ws");
45
50
 
46
51
  // src/config.ts
47
52
  var import_node_fs = require("fs");
@@ -114,12 +119,6 @@ function getProcessPreference(config) {
114
119
  return config.processPreference ?? null;
115
120
  }
116
121
 
117
- // src/server.ts
118
- var import_agentflow_core3 = require("agentflow-core");
119
- var import_chokidar2 = __toESM(require("chokidar"), 1);
120
- var import_express = __toESM(require("express"), 1);
121
- var import_ws = require("ws");
122
-
123
122
  // src/adapters/agentflow.ts
124
123
  var SKIP_FILES = /* @__PURE__ */ new Set([
125
124
  "workers.json",
@@ -131,7 +130,14 @@ var SKIP_FILES = /* @__PURE__ */ new Set([
131
130
  "models.json",
132
131
  "config.json"
133
132
  ]);
134
- var SKIP_SUFFIXES = ["-state.json", "-config.json", "-watch-state.json", ".tmp", ".bak", ".backup"];
133
+ var SKIP_SUFFIXES = [
134
+ "-state.json",
135
+ "-config.json",
136
+ "-watch-state.json",
137
+ ".tmp",
138
+ ".bak",
139
+ ".backup"
140
+ ];
135
141
  var AgentFlowAdapter = class {
136
142
  name = "agentflow";
137
143
  detect(_dirPath) {
@@ -408,8 +414,14 @@ registerAdapter(new AgentFlowAdapter());
408
414
  var PURPOSE_KEYWORDS = [
409
415
  { keywords: ["email", "mail", "inbox", "smtp"], group: "Email Processors" },
410
416
  { keywords: ["monitor", "watch", "alert", "surveillance"], group: "Monitors" },
411
- { keywords: ["digest", "newsletter", "summary", "report", "briefing"], group: "Digests & Reports" },
412
- { keywords: ["curator", "janitor", "distiller", "surveyor", "worker", "indexer"], group: "Workers" },
417
+ {
418
+ keywords: ["digest", "newsletter", "summary", "report", "briefing"],
419
+ group: "Digests & Reports"
420
+ },
421
+ {
422
+ keywords: ["curator", "janitor", "distiller", "surveyor", "worker", "indexer"],
423
+ group: "Workers"
424
+ },
413
425
  { keywords: ["cron", "schedule", "timer", "periodic"], group: "Scheduled Jobs" },
414
426
  { keywords: ["search", "scrape", "crawl", "fetch"], group: "Data Collection" },
415
427
  { keywords: ["embed", "vector", "index"], group: "Embeddings" }
@@ -441,6 +453,7 @@ function capitalize(s) {
441
453
  return s.charAt(0).toUpperCase() + s.slice(1);
442
454
  }
443
455
  function deduplicateAgents(agents) {
456
+ var _a, _b, _c, _d;
444
457
  const tagged = agents.map((a) => ({
445
458
  ...a,
446
459
  ...extractSource(a.agentId)
@@ -457,14 +470,14 @@ function deduplicateAgents(agents) {
457
470
  const mergedIds = /* @__PURE__ */ new Set();
458
471
  const mergedAgents = [];
459
472
  for (const [_key, group] of suffixGroups) {
460
- const suffix = extractSuffix(group[0].localId);
473
+ const suffix = extractSuffix((_a = group[0]) == null ? void 0 : _a.localId);
461
474
  if (group.length < 2) continue;
462
475
  const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
463
476
  if (prefixes.size < 2) continue;
464
477
  const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
465
478
  if (longPrefixes.length >= 2) continue;
466
479
  const merged = {
467
- agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
480
+ agentId: ((_b = group[0]) == null ? void 0 : _b.source) === "agentflow" ? suffix : `${(_c = group[0]) == null ? void 0 : _c.source}:${suffix}`,
468
481
  displayName: suffix,
469
482
  totalExecutions: group.reduce((s, a) => s + a.totalExecutions, 0),
470
483
  successfulExecutions: group.reduce((s, a) => s + a.successfulExecutions, 0),
@@ -475,7 +488,7 @@ function deduplicateAgents(agents) {
475
488
  triggers: {},
476
489
  recentActivity: group.flatMap((a) => a.recentActivity).sort((a, b) => b.timestamp - a.timestamp).slice(0, 50),
477
490
  sources: group.map((a) => a.agentId),
478
- adapterSource: group[0].source
491
+ adapterSource: (_d = group[0]) == null ? void 0 : _d.source
479
492
  };
480
493
  merged.successRate = merged.totalExecutions > 0 ? merged.successfulExecutions / merged.totalExecutions * 100 : 0;
481
494
  const totalExecTime = group.reduce((s, a) => s + a.avgExecutionTime * a.totalExecutions, 0);
@@ -891,7 +904,9 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
891
904
  ...getSkipFiles(this.userConfig)
892
905
  ]);
893
906
  this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
894
- this.allWatchDirs = [...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))];
907
+ this.allWatchDirs = [
908
+ ...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))
909
+ ];
895
910
  this.ensureTracesDir();
896
911
  this.loadExistingFiles();
897
912
  this.archiveOldTraces();
@@ -901,7 +916,7 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
901
916
  /** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
902
917
  archiveOldTraces() {
903
918
  const cutoff = Date.now() - this.maxAgeMs;
904
- let archived = 0;
919
+ const _archived = 0;
905
920
  for (const dir of this.allWatchDirs) {
906
921
  if (!fs.existsSync(dir)) continue;
907
922
  try {
@@ -918,7 +933,8 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
918
933
  try {
919
934
  const entries = fs.readdirSync(dir, { withFileTypes: true });
920
935
  for (const entry of entries) {
921
- if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
936
+ if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name))
937
+ continue;
922
938
  const fullPath = path.join(dir, entry.name);
923
939
  if (entry.isDirectory()) {
924
940
  archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
@@ -2165,7 +2181,9 @@ var path2 = __toESM(require("path"), 1);
2165
2181
  var import_node_url = require("url");
2166
2182
  var import_meta = {};
2167
2183
  var __cliDirname = path2.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
2168
- var VERSION = JSON.parse(fs2.readFileSync(path2.resolve(__cliDirname, "../package.json"), "utf-8")).version;
2184
+ var VERSION = JSON.parse(
2185
+ fs2.readFileSync(path2.resolve(__cliDirname, "../package.json"), "utf-8")
2186
+ ).version;
2169
2187
  function getLanAddress() {
2170
2188
  const interfaces = os.networkInterfaces();
2171
2189
  for (const name of Object.keys(interfaces)) {
@@ -2423,6 +2441,17 @@ var DashboardServer = class {
2423
2441
  userConfig;
2424
2442
  configPath;
2425
2443
  setupExpress() {
2444
+ this.app.use(
2445
+ "/api/",
2446
+ (0, import_express_rate_limit.default)({
2447
+ windowMs: 60 * 1e3,
2448
+ // 1 minute
2449
+ max: 300,
2450
+ // 300 requests per minute per IP
2451
+ standardHeaders: true,
2452
+ legacyHeaders: false
2453
+ })
2454
+ );
2426
2455
  if (this.config.enableCors) {
2427
2456
  this.app.use((_req, res, next) => {
2428
2457
  res.header("Access-Control-Allow-Origin", "*");
@@ -2449,10 +2478,6 @@ var DashboardServer = class {
2449
2478
  if (fs3.existsSync(clientDir)) {
2450
2479
  this.app.use(import_express.default.static(clientDir));
2451
2480
  }
2452
- const pkgVersion = JSON.parse(fs3.readFileSync(path3.resolve(__dirname, "../package.json"), "utf-8")).version;
2453
- this.app.get("/api/version", (_req, res) => {
2454
- res.json({ version: pkgVersion });
2455
- });
2456
2481
  this.app.get("/api/traces", (req, res) => {
2457
2482
  try {
2458
2483
  const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
@@ -2669,6 +2694,7 @@ var DashboardServer = class {
2669
2694
  }
2670
2695
  });
2671
2696
  this.app.get("/api/process-model/:agentId", (req, res) => {
2697
+ var _a, _b;
2672
2698
  try {
2673
2699
  const agentId = req.params.agentId;
2674
2700
  const allTraces = this.watcher.getTracesByAgent(agentId);
@@ -2686,8 +2712,8 @@ var DashboardServer = class {
2686
2712
  const nodeArr = Object.values(nodes);
2687
2713
  const sorted = nodeArr.filter((n) => n.name && typeof n.startTime === "number" && n.startTime > 0).sort((a, b) => (a.startTime ?? 0) - (b.startTime ?? 0));
2688
2714
  for (let i = 0; i < sorted.length - 1; i++) {
2689
- const from = sorted[i].name;
2690
- const to = sorted[i + 1].name;
2715
+ const from = (_a = sorted[i]) == null ? void 0 : _a.name;
2716
+ const to = (_b = sorted[i + 1]) == null ? void 0 : _b.name;
2691
2717
  const key = `${from}|||${to}`;
2692
2718
  transMap.set(key, (transMap.get(key) ?? 0) + 1);
2693
2719
  }
@@ -2786,7 +2812,11 @@ var DashboardServer = class {
2786
2812
  try {
2787
2813
  const reportPath = path3.join(somaVault, "..", "soma-report.json");
2788
2814
  if (!fs3.existsSync(reportPath)) {
2789
- return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
2815
+ return res.json({
2816
+ available: false,
2817
+ teaser: false,
2818
+ message: "No report file yet. Run soma watch."
2819
+ });
2790
2820
  }
2791
2821
  const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2792
2822
  res.json(report);
@@ -2810,7 +2840,9 @@ var DashboardServer = class {
2810
2840
  available: true,
2811
2841
  layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
2812
2842
  governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
2813
- insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
2843
+ insights: (report.insights ?? []).filter(
2844
+ (i) => i.layer === "emerging" && i.proposal_status === "pending"
2845
+ ),
2814
2846
  canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
2815
2847
  generatedAt: report.generatedAt
2816
2848
  });
@@ -2819,21 +2851,23 @@ var DashboardServer = class {
2819
2851
  res.status(500).json({ available: false, message: "Failed to read governance data" });
2820
2852
  }
2821
2853
  });
2822
- const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
2823
- const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
2854
+ const isValidId = (s) => /^[a-zA-Z0-9_\-.:]+$/.test(s);
2824
2855
  this.app.post("/api/soma/governance/promote", (req, res) => {
2825
2856
  var _a;
2826
2857
  const somaVault = this.config.somaVault;
2827
2858
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2828
2859
  const { entryId } = req.body ?? {};
2829
- if (!entryId) return res.status(400).json({ error: "entryId required" });
2860
+ if (!entryId || !isValidId(String(entryId)))
2861
+ return res.status(400).json({ error: "Invalid entryId" });
2830
2862
  try {
2831
- const { execSync: execSync2 } = require("child_process");
2832
- const safeId = sanitizeArg(String(entryId));
2833
- const result = execSync2(`npx soma governance promote ${safeId} --vault "${somaVault}"`, {
2834
- encoding: "utf-8",
2835
- timeout: 1e4
2836
- });
2863
+ const result = (0, import_node_child_process.execFileSync)(
2864
+ "npx",
2865
+ ["soma", "governance", "promote", String(entryId), "--vault", somaVault],
2866
+ {
2867
+ encoding: "utf-8",
2868
+ timeout: 1e4
2869
+ }
2870
+ );
2837
2871
  res.json({ success: true, message: result.trim() });
2838
2872
  } catch (error) {
2839
2873
  res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2844,15 +2878,27 @@ var DashboardServer = class {
2844
2878
  const somaVault = this.config.somaVault;
2845
2879
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2846
2880
  const { entryId, reason } = req.body ?? {};
2847
- if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
2881
+ if (!entryId || !isValidId(String(entryId)))
2882
+ return res.status(400).json({ error: "Invalid entryId" });
2883
+ if (!reason || typeof reason !== "string")
2884
+ return res.status(400).json({ error: "reason required" });
2848
2885
  try {
2849
- const { execSync: execSync2 } = require("child_process");
2850
- const safeId = sanitizeArg(String(entryId));
2851
- const safeReason = sanitizeReason(String(reason));
2852
- const result = execSync2(`npx soma governance reject ${safeId} "${safeReason}" --vault "${somaVault}"`, {
2853
- encoding: "utf-8",
2854
- timeout: 1e4
2855
- });
2886
+ const result = (0, import_node_child_process.execFileSync)(
2887
+ "npx",
2888
+ [
2889
+ "soma",
2890
+ "governance",
2891
+ "reject",
2892
+ String(entryId),
2893
+ String(reason).slice(0, 500),
2894
+ "--vault",
2895
+ somaVault
2896
+ ],
2897
+ {
2898
+ encoding: "utf-8",
2899
+ timeout: 1e4
2900
+ }
2901
+ );
2856
2902
  res.json({ success: true, message: result.trim() });
2857
2903
  } catch (error) {
2858
2904
  res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2862,13 +2908,16 @@ var DashboardServer = class {
2862
2908
  var _a;
2863
2909
  const somaVault = this.config.somaVault;
2864
2910
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2911
+ if (!isValidId(String(req.params.id))) return res.status(400).json({ error: "Invalid id" });
2865
2912
  try {
2866
- const { execSync: execSync2 } = require("child_process");
2867
- const safeId = sanitizeArg(String(req.params.id));
2868
- const result = execSync2(`npx soma governance show ${safeId} --vault "${somaVault}"`, {
2869
- encoding: "utf-8",
2870
- timeout: 1e4
2871
- });
2913
+ const result = (0, import_node_child_process.execFileSync)(
2914
+ "npx",
2915
+ ["soma", "governance", "show", String(req.params.id), "--vault", somaVault],
2916
+ {
2917
+ encoding: "utf-8",
2918
+ timeout: 1e4
2919
+ }
2920
+ );
2872
2921
  res.json({ available: true, output: result.trim() });
2873
2922
  } catch (error) {
2874
2923
  res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2891,16 +2940,24 @@ var DashboardServer = class {
2891
2940
  const somaVault = this.config.somaVault;
2892
2941
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2893
2942
  const { name, enforcement, scope, conditions } = req.body ?? {};
2894
- if (!name) return res.status(400).json({ error: "name required" });
2943
+ if (!name || !isValidId(String(name)))
2944
+ return res.status(400).json({ error: "Invalid policy name" });
2945
+ const enf = String(enforcement || "warn");
2946
+ if (!isValidId(enf)) return res.status(400).json({ error: "Invalid enforcement value" });
2895
2947
  try {
2896
- const safeName = sanitizeArg(String(name));
2897
- const safeEnf = sanitizeArg(String(enforcement || "warn"));
2898
- const safeScope = sanitizeReason(String(scope || "all"));
2899
- const safeCond = sanitizeReason(String(conditions || ""));
2900
- const result = (0, import_node_child_process.execSync)(
2901
- `npx soma policy create "${safeName}" --enforcement ${safeEnf} --scope "${safeScope}" --conditions "${safeCond}" --vault "${somaVault}"`,
2902
- { encoding: "utf-8", timeout: 1e4 }
2903
- );
2948
+ const args = [
2949
+ "soma",
2950
+ "policy",
2951
+ "create",
2952
+ String(name),
2953
+ "--enforcement",
2954
+ enf,
2955
+ "--vault",
2956
+ somaVault
2957
+ ];
2958
+ if (scope) args.push("--scope", String(scope).slice(0, 500));
2959
+ if (conditions) args.push("--conditions", String(conditions).slice(0, 500));
2960
+ const result = (0, import_node_child_process.execFileSync)("npx", args, { encoding: "utf-8", timeout: 1e4 });
2904
2961
  res.json({ success: true, message: result.trim() });
2905
2962
  } catch (error) {
2906
2963
  res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
@@ -2910,11 +2967,16 @@ var DashboardServer = class {
2910
2967
  var _a;
2911
2968
  const somaVault = this.config.somaVault;
2912
2969
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2970
+ if (!isValidId(String(req.params.name)))
2971
+ return res.status(400).json({ error: "Invalid policy name" });
2913
2972
  try {
2914
- const safeName = sanitizeArg(String(req.params.name));
2915
- const result = (0, import_node_child_process.execSync)(
2916
- `npx soma policy delete "${safeName}" --vault "${somaVault}"`,
2917
- { encoding: "utf-8", timeout: 1e4 }
2973
+ const result = (0, import_node_child_process.execFileSync)(
2974
+ "npx",
2975
+ ["soma", "policy", "delete", String(req.params.name), "--vault", somaVault],
2976
+ {
2977
+ encoding: "utf-8",
2978
+ timeout: 1e4
2979
+ }
2918
2980
  );
2919
2981
  res.json({ success: true, message: result.trim() });
2920
2982
  } catch (error) {
@@ -2932,16 +2994,28 @@ var DashboardServer = class {
2932
2994
  ...(report.agents ?? []).map((a) => ({ ...a, type: "agent", id: a.name })),
2933
2995
  ...(report.insights ?? []).map((i, idx) => {
2934
2996
  var _a;
2935
- return { ...i, type: i.type || "insight", id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}` };
2997
+ return {
2998
+ ...i,
2999
+ type: i.type || "insight",
3000
+ id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}`
3001
+ };
2936
3002
  }),
2937
3003
  ...(report.policies ?? []).map((p) => ({ ...p, type: "policy", id: p.name }))
2938
3004
  ];
2939
- const { type, layer, q, limit: limitStr, offset: offsetStr } = req.query;
3005
+ const {
3006
+ type,
3007
+ layer,
3008
+ q,
3009
+ limit: limitStr,
3010
+ offset: offsetStr
3011
+ } = req.query;
2940
3012
  if (type) entities = entities.filter((e) => e.type === type);
2941
3013
  if (layer) entities = entities.filter((e) => e.layer === layer);
2942
3014
  if (q) {
2943
3015
  const lq = q.toLowerCase();
2944
- entities = entities.filter((e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq));
3016
+ entities = entities.filter(
3017
+ (e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq)
3018
+ );
2945
3019
  }
2946
3020
  const total = entities.length;
2947
3021
  const offset = parseInt(offsetStr || "0", 10);
@@ -3032,9 +3106,7 @@ var DashboardServer = class {
3032
3106
  const orphans = uniqueProcesses.filter(
3033
3107
  (p) => !allKnownPids.has(p.pid) && p.pid !== process.pid && p.pid !== process.ppid
3034
3108
  );
3035
- const problems = services.flatMap(
3036
- (s) => s.audit.problems.map((p) => `[${s.name}] ${p}`)
3037
- );
3109
+ const problems = services.flatMap((s) => s.audit.problems.map((p) => `[${s.name}] ${p}`));
3038
3110
  const result = {
3039
3111
  // Backward-compatible fields from primary service
3040
3112
  pidFile: (primary == null ? void 0 : primary.audit.pidFile) ?? null,
@@ -3089,19 +3161,24 @@ var DashboardServer = class {
3089
3161
  }
3090
3162
  } catch {
3091
3163
  }
3092
- const watched = [...new Set([
3093
- this.config.tracesDir,
3094
- ...this.config.dataDirs || [],
3095
- ...extraDirs
3096
- ].map((w) => path3.resolve(w)))];
3164
+ const watched = [
3165
+ ...new Set(
3166
+ [this.config.tracesDir, ...this.config.dataDirs || [], ...extraDirs].map(
3167
+ (w) => path3.resolve(w)
3168
+ )
3169
+ )
3170
+ ];
3097
3171
  const discovered = [];
3098
3172
  const svcNames = getSystemdServices(this.userConfig);
3099
3173
  if (svcNames.length > 0) {
3100
3174
  try {
3101
- const { execSync: execSync2 } = require("child_process");
3102
- const raw = execSync2(
3103
- `systemctl --user show --property=ExecStart --no-pager ${svcNames.join(" ")} 2>/dev/null`,
3104
- { encoding: "utf8", timeout: 5e3 }
3175
+ const raw = (0, import_node_child_process.execFileSync)(
3176
+ "systemctl",
3177
+ ["--user", "show", "--property=ExecStart", "--no-pager", ...svcNames],
3178
+ {
3179
+ encoding: "utf8",
3180
+ timeout: 5e3
3181
+ }
3105
3182
  );
3106
3183
  for (const line of raw.split("\n")) {
3107
3184
  const match = line.match(/path=([^\s;]+)/);
@@ -3133,10 +3210,19 @@ var DashboardServer = class {
3133
3210
  this.app.post("/api/directories", import_express.default.json(), (req, res) => {
3134
3211
  try {
3135
3212
  const { add, remove } = req.body;
3136
- if (add && !fs3.existsSync(add)) {
3137
- return res.status(400).json({ error: `Directory does not exist: ${add}` });
3213
+ if (add) {
3214
+ const resolved = path3.resolve(add);
3215
+ if (resolved !== add || add.includes("..")) {
3216
+ return res.status(400).json({ error: "Invalid directory path" });
3217
+ }
3218
+ if (!fs3.existsSync(resolved)) {
3219
+ return res.status(400).json({ error: `Directory does not exist: ${add}` });
3220
+ }
3138
3221
  }
3139
- const configPath = path3.join(process.env.HOME ?? "/home/trader", ".agentflow/dashboard-config.json");
3222
+ const configPath = path3.join(
3223
+ process.env.HOME ?? "/home/trader",
3224
+ ".agentflow/dashboard-config.json"
3225
+ );
3140
3226
  let config = {};
3141
3227
  try {
3142
3228
  if (fs3.existsSync(configPath)) {
@@ -3346,13 +3432,31 @@ var DashboardServer = class {
3346
3432
  isVirtual: false
3347
3433
  });
3348
3434
  }
3349
- const rootSteps = new Set(model.steps);
3350
- const childSteps = new Set(model.transitions.map((t) => t.to));
3351
- const leafSteps = new Set(model.steps);
3352
- for (const t of model.transitions) {
3353
- }
3354
- nodes.push({ id: "[START]", label: "[START]", count: model.totalGraphs, frequency: 1, avgDuration: 0, failRate: 0, p95Duration: 0, isVirtual: true });
3355
- nodes.push({ id: "[END]", label: "[END]", count: model.totalGraphs, frequency: 1, avgDuration: 0, failRate: 0, p95Duration: 0, isVirtual: true });
3435
+ const _rootSteps = new Set(model.steps);
3436
+ const _childSteps = new Set(model.transitions.map((t) => t.to));
3437
+ const _leafSteps = new Set(model.steps);
3438
+ for (const _t of model.transitions) {
3439
+ }
3440
+ nodes.push({
3441
+ id: "[START]",
3442
+ label: "[START]",
3443
+ count: model.totalGraphs,
3444
+ frequency: 1,
3445
+ avgDuration: 0,
3446
+ failRate: 0,
3447
+ p95Duration: 0,
3448
+ isVirtual: true
3449
+ });
3450
+ nodes.push({
3451
+ id: "[END]",
3452
+ label: "[END]",
3453
+ count: model.totalGraphs,
3454
+ frequency: 1,
3455
+ avgDuration: 0,
3456
+ failRate: 0,
3457
+ p95Duration: 0,
3458
+ isVirtual: true
3459
+ });
3356
3460
  const edges = model.transitions.map((t) => ({
3357
3461
  source: t.from,
3358
3462
  target: t.to,
@@ -3372,7 +3476,10 @@ var DashboardServer = class {
3372
3476
  }
3373
3477
  }
3374
3478
  const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
3375
- const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
3479
+ const maxNodeCount = Math.max(
3480
+ ...nodes.filter((n) => !n.isVirtual).map((n) => n.count),
3481
+ 1
3482
+ );
3376
3483
  return { agentId, totalTraces: model.totalGraphs, nodes, edges, maxEdgeCount, maxNodeCount };
3377
3484
  }
3378
3485
  /**
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  AgentStats,
4
4
  DashboardServer,
5
5
  TraceWatcher
6
- } from "./chunk-EG254FLY.js";
6
+ } from "./chunk-YLQ5MVCW.js";
7
7
  export {
8
8
  AgentStats,
9
9
  DashboardServer,