agentflow-dashboard 0.8.3 → 0.9.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.
@@ -1,16 +1,28 @@
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
- });
1
+ import {
2
+ __require
3
+ } from "./chunk-3RG5ZIWI.js";
7
4
 
8
5
  // src/server.ts
9
- import { execSync } from "child_process";
10
- import * as fs3 from "fs";
6
+ import { execFileSync, execSync } from "child_process";
7
+ import * as fs4 from "fs";
11
8
  import { createServer } from "http";
12
- import * as path3 from "path";
9
+ import * as path4 from "path";
13
10
  import { fileURLToPath as fileURLToPath2 } from "url";
11
+ import {
12
+ auditProcesses,
13
+ createExecutionEvent,
14
+ createKnowledgeStore,
15
+ discoverAllProcessConfigs,
16
+ discoverProcess,
17
+ findVariants,
18
+ getBottlenecks,
19
+ loadGraph as loadGraph2,
20
+ toReceipt
21
+ } from "agentflow-core";
22
+ import chokidar2 from "chokidar";
23
+ import express from "express";
24
+ import rateLimit from "express-rate-limit";
25
+ import { WebSocketServer } from "ws";
14
26
 
15
27
  // src/config.ts
16
28
  import { existsSync, readFileSync } from "fs";
@@ -82,21 +94,70 @@ function getAgentDetection(config) {
82
94
  function getProcessPreference(config) {
83
95
  return config.processPreference ?? null;
84
96
  }
85
-
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";
97
+ function getExternalCommands(config) {
98
+ return config.externalCommands ?? {};
99
+ }
100
+ function getValidatedExternalCommands(config) {
101
+ var _a, _b, _c, _d;
102
+ const externalCommands = getExternalCommands(config);
103
+ const errors = [];
104
+ const validatedCommands = {};
105
+ if (!externalCommands.commands) {
106
+ return { commands: {}, errors: [] };
107
+ }
108
+ for (const [commandId, command] of Object.entries(externalCommands.commands)) {
109
+ try {
110
+ if (!commandId.match(/^[a-z][a-z0-9-_]*$/i)) {
111
+ errors.push(`Invalid command ID "${commandId}": must start with letter and contain only letters, numbers, hyphens, and underscores`);
112
+ continue;
113
+ }
114
+ if (!((_a = command.name) == null ? void 0 : _a.trim())) {
115
+ errors.push(`Command "${commandId}": name is required`);
116
+ continue;
117
+ }
118
+ if (!((_b = command.command) == null ? void 0 : _b.trim())) {
119
+ errors.push(`Command "${commandId}": command is required`);
120
+ continue;
121
+ }
122
+ const validatedCommand = {
123
+ name: command.name.trim(),
124
+ command: command.command.trim(),
125
+ args: command.args ?? [],
126
+ cwd: expandTilde(command.cwd ?? externalCommands.globalCwd ?? process.cwd()),
127
+ env: { ...externalCommands.globalEnv, ...command.env },
128
+ timeout: command.timeout ?? externalCommands.globalTimeout ?? 6e4,
129
+ description: ((_c = command.description) == null ? void 0 : _c.trim()) ?? "",
130
+ category: ((_d = command.category) == null ? void 0 : _d.trim()) ?? "general",
131
+ allowConcurrent: command.allowConcurrent ?? false
132
+ };
133
+ if (validatedCommand.timeout <= 0 || validatedCommand.timeout > 6e5) {
134
+ errors.push(`Command "${commandId}": timeout must be between 1ms and 600000ms (10 minutes)`);
135
+ continue;
136
+ }
137
+ if (validatedCommand.cwd.startsWith("/")) {
138
+ try {
139
+ const fs5 = __require("fs");
140
+ const stats = fs5.statSync(validatedCommand.cwd);
141
+ if (!stats.isDirectory()) {
142
+ errors.push(`Command "${commandId}": cwd "${validatedCommand.cwd}" is not a directory`);
143
+ continue;
144
+ }
145
+ } catch (err) {
146
+ errors.push(`Command "${commandId}": cwd "${validatedCommand.cwd}" does not exist or is not accessible`);
147
+ continue;
148
+ }
149
+ }
150
+ validatedCommands[commandId] = validatedCommand;
151
+ } catch (error) {
152
+ errors.push(`Command "${commandId}": validation error - ${error.message}`);
153
+ }
154
+ }
155
+ return { commands: validatedCommands, errors };
156
+ }
157
+ function getExternalCommand(config, commandId) {
158
+ const { commands } = getValidatedExternalCommands(config);
159
+ return commands[commandId] ?? null;
160
+ }
100
161
 
101
162
  // src/adapters/agentflow.ts
102
163
  var SKIP_FILES = /* @__PURE__ */ new Set([
@@ -109,7 +170,14 @@ var SKIP_FILES = /* @__PURE__ */ new Set([
109
170
  "models.json",
110
171
  "config.json"
111
172
  ]);
112
- var SKIP_SUFFIXES = ["-state.json", "-config.json", "-watch-state.json", ".tmp", ".bak", ".backup"];
173
+ var SKIP_SUFFIXES = [
174
+ "-state.json",
175
+ "-config.json",
176
+ "-watch-state.json",
177
+ ".tmp",
178
+ ".bak",
179
+ ".backup"
180
+ ];
113
181
  var AgentFlowAdapter = class {
114
182
  name = "agentflow";
115
183
  detect(_dirPath) {
@@ -168,6 +236,7 @@ var OpenClawAdapter = class {
168
236
  return filePath.includes("/cron/runs/") || filePath.includes("\\cron\\runs\\");
169
237
  }
170
238
  parse(filePath) {
239
+ var _a, _b, _c, _d, _e, _f, _g, _h;
171
240
  const traces = [];
172
241
  try {
173
242
  const content = readFileSync2(filePath, "utf-8");
@@ -187,34 +256,100 @@ var OpenClawAdapter = class {
187
256
  const jobName = (job == null ? void 0 : job.name) ?? jobId;
188
257
  const startTime = entry.runAtMs ?? entry.ts;
189
258
  const duration = entry.durationMs ?? 0;
259
+ const runStatus = entry.status === "ok" ? "completed" : entry.status === "error" ? "failed" : "unknown";
260
+ const summary = entry.summary ?? "";
261
+ const nodes = {};
262
+ const rootChildren = [];
263
+ let stepIdx = 0;
264
+ let m;
265
+ const tableRe = /\|\s*\*?\*?(\d+)\.\s*\*?\*?([^|]+)\*?\*?\s*\|\s*([^|]+)\s*\|\s*([^|]*)\|/g;
266
+ while ((m = tableRe.exec(summary)) !== null) {
267
+ stepIdx++;
268
+ const nodeId = `step-${stepIdx}`;
269
+ rootChildren.push(nodeId);
270
+ nodes[nodeId] = {
271
+ id: nodeId,
272
+ type: "tool",
273
+ name: ((_a = m[2]) == null ? void 0 : _a.trim().replace(/\*+/g, "")) || `Step ${stepIdx}`,
274
+ status: ((_b = m[3]) == null ? void 0 : _b.includes("\u274C")) || ((_c = m[3]) == null ? void 0 : _c.toLowerCase().includes("fail")) ? "failed" : "completed",
275
+ startTime: startTime + (stepIdx - 1) * Math.floor(duration / Math.max(1, stepIdx + 1)),
276
+ endTime: startTime + stepIdx * Math.floor(duration / Math.max(1, stepIdx + 1)),
277
+ parentId: "root",
278
+ children: [],
279
+ metadata: { detail: ((_d = m[4]) == null ? void 0 : _d.trim()) || "" }
280
+ };
281
+ }
282
+ if (stepIdx === 0) {
283
+ const listRe = /^\s*(\d+)\.\s*\*?\*?([^*\n]+)\*?\*?\s*(?:[-\u2014]\s*(.+))?$/gm;
284
+ while ((m = listRe.exec(summary)) !== null) {
285
+ if ((_e = m[2]) == null ? void 0 : _e.trim().startsWith("#")) continue;
286
+ stepIdx++;
287
+ const nodeId = `step-${stepIdx}`;
288
+ const detail = ((_f = m[3]) == null ? void 0 : _f.trim()) || "";
289
+ rootChildren.push(nodeId);
290
+ nodes[nodeId] = {
291
+ id: nodeId,
292
+ type: "tool",
293
+ name: ((_g = m[2]) == null ? void 0 : _g.trim().replace(/\*+/g, "")) || `Step ${stepIdx}`,
294
+ status: detail.toLowerCase().includes("fail") ? "failed" : "completed",
295
+ startTime: startTime + (stepIdx - 1) * Math.floor(duration / Math.max(1, stepIdx + 1)),
296
+ endTime: startTime + stepIdx * Math.floor(duration / Math.max(1, stepIdx + 1)),
297
+ parentId: "root",
298
+ children: [],
299
+ metadata: { detail }
300
+ };
301
+ }
302
+ }
303
+ if (stepIdx === 0 && summary.length > 50) {
304
+ const sectionRe = /(?:^|\n)\s*(?:#{1,3}|(?:\*\*[^*\n]{3,50}\*\*))\s*(.+)/g;
305
+ while ((m = sectionRe.exec(summary)) !== null) {
306
+ const heading = ((_h = m[1]) == null ? void 0 : _h.trim().replace(/\*+/g, "").replace(/^#+\s*/, "")) || "";
307
+ if (!heading || heading.length < 3 || heading.startsWith("|") || heading.startsWith("---"))
308
+ continue;
309
+ stepIdx++;
310
+ const nodeId = `section-${stepIdx}`;
311
+ rootChildren.push(nodeId);
312
+ nodes[nodeId] = {
313
+ id: nodeId,
314
+ type: "custom",
315
+ name: heading.slice(0, 60),
316
+ status: "completed",
317
+ startTime: startTime + (stepIdx - 1) * Math.floor(duration / Math.max(1, stepIdx + 1)),
318
+ endTime: startTime + stepIdx * Math.floor(duration / Math.max(1, stepIdx + 1)),
319
+ parentId: "root",
320
+ children: [],
321
+ metadata: {}
322
+ };
323
+ if (stepIdx >= 10) break;
324
+ }
325
+ }
326
+ nodes["root"] = {
327
+ id: "root",
328
+ type: "cron-job",
329
+ name: jobName,
330
+ status: runStatus,
331
+ startTime,
332
+ endTime: startTime + duration,
333
+ parentId: null,
334
+ children: rootChildren,
335
+ metadata: {
336
+ jobId,
337
+ summary: entry.summary,
338
+ error: entry.error,
339
+ delivered: entry.delivered,
340
+ deliveryStatus: entry.deliveryStatus
341
+ }
342
+ };
190
343
  const trace = {
191
344
  id: entry.sessionId ?? `${jobId}-${entry.ts}`,
192
345
  agentId: `openclaw:${jobId}`,
193
346
  name: jobName,
194
- status: entry.status === "ok" ? "completed" : entry.status === "error" ? "failed" : "unknown",
347
+ status: runStatus,
195
348
  startTime,
196
349
  endTime: startTime + duration,
197
350
  trigger: "cron",
198
351
  source: "openclaw",
199
- nodes: {
200
- root: {
201
- id: "root",
202
- type: "cron-job",
203
- name: jobName,
204
- status: entry.status === "ok" ? "completed" : entry.status === "error" ? "failed" : "unknown",
205
- startTime,
206
- endTime: startTime + duration,
207
- parentId: null,
208
- children: [],
209
- metadata: {
210
- jobId,
211
- summary: entry.summary,
212
- error: entry.error,
213
- delivered: entry.delivered,
214
- deliveryStatus: entry.deliveryStatus
215
- }
216
- }
217
- },
352
+ nodes,
218
353
  metadata: {
219
354
  model: entry.model,
220
355
  provider: entry.provider,
@@ -234,7 +369,7 @@ var OpenClawAdapter = class {
234
369
  };
235
370
 
236
371
  // src/adapters/otel.ts
237
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
372
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
238
373
  import { join as join3 } from "path";
239
374
  var SPAN_TYPE_MAP = {
240
375
  "gen_ai.chat": "llm",
@@ -386,8 +521,14 @@ registerAdapter(new AgentFlowAdapter());
386
521
  var PURPOSE_KEYWORDS = [
387
522
  { keywords: ["email", "mail", "inbox", "smtp"], group: "Email Processors" },
388
523
  { 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" },
524
+ {
525
+ keywords: ["digest", "newsletter", "summary", "report", "briefing"],
526
+ group: "Digests & Reports"
527
+ },
528
+ {
529
+ keywords: ["curator", "janitor", "distiller", "surveyor", "worker", "indexer"],
530
+ group: "Workers"
531
+ },
391
532
  { keywords: ["cron", "schedule", "timer", "periodic"], group: "Scheduled Jobs" },
392
533
  { keywords: ["search", "scrape", "crawl", "fetch"], group: "Data Collection" },
393
534
  { keywords: ["embed", "vector", "index"], group: "Embeddings" }
@@ -419,6 +560,7 @@ function capitalize(s) {
419
560
  return s.charAt(0).toUpperCase() + s.slice(1);
420
561
  }
421
562
  function deduplicateAgents(agents) {
563
+ var _a, _b, _c, _d;
422
564
  const tagged = agents.map((a) => ({
423
565
  ...a,
424
566
  ...extractSource(a.agentId)
@@ -435,14 +577,15 @@ function deduplicateAgents(agents) {
435
577
  const mergedIds = /* @__PURE__ */ new Set();
436
578
  const mergedAgents = [];
437
579
  for (const [_key, group] of suffixGroups) {
438
- const suffix = extractSuffix(group[0].localId);
439
580
  if (group.length < 2) continue;
581
+ const suffix = extractSuffix(((_a = group[0]) == null ? void 0 : _a.localId) ?? "");
582
+ if (!suffix) continue;
440
583
  const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
441
584
  if (prefixes.size < 2) continue;
442
585
  const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
443
586
  if (longPrefixes.length >= 2) continue;
444
587
  const merged = {
445
- agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
588
+ agentId: ((_b = group[0]) == null ? void 0 : _b.source) === "agentflow" ? suffix : `${(_c = group[0]) == null ? void 0 : _c.source}:${suffix}`,
446
589
  displayName: suffix,
447
590
  totalExecutions: group.reduce((s, a) => s + a.totalExecutions, 0),
448
591
  successfulExecutions: group.reduce((s, a) => s + a.successfulExecutions, 0),
@@ -453,7 +596,7 @@ function deduplicateAgents(agents) {
453
596
  triggers: {},
454
597
  recentActivity: group.flatMap((a) => a.recentActivity).sort((a, b) => b.timestamp - a.timestamp).slice(0, 50),
455
598
  sources: group.map((a) => a.agentId),
456
- adapterSource: group[0].source
599
+ adapterSource: (_d = group[0]) == null ? void 0 : _d.source
457
600
  };
458
601
  merged.successRate = merged.totalExecutions > 0 ? merged.successfulExecutions / merged.totalExecutions * 100 : 0;
459
602
  const totalExecTime = group.reduce((s, a) => s + a.avgExecutionTime * a.totalExecutions, 0);
@@ -513,13 +656,470 @@ function groupAgents(agents) {
513
656
  return { groups };
514
657
  }
515
658
 
659
+ // src/command-executor.ts
660
+ import * as fs from "fs";
661
+ import * as path from "path";
662
+ import { spawn } from "child_process";
663
+ var CommandExecutor = class {
664
+ constructor(config, options = {}) {
665
+ this.config = config;
666
+ var _a;
667
+ this.maxConcurrentExecutions = options.maxConcurrentExecutions ?? ((_a = config.externalCommands) == null ? void 0 : _a.maxConcurrentExecutions) ?? 5;
668
+ }
669
+ executions = /* @__PURE__ */ new Map();
670
+ runningProcesses = /* @__PURE__ */ new Map();
671
+ executionCounter = 0;
672
+ maxConcurrentExecutions;
673
+ /**
674
+ * Execute an external command with security validation
675
+ */
676
+ async executeCommand(request) {
677
+ var _a, _b;
678
+ const executionId = this.generateExecutionId();
679
+ try {
680
+ const validation = this.validateExecutionRequest(request);
681
+ if (validation.length > 0) {
682
+ return this.createFailedResult(executionId, request.commandId, validation[0].message);
683
+ }
684
+ const command = getExternalCommand(this.config, request.commandId);
685
+ if (!command) {
686
+ return this.createFailedResult(executionId, request.commandId, `Command "${request.commandId}" not found in configuration`);
687
+ }
688
+ if (!this.canStartExecution(command)) {
689
+ return this.createFailedResult(
690
+ executionId,
691
+ request.commandId,
692
+ `Cannot start command: ${command.allowConcurrent ? "concurrent execution limit reached" : "command already running"}`
693
+ );
694
+ }
695
+ const result = {
696
+ executionId,
697
+ started: false,
698
+ command,
699
+ startedAt: Date.now(),
700
+ stdout: "",
701
+ stderr: "",
702
+ status: "running"
703
+ };
704
+ this.executions.set(executionId, result);
705
+ const sanitizedArgs = this.sanitizeArguments([
706
+ ...command.args,
707
+ ...request.additionalArgs ?? []
708
+ ]);
709
+ const executionTimeout = request.timeout ?? command.timeout;
710
+ const sanitizedEnv = this.sanitizeEnvironment(command.env);
711
+ const childProcess = spawn(command.command, sanitizedArgs, {
712
+ cwd: command.cwd,
713
+ env: { ...process.env, ...sanitizedEnv },
714
+ stdio: ["ignore", "pipe", "pipe"],
715
+ // stdin ignored, capture stdout/stderr
716
+ detached: false
717
+ // Keep process attached for proper cleanup
718
+ });
719
+ result.started = true;
720
+ result.pid = childProcess.pid;
721
+ this.runningProcesses.set(executionId, childProcess);
722
+ const timeoutHandle = setTimeout(() => {
723
+ this.killExecution(executionId, "timeout");
724
+ }, executionTimeout);
725
+ (_a = childProcess.stdout) == null ? void 0 : _a.on("data", (data) => {
726
+ result.stdout += data.toString();
727
+ });
728
+ (_b = childProcess.stderr) == null ? void 0 : _b.on("data", (data) => {
729
+ result.stderr += data.toString();
730
+ });
731
+ childProcess.on("close", (code, signal) => {
732
+ clearTimeout(timeoutHandle);
733
+ this.runningProcesses.delete(executionId);
734
+ result.completedAt = Date.now();
735
+ result.duration = result.completedAt - result.startedAt;
736
+ result.exitCode = code ?? void 0;
737
+ if (signal) {
738
+ result.status = result.status === "running" ? "killed" : result.status;
739
+ result.error = `Process killed by signal: ${signal}`;
740
+ } else if (code === 0) {
741
+ result.status = "completed";
742
+ } else {
743
+ result.status = "failed";
744
+ result.error = `Process exited with code: ${code}`;
745
+ }
746
+ this.logExecution(result, request.context);
747
+ });
748
+ childProcess.on("error", (error) => {
749
+ clearTimeout(timeoutHandle);
750
+ this.runningProcesses.delete(executionId);
751
+ result.completedAt = Date.now();
752
+ result.duration = result.completedAt - result.startedAt;
753
+ result.status = "failed";
754
+ result.error = `Process error: ${error.message}`;
755
+ this.logExecution(result, request.context);
756
+ });
757
+ return result;
758
+ } catch (error) {
759
+ return this.createFailedResult(
760
+ executionId,
761
+ request.commandId,
762
+ `Execution setup failed: ${error.message}`
763
+ );
764
+ }
765
+ }
766
+ /**
767
+ * Get execution status by ID
768
+ */
769
+ getExecution(executionId) {
770
+ return this.executions.get(executionId) ?? null;
771
+ }
772
+ /**
773
+ * Get all executions (recent first)
774
+ */
775
+ getAllExecutions(limit = 100) {
776
+ return Array.from(this.executions.values()).sort((a, b) => b.startedAt - a.startedAt).slice(0, limit);
777
+ }
778
+ /**
779
+ * Kill a running execution
780
+ */
781
+ killExecution(executionId, reason = "manual") {
782
+ const execution = this.executions.get(executionId);
783
+ const process2 = this.runningProcesses.get(executionId);
784
+ if (!execution || !process2) {
785
+ return false;
786
+ }
787
+ try {
788
+ process2.kill("SIGTERM");
789
+ setTimeout(() => {
790
+ if (this.runningProcesses.has(executionId)) {
791
+ process2.kill("SIGKILL");
792
+ }
793
+ }, 5e3);
794
+ if (reason === "timeout") {
795
+ execution.status = "timeout";
796
+ execution.error = "Command execution timed out";
797
+ } else {
798
+ execution.status = "killed";
799
+ execution.error = `Command killed: ${reason}`;
800
+ }
801
+ return true;
802
+ } catch (error) {
803
+ execution.error = `Failed to kill process: ${error.message}`;
804
+ return false;
805
+ }
806
+ }
807
+ /**
808
+ * Get currently running executions
809
+ */
810
+ getRunningExecutions() {
811
+ return Array.from(this.executions.values()).filter((exec) => exec.status === "running");
812
+ }
813
+ /**
814
+ * Clean up old execution records
815
+ */
816
+ cleanupExecutions(maxAge = 24 * 60 * 60 * 1e3) {
817
+ const cutoff = Date.now() - maxAge;
818
+ let cleaned = 0;
819
+ for (const [id, execution] of this.executions.entries()) {
820
+ if (execution.status !== "running" && execution.startedAt < cutoff) {
821
+ this.executions.delete(id);
822
+ cleaned++;
823
+ }
824
+ }
825
+ return cleaned;
826
+ }
827
+ validateExecutionRequest(request) {
828
+ var _a;
829
+ const errors = [];
830
+ if (!((_a = request.commandId) == null ? void 0 : _a.trim())) {
831
+ errors.push({
832
+ type: "validation_error",
833
+ message: "Command ID is required"
834
+ });
835
+ return errors;
836
+ }
837
+ const { commands, errors: configErrors } = getValidatedExternalCommands(this.config);
838
+ if (configErrors.length > 0) {
839
+ errors.push({
840
+ type: "config_error",
841
+ message: `Configuration errors: ${configErrors.join(", ")}`
842
+ });
843
+ }
844
+ if (!commands[request.commandId]) {
845
+ errors.push({
846
+ type: "validation_error",
847
+ message: `Command "${request.commandId}" not found in configuration`,
848
+ commandId: request.commandId
849
+ });
850
+ }
851
+ if (request.additionalArgs) {
852
+ for (const arg of request.additionalArgs) {
853
+ if (this.containsUnsafeContent(arg)) {
854
+ errors.push({
855
+ type: "security_violation",
856
+ message: `Unsafe content detected in additional arguments`,
857
+ commandId: request.commandId
858
+ });
859
+ break;
860
+ }
861
+ }
862
+ }
863
+ if (request.timeout !== void 0 && (request.timeout <= 0 || request.timeout > 6e5)) {
864
+ errors.push({
865
+ type: "validation_error",
866
+ message: "Timeout must be between 1ms and 600000ms (10 minutes)",
867
+ commandId: request.commandId
868
+ });
869
+ }
870
+ return errors;
871
+ }
872
+ containsUnsafeContent(content) {
873
+ const dangerousPatterns = [
874
+ /[;&|`$(){}[\]]/,
875
+ // Shell metacharacters
876
+ /\.\./,
877
+ // Directory traversal
878
+ /\/dev\/|\/proc\/|\/sys\//,
879
+ // System paths
880
+ /rm\s+-rf|rm\s+-f/i,
881
+ // Dangerous rm commands
882
+ /chmod|chown|sudo/i,
883
+ // Privilege escalation
884
+ /curl|wget|nc|telnet/i
885
+ // Network commands
886
+ ];
887
+ return dangerousPatterns.some((pattern) => pattern.test(content));
888
+ }
889
+ sanitizeArguments(args) {
890
+ return args.map((arg) => {
891
+ let sanitized = arg.replace(/[\x00-\x1f\x7f]/g, "");
892
+ sanitized = sanitized.trim();
893
+ if (sanitized.length > 1e3) {
894
+ sanitized = sanitized.substring(0, 1e3);
895
+ }
896
+ return sanitized;
897
+ }).filter((arg) => arg.length > 0);
898
+ }
899
+ sanitizeEnvironment(env) {
900
+ const sanitized = {};
901
+ if (!env) return sanitized;
902
+ for (const [key, value] of Object.entries(env)) {
903
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
904
+ continue;
905
+ }
906
+ const sanitizedValue = value.replace(/[\x00-\x1f\x7f]/g, "").trim();
907
+ if (sanitizedValue.length > 0 && sanitizedValue.length <= 4096) {
908
+ sanitized[key] = sanitizedValue;
909
+ }
910
+ }
911
+ return sanitized;
912
+ }
913
+ canStartExecution(command) {
914
+ const running = this.getRunningExecutions();
915
+ if (running.length >= this.maxConcurrentExecutions) {
916
+ return false;
917
+ }
918
+ if (!command.allowConcurrent) {
919
+ const commandRunning = running.some(
920
+ (exec) => exec.command.name === command.name
921
+ );
922
+ if (commandRunning) {
923
+ return false;
924
+ }
925
+ }
926
+ return true;
927
+ }
928
+ generateExecutionId() {
929
+ return `exec_${Date.now()}_${++this.executionCounter}`;
930
+ }
931
+ createFailedResult(executionId, commandId, error) {
932
+ const result = {
933
+ executionId,
934
+ started: false,
935
+ command: {
936
+ name: commandId,
937
+ command: "unknown",
938
+ timeout: 0
939
+ },
940
+ startedAt: Date.now(),
941
+ completedAt: Date.now(),
942
+ duration: 0,
943
+ stdout: "",
944
+ stderr: "",
945
+ status: "failed",
946
+ error
947
+ };
948
+ this.executions.set(executionId, result);
949
+ return result;
950
+ }
951
+ logExecution(result, context) {
952
+ var _a;
953
+ const logEntry = {
954
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
955
+ executionId: result.executionId,
956
+ commandId: result.command.name,
957
+ commandLine: `${result.command.command} ${((_a = result.command.args) == null ? void 0 : _a.join(" ")) ?? ""}`.trim(),
958
+ status: result.status,
959
+ duration: result.duration,
960
+ exitCode: result.exitCode,
961
+ pid: result.pid,
962
+ cwd: result.command.cwd,
963
+ timeout: result.command.timeout,
964
+ context,
965
+ hasOutput: result.stdout.length > 0,
966
+ hasError: result.stderr.length > 0,
967
+ outputSize: result.stdout.length,
968
+ errorSize: result.stderr.length,
969
+ error: result.error,
970
+ userAgent: (context == null ? void 0 : context.userId) ? `user:${context.userId}` : "system",
971
+ sessionId: context == null ? void 0 : context.sessionId,
972
+ requestId: context == null ? void 0 : context.requestId
973
+ };
974
+ console.log(`[CommandExecution] ${JSON.stringify(logEntry)}`);
975
+ this.writeAuditLog(logEntry, result);
976
+ }
977
+ /**
978
+ * Write detailed audit log to file for compliance and debugging
979
+ */
980
+ writeAuditLog(logEntry, result) {
981
+ var _a, _b, _c, _d;
982
+ try {
983
+ const auditDir = path.join(process.cwd(), ".agentflow", "audit");
984
+ const auditFile = path.join(auditDir, `command-executions-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.jsonl`);
985
+ if (!fs.existsSync(auditDir)) {
986
+ fs.mkdirSync(auditDir, { recursive: true });
987
+ }
988
+ const auditEntry = {
989
+ ...logEntry,
990
+ // Additional audit fields
991
+ startedAt: new Date(result.startedAt).toISOString(),
992
+ completedAt: result.completedAt ? new Date(result.completedAt).toISOString() : null,
993
+ commandArgs: result.command.args,
994
+ allowConcurrent: result.command.allowConcurrent,
995
+ category: result.command.category,
996
+ description: result.command.description,
997
+ // Include first/last lines of output for audit trail
998
+ outputPreview: result.stdout ? {
999
+ firstLine: ((_a = result.stdout.split("\n")[0]) == null ? void 0 : _a.slice(0, 200)) || "",
1000
+ lastLine: ((_b = result.stdout.split("\n").slice(-1)[0]) == null ? void 0 : _b.slice(0, 200)) || "",
1001
+ totalLines: result.stdout.split("\n").length
1002
+ } : null,
1003
+ errorPreview: result.stderr ? {
1004
+ firstLine: ((_c = result.stderr.split("\n")[0]) == null ? void 0 : _c.slice(0, 200)) || "",
1005
+ lastLine: ((_d = result.stderr.split("\n").slice(-1)[0]) == null ? void 0 : _d.slice(0, 200)) || "",
1006
+ totalLines: result.stderr.split("\n").length
1007
+ } : null
1008
+ };
1009
+ fs.appendFileSync(auditFile, JSON.stringify(auditEntry) + "\n", "utf-8");
1010
+ } catch (error) {
1011
+ console.warn(`[CommandExecutor] Failed to write audit log: ${error.message}`);
1012
+ }
1013
+ }
1014
+ /**
1015
+ * Get audit trail for a specific execution
1016
+ */
1017
+ getAuditTrail(executionId) {
1018
+ try {
1019
+ const auditDir = path.join(process.cwd(), ".agentflow", "audit");
1020
+ const auditEntries = [];
1021
+ if (!fs.existsSync(auditDir)) {
1022
+ return auditEntries;
1023
+ }
1024
+ const files = fs.readdirSync(auditDir).filter((f) => f.startsWith("command-executions-") && f.endsWith(".jsonl")).sort().slice(-7);
1025
+ for (const file of files) {
1026
+ const filePath = path.join(auditDir, file);
1027
+ try {
1028
+ const content = fs.readFileSync(filePath, "utf-8");
1029
+ const lines = content.trim().split("\n").filter((line) => line.trim());
1030
+ for (const line of lines) {
1031
+ try {
1032
+ const entry = JSON.parse(line);
1033
+ if (entry.executionId === executionId) {
1034
+ auditEntries.push(entry);
1035
+ }
1036
+ } catch {
1037
+ }
1038
+ }
1039
+ } catch {
1040
+ }
1041
+ }
1042
+ return auditEntries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
1043
+ } catch (error) {
1044
+ console.warn(`[CommandExecutor] Failed to get audit trail: ${error.message}`);
1045
+ return [];
1046
+ }
1047
+ }
1048
+ /**
1049
+ * Get audit statistics
1050
+ */
1051
+ getAuditStats(days = 7) {
1052
+ try {
1053
+ const auditDir = path.join(process.cwd(), ".agentflow", "audit");
1054
+ const stats = {
1055
+ totalExecutions: 0,
1056
+ successfulExecutions: 0,
1057
+ failedExecutions: 0,
1058
+ averageDuration: 0,
1059
+ commandFrequency: {},
1060
+ userActivity: {}
1061
+ };
1062
+ if (!fs.existsSync(auditDir)) {
1063
+ return stats;
1064
+ }
1065
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
1066
+ let totalDuration = 0;
1067
+ const files = fs.readdirSync(auditDir).filter((f) => f.startsWith("command-executions-") && f.endsWith(".jsonl")).sort().slice(-days);
1068
+ for (const file of files) {
1069
+ const filePath = path.join(auditDir, file);
1070
+ try {
1071
+ const content = fs.readFileSync(filePath, "utf-8");
1072
+ const lines = content.trim().split("\n").filter((line) => line.trim());
1073
+ for (const line of lines) {
1074
+ try {
1075
+ const entry = JSON.parse(line);
1076
+ const entryTime = new Date(entry.timestamp).getTime();
1077
+ if (entryTime < cutoff) continue;
1078
+ stats.totalExecutions++;
1079
+ if (entry.status === "completed") {
1080
+ stats.successfulExecutions++;
1081
+ } else if (["failed", "timeout", "killed"].includes(entry.status)) {
1082
+ stats.failedExecutions++;
1083
+ }
1084
+ if (entry.duration) {
1085
+ totalDuration += entry.duration;
1086
+ }
1087
+ const cmd = entry.commandId || "unknown";
1088
+ stats.commandFrequency[cmd] = (stats.commandFrequency[cmd] || 0) + 1;
1089
+ const user = entry.userAgent || "unknown";
1090
+ stats.userActivity[user] = (stats.userActivity[user] || 0) + 1;
1091
+ } catch {
1092
+ }
1093
+ }
1094
+ } catch {
1095
+ }
1096
+ }
1097
+ stats.averageDuration = stats.totalExecutions > 0 ? totalDuration / stats.totalExecutions : 0;
1098
+ return stats;
1099
+ } catch (error) {
1100
+ console.warn(`[CommandExecutor] Failed to get audit stats: ${error.message}`);
1101
+ return {
1102
+ totalExecutions: 0,
1103
+ successfulExecutions: 0,
1104
+ failedExecutions: 0,
1105
+ averageDuration: 0,
1106
+ commandFrequency: {},
1107
+ userActivity: {}
1108
+ };
1109
+ }
1110
+ }
1111
+ };
1112
+ function createCommandExecutor(config, options) {
1113
+ return new CommandExecutor(config, options);
1114
+ }
1115
+
516
1116
  // src/stats.ts
517
1117
  import { getFailures, getHungNodes, getStats } from "agentflow-core";
518
1118
  var AgentStats = class {
519
1119
  agentMetrics = /* @__PURE__ */ new Map();
520
1120
  processedTraces = /* @__PURE__ */ new Set();
521
1121
  processTrace(trace) {
522
- const traceKey = `${trace.filename || trace.agentId}-${trace.startTime}`;
1122
+ const traceKey = `${trace.agentId}#${trace.filename || trace.id}-${trace.startTime}`;
523
1123
  if (this.processedTraces.has(traceKey)) {
524
1124
  return;
525
1125
  }
@@ -690,8 +1290,8 @@ var AgentStats = class {
690
1290
 
691
1291
  // src/watcher.ts
692
1292
  import { EventEmitter } from "events";
693
- import * as fs from "fs";
694
- import * as path from "path";
1293
+ import * as fs2 from "fs";
1294
+ import * as path2 from "path";
695
1295
  import { loadGraph } from "agentflow-core";
696
1296
  import chokidar from "chokidar";
697
1297
 
@@ -739,13 +1339,19 @@ function extractKeyValuePairs(line) {
739
1339
  const kvRegex = /(\w+)=('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\S+)/g;
740
1340
  let match;
741
1341
  while ((match = kvRegex.exec(clean)) !== null) {
742
- if (match[1] === "Z" || match[1] === "m") continue;
743
- pairs[match[1]] = parseValue(match[2]);
1342
+ const key = match[1];
1343
+ const value = match[2];
1344
+ if (!key || !value) continue;
1345
+ if (key === "Z" || key === "m") continue;
1346
+ pairs[key] = parseValue(value);
744
1347
  }
745
1348
  return pairs;
746
1349
  }
747
1350
  function detectComponent(action, kvPairs) {
748
- if (action.includes(".")) return action.split(".")[0];
1351
+ if (action.includes(".")) {
1352
+ const parts = action.split(".");
1353
+ return parts[0] || action;
1354
+ }
749
1355
  if (kvPairs.component) return String(kvPairs.component);
750
1356
  if (kvPairs.service) return String(kvPairs.service);
751
1357
  if (kvPairs.module) return String(kvPairs.module);
@@ -783,7 +1389,9 @@ function detectActivityPattern(line) {
783
1389
  const pairs = {};
784
1390
  for (const m of kvMatches) {
785
1391
  const [key, value] = m.split("=", 2);
786
- pairs[key] = parseValue(value);
1392
+ if (key && value !== void 0) {
1393
+ pairs[key] = parseValue(value);
1394
+ }
787
1395
  }
788
1396
  timestamp = parseTimestamp(pairs.timestamp || pairs.time) || Date.now();
789
1397
  level = String(pairs.level || "info");
@@ -795,7 +1403,7 @@ function detectActivityPattern(line) {
795
1403
  const logMatch = line.match(
796
1404
  /^(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)\s+(\w+)?\s*:?\s*(.+)/
797
1405
  );
798
- if (logMatch) {
1406
+ if (logMatch && logMatch[1]) {
799
1407
  timestamp = new Date(logMatch[1]).getTime();
800
1408
  level = logMatch[2] || "info";
801
1409
  action = logMatch[3] || "";
@@ -833,11 +1441,38 @@ function getUniversalNodeStatus(activity) {
833
1441
  if (op.match(/complete|finish|end|done/i)) return "completed";
834
1442
  return "completed";
835
1443
  }
836
- function openClawSessionIdToAgent(sessionId) {
1444
+ function openClawSessionIdToAgent(sessionId, lookupMap) {
1445
+ if (lookupMap == null ? void 0 : lookupMap.has(sessionId)) {
1446
+ return lookupMap.get(sessionId);
1447
+ }
837
1448
  const firstSegment = sessionId.split("-")[0];
838
1449
  if (firstSegment) return firstSegment;
839
1450
  return "openclaw";
840
1451
  }
1452
+ function parseOpenClawSessionKey(key) {
1453
+ const parts = key.split(":");
1454
+ if (parts.length < 3 || parts[0] !== "agent") return null;
1455
+ const agentName = parts[1];
1456
+ const kind = parts[2];
1457
+ if (parts.length === 3 && kind === agentName) {
1458
+ return `openclaw:${agentName}`;
1459
+ }
1460
+ if (kind === "cron" && parts.length >= 4) {
1461
+ const runIdx = parts.indexOf("run", 4);
1462
+ const jobParts = runIdx > 0 ? parts.slice(3, runIdx) : parts.slice(3);
1463
+ const jobId = jobParts.join("-");
1464
+ return jobId ? `openclaw:${jobId}` : null;
1465
+ }
1466
+ if (parts.length >= 5) {
1467
+ const target = parts[4];
1468
+ return `openclaw:${kind}:${target}`;
1469
+ }
1470
+ if (parts.length >= 4) {
1471
+ const target = parts[3];
1472
+ return `openclaw:${kind}:${target}`;
1473
+ }
1474
+ return null;
1475
+ }
841
1476
 
842
1477
  // src/watcher.ts
843
1478
  var TraceWatcher = class _TraceWatcher extends EventEmitter {
@@ -848,19 +1483,21 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
848
1483
  allWatchDirs;
849
1484
  maxAgeMs;
850
1485
  userConfig;
1486
+ /** Maps OpenClaw session UUIDs to resolved agentIds (populated from sessions.json) */
1487
+ sessionAgentMap = /* @__PURE__ */ new Map();
851
1488
  constructor(tracesDirOrOptions) {
852
1489
  super();
853
1490
  const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
854
1491
  const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
855
1492
  const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
856
1493
  if (typeof tracesDirOrOptions === "string") {
857
- this.tracesDir = path.resolve(tracesDirOrOptions);
1494
+ this.tracesDir = path2.resolve(tracesDirOrOptions);
858
1495
  this.dataDirs = [];
859
1496
  this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
860
1497
  this.userConfig = {};
861
1498
  } else {
862
- this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
863
- this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
1499
+ this.tracesDir = path2.resolve(tracesDirOrOptions.tracesDir);
1500
+ this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path2.resolve(d));
864
1501
  this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
865
1502
  this.userConfig = tracesDirOrOptions.userConfig ?? {};
866
1503
  }
@@ -869,7 +1506,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
869
1506
  ...getSkipFiles(this.userConfig)
870
1507
  ]);
871
1508
  this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
872
- this.allWatchDirs = [...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))];
1509
+ this.allWatchDirs = [
1510
+ ...new Set([this.tracesDir, ...this.dataDirs].map((d) => path2.resolve(d)))
1511
+ ];
873
1512
  this.ensureTracesDir();
874
1513
  this.loadExistingFiles();
875
1514
  this.archiveOldTraces();
@@ -879,9 +1518,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
879
1518
  /** Move trace files older than maxAgeMs into archive/YYYY-MM/ subdirectories. */
880
1519
  archiveOldTraces() {
881
1520
  const cutoff = Date.now() - this.maxAgeMs;
882
- let archived = 0;
1521
+ const _archived = 0;
883
1522
  for (const dir of this.allWatchDirs) {
884
- if (!fs.existsSync(dir)) continue;
1523
+ if (!fs2.existsSync(dir)) continue;
885
1524
  try {
886
1525
  this.archiveDirectory(dir, cutoff, 0);
887
1526
  } catch (error) {
@@ -891,27 +1530,28 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
891
1530
  }
892
1531
  archiveDirectory(dir, cutoff, depth) {
893
1532
  if (depth > 10) return 0;
894
- if (path.basename(dir) === "archive") return 0;
1533
+ if (path2.basename(dir) === "archive") return 0;
895
1534
  let archived = 0;
896
1535
  try {
897
- const entries = fs.readdirSync(dir, { withFileTypes: true });
1536
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
898
1537
  for (const entry of entries) {
899
- if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name)) continue;
900
- const fullPath = path.join(dir, entry.name);
1538
+ if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name))
1539
+ continue;
1540
+ const fullPath = path2.join(dir, entry.name);
901
1541
  if (entry.isDirectory()) {
902
1542
  archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
903
1543
  continue;
904
1544
  }
905
1545
  if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
906
1546
  try {
907
- const stats = fs.statSync(fullPath);
1547
+ const stats = fs2.statSync(fullPath);
908
1548
  if (stats.mtimeMs >= cutoff) continue;
909
1549
  const mtime = new Date(stats.mtimeMs);
910
1550
  const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
911
- const archiveDir = path.join(this.tracesDir, "archive", yearMonth);
912
- fs.mkdirSync(archiveDir, { recursive: true });
913
- const dest = path.join(archiveDir, entry.name);
914
- fs.renameSync(fullPath, dest);
1551
+ const archiveDir = path2.join(this.tracesDir, "archive", yearMonth);
1552
+ fs2.mkdirSync(archiveDir, { recursive: true });
1553
+ const dest = path2.join(archiveDir, entry.name);
1554
+ fs2.renameSync(fullPath, dest);
915
1555
  const key = this.traceKey(fullPath);
916
1556
  this.traces.delete(key);
917
1557
  archived++;
@@ -923,16 +1563,19 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
923
1563
  return archived;
924
1564
  }
925
1565
  ensureTracesDir() {
926
- if (!fs.existsSync(this.tracesDir)) {
927
- fs.mkdirSync(this.tracesDir, { recursive: true });
928
- console.log(`Created traces directory: ${this.tracesDir}`);
1566
+ try {
1567
+ if (!fs2.existsSync(this.tracesDir)) {
1568
+ fs2.mkdirSync(this.tracesDir, { recursive: true });
1569
+ console.log(`Created traces directory: ${this.tracesDir}`);
1570
+ }
1571
+ } catch {
929
1572
  }
930
1573
  }
931
1574
  loadExistingFiles() {
932
1575
  let totalFiles = 0;
933
1576
  let totalDirectories = 0;
934
1577
  for (const dir of this.allWatchDirs) {
935
- if (!fs.existsSync(dir)) continue;
1578
+ if (!fs2.existsSync(dir)) continue;
936
1579
  try {
937
1580
  totalDirectories++;
938
1581
  const loadedFiles = this.scanDirectoryRecursive(dir);
@@ -950,16 +1593,16 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
950
1593
  if (depth > 10) return 0;
951
1594
  let fileCount = 0;
952
1595
  try {
953
- const entries = fs.readdirSync(dir, { withFileTypes: true });
1596
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
954
1597
  for (const entry of entries) {
955
1598
  if (entry.name.startsWith(".")) continue;
956
1599
  if (entry.name === "archive") continue;
957
1600
  if (this.userSkipDirs.has(entry.name)) continue;
958
- const fullPath = path.join(dir, entry.name);
1601
+ const fullPath = path2.join(dir, entry.name);
959
1602
  if (entry.isFile()) {
960
1603
  if (this.isSupportedFile(entry.name)) {
961
1604
  try {
962
- const mtime = fs.statSync(fullPath).mtimeMs;
1605
+ const mtime = fs2.statSync(fullPath).mtimeMs;
963
1606
  if (Date.now() - mtime > this.maxAgeMs) continue;
964
1607
  } catch {
965
1608
  continue;
@@ -1011,7 +1654,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1011
1654
  ];
1012
1655
  /** Load a file using the adapter registry, falling back to built-in parsing. */
1013
1656
  loadFile(filePath) {
1014
- const filename = path.basename(filePath);
1657
+ const filename = path2.basename(filePath);
1015
1658
  if (this.skipFiles.has(filename)) return false;
1016
1659
  if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
1017
1660
  const adapter = findAdapter(filePath);
@@ -1064,9 +1707,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1064
1707
  metadata: { ...trace.metadata, adapterSource: adapterName },
1065
1708
  sessionEvents: trace.sessionEvents ?? [],
1066
1709
  sourceType: "session",
1067
- filename: path.basename(filePath),
1710
+ filename: path2.basename(filePath),
1068
1711
  lastModified: Date.now(),
1069
- sourceDir: path.dirname(filePath)
1712
+ sourceDir: path2.dirname(filePath)
1070
1713
  };
1071
1714
  const key = `${adapterName}:${trace.id}`;
1072
1715
  this.traces.set(key, watched);
@@ -1079,9 +1722,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1079
1722
  }
1080
1723
  loadLogFile(filePath) {
1081
1724
  try {
1082
- const content = fs.readFileSync(filePath, "utf8");
1083
- const filename = path.basename(filePath);
1084
- const stats = fs.statSync(filePath);
1725
+ const content = fs2.readFileSync(filePath, "utf8");
1726
+ const filename = path2.basename(filePath);
1727
+ const stats = fs2.statSync(filePath);
1085
1728
  if (filename.startsWith("openclaw-") || filePath.includes("openclaw")) {
1086
1729
  const result = this.loadOpenClawLogFile(content, filename, filePath, stats);
1087
1730
  if (result) return true;
@@ -1099,8 +1742,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1099
1742
  trace.filename = filename;
1100
1743
  trace.lastModified = stats.mtime.getTime();
1101
1744
  trace.sourceType = trace.sourceType || "trace";
1102
- trace.sourceDir = path.dirname(filePath);
1103
- const key = traces.length === 1 ? this.traceKey(filePath) : `${this.traceKey(filePath)}-${i}`;
1745
+ trace.sourceDir = path2.dirname(filePath);
1746
+ const traceAgentId = trace.agentId;
1747
+ const key = traces.length === 1 ? this.traceKey(filePath, traceAgentId) : `${this.traceKey(filePath, traceAgentId)}-${i}`;
1104
1748
  this.traces.set(key, trace);
1105
1749
  }
1106
1750
  return traces.length > 0;
@@ -1161,7 +1805,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1161
1805
  trace.sourceType = "session";
1162
1806
  }
1163
1807
  if (traces.length === 0) {
1164
- const stats = fs.statSync(filePath);
1808
+ const stats = fs2.statSync(filePath);
1165
1809
  traces.push({
1166
1810
  id: "",
1167
1811
  rootNodeId: "root",
@@ -1201,10 +1845,10 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1201
1845
  const pathAgent = this.extractAgentFromPath(filePath);
1202
1846
  const detection = getAgentDetection(this.userConfig);
1203
1847
  if (detection.filePatterns) {
1204
- const basename3 = path.basename(filePath, path.extname(filePath));
1848
+ const basename4 = path2.basename(filePath, path2.extname(filePath));
1205
1849
  for (const [pattern, template] of Object.entries(detection.filePatterns)) {
1206
1850
  const re = new RegExp(`^(${pattern})$`);
1207
- const match = basename3.match(re);
1851
+ const match = basename4.match(re);
1208
1852
  if (match) {
1209
1853
  const resolved = template.replace("${match}", match[1]);
1210
1854
  return this.normaliseAgentId(resolved);
@@ -1214,8 +1858,8 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1214
1858
  return this.normaliseAgentId(pathAgent);
1215
1859
  }
1216
1860
  extractAgentFromPath(filePath) {
1217
- const filename = path.basename(filePath, path.extname(filePath));
1218
- const pathParts = filePath.split(path.sep);
1861
+ const filename = path2.basename(filePath, path2.extname(filePath));
1862
+ const pathParts = filePath.split(path2.sep);
1219
1863
  const detection = getAgentDetection(this.userConfig);
1220
1864
  let pathPrefix = "";
1221
1865
  if (detection.pathPatterns) {
@@ -1286,7 +1930,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1286
1930
  if ((inner == null ? void 0 : inner.payloads) && ((_a = inner == null ? void 0 : inner.meta) == null ? void 0 : _a.agentMeta)) {
1287
1931
  const agentMeta = inner.meta.agentMeta;
1288
1932
  const sessionId = agentMeta.sessionId || "unknown";
1289
- const agentName = openClawSessionIdToAgent(sessionId);
1933
+ const agentName = openClawSessionIdToAgent(sessionId, this.sessionAgentMap);
1290
1934
  const timestamp = parsed.time ? new Date(parsed.time).getTime() : ((_b = parsed._meta) == null ? void 0 : _b.date) ? new Date(parsed._meta.date).getTime() : stats.mtime.getTime();
1291
1935
  const texts = (inner.payloads || []).map((p) => p.text || "").filter(Boolean);
1292
1936
  if (!sessions.has(sessionId)) {
@@ -1310,7 +1954,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1310
1954
  if (parsed.payloads && ((_d = parsed.meta) == null ? void 0 : _d.agentMeta)) {
1311
1955
  const agentMeta = parsed.meta.agentMeta;
1312
1956
  const sessionId = agentMeta.sessionId || "unknown";
1313
- const agentName = openClawSessionIdToAgent(sessionId);
1957
+ const agentName = openClawSessionIdToAgent(sessionId, this.sessionAgentMap);
1314
1958
  const timestamp = parsed.time ? new Date(parsed.time).getTime() : ((_e = parsed._meta) == null ? void 0 : _e.date) ? new Date(parsed._meta.date).getTime() : stats.mtime.getTime();
1315
1959
  const texts = (parsed.payloads || []).map((p) => p.text || "").filter(Boolean);
1316
1960
  if (!sessions.has(sessionId)) {
@@ -1418,7 +2062,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1418
2062
  filename,
1419
2063
  lastModified: stats.mtime.getTime(),
1420
2064
  sourceType: "session",
1421
- sourceDir: path.dirname(filePath),
2065
+ sourceDir: path2.dirname(filePath),
1422
2066
  sessionEvents,
1423
2067
  tokenUsage: {
1424
2068
  input: totalInput,
@@ -1433,7 +2077,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1433
2077
  source: "openclaw-log"
1434
2078
  }
1435
2079
  };
1436
- const key = sessions.size === 1 ? this.traceKey(filePath) : `${this.traceKey(filePath)}-${traceIndex}`;
2080
+ const key = sessions.size === 1 ? this.traceKey(filePath, agentId) : `${this.traceKey(filePath, agentId)}-${traceIndex}`;
1437
2081
  this.traces.set(key, trace);
1438
2082
  traceIndex++;
1439
2083
  }
@@ -1441,23 +2085,26 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1441
2085
  }
1442
2086
  loadTraceFile(filePath) {
1443
2087
  try {
1444
- const content = fs.readFileSync(filePath, "utf8");
1445
- const filename = path.basename(filePath);
2088
+ const content = fs2.readFileSync(filePath, "utf8");
2089
+ const filename = path2.basename(filePath);
1446
2090
  if (filename === "sessions.json") {
1447
2091
  return this.loadSessionsIndex(filePath, content);
1448
2092
  }
1449
2093
  const graph = loadGraph(content);
1450
- const stats = fs.statSync(filePath);
2094
+ const stats = fs2.statSync(filePath);
1451
2095
  graph.filename = filename;
1452
2096
  graph.lastModified = stats.mtime.getTime();
1453
2097
  graph.sourceType = "trace";
1454
- graph.sourceDir = path.dirname(filePath);
2098
+ graph.sourceDir = path2.dirname(filePath);
2099
+ if (!graph.agentId || graph.agentId === "unknown") {
2100
+ graph.agentId = this.extractAgentFromPath(filePath);
2101
+ }
1455
2102
  if (graph.nodes instanceof Map) {
1456
2103
  for (const node of graph.nodes.values()) {
1457
2104
  if (!node.children) node.children = [];
1458
2105
  }
1459
2106
  }
1460
- this.traces.set(this.traceKey(filePath), graph);
2107
+ this.traces.set(this.traceKey(filePath, graph.agentId), graph);
1461
2108
  return true;
1462
2109
  } catch {
1463
2110
  return false;
@@ -1465,11 +2112,12 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1465
2112
  }
1466
2113
  /** Parse sessions.json index to discover agents and their sessions. */
1467
2114
  loadSessionsIndex(filePath, content) {
2115
+ var _a;
1468
2116
  try {
1469
2117
  const data = JSON.parse(content);
1470
2118
  if (typeof data !== "object" || data === null) return false;
1471
- const stats = fs.statSync(filePath);
1472
- const pathParts = filePath.split(path.sep);
2119
+ const stats = fs2.statSync(filePath);
2120
+ const pathParts = filePath.split(path2.sep);
1473
2121
  const agentsIndex = pathParts.lastIndexOf("agents");
1474
2122
  if (agentsIndex === -1 || agentsIndex + 1 >= pathParts.length) return false;
1475
2123
  const agentName = pathParts[agentsIndex + 1];
@@ -1480,11 +2128,19 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1480
2128
  const session = sessionData;
1481
2129
  const sessionId = session.sessionId;
1482
2130
  if (!sessionId) continue;
2131
+ const resolvedAgentId = parseOpenClawSessionKey(sessionKey) ?? agentId;
2132
+ this.sessionAgentMap.set(String(sessionId), resolvedAgentId);
1483
2133
  const existingKey = Array.from(this.traces.keys()).find((k) => {
1484
2134
  const t = this.traces.get(k);
1485
2135
  return (t == null ? void 0 : t.id) === sessionId || (t == null ? void 0 : t.traceId) === sessionId;
1486
2136
  });
1487
- if (existingKey) continue;
2137
+ if (existingKey) {
2138
+ const existing = this.traces.get(existingKey);
2139
+ if (existing && (existing.agentId === agentId || ((_a = existing.agentId) == null ? void 0 : _a.startsWith("openclaw-main")))) {
2140
+ existing.agentId = resolvedAgentId;
2141
+ }
2142
+ continue;
2143
+ }
1488
2144
  const updatedAt = session.updatedAt || stats.mtime.getTime();
1489
2145
  const label = session.label || sessionKey.split(":").pop() || sessionId;
1490
2146
  const chatType = session.chatType || (sessionKey.includes("cron") ? "cron" : "direct");
@@ -1513,7 +2169,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1513
2169
  edges: [],
1514
2170
  events: [],
1515
2171
  startTime: updatedAt,
1516
- agentId,
2172
+ agentId: resolvedAgentId,
1517
2173
  trigger,
1518
2174
  name: label,
1519
2175
  traceId: sessionId,
@@ -1521,7 +2177,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1521
2177
  filename: `${agentName}-${sessionId.slice(0, 8)}.index`,
1522
2178
  lastModified: updatedAt,
1523
2179
  sourceType: "session",
1524
- sourceDir: path.dirname(filePath),
2180
+ sourceDir: path2.dirname(filePath),
1525
2181
  sessionEvents: [],
1526
2182
  tokenUsage: { input: 0, output: 0, total: 0, cost: 0 },
1527
2183
  metadata: {
@@ -1531,7 +2187,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1531
2187
  agentName
1532
2188
  }
1533
2189
  };
1534
- const key = `${this.traceKey(filePath)}-${sessionId.slice(0, 12)}`;
2190
+ const key = `${this.traceKey(filePath, agentId)}-${sessionId.slice(0, 12)}`;
1535
2191
  this.traces.set(key, trace);
1536
2192
  loaded++;
1537
2193
  }
@@ -1544,7 +2200,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1544
2200
  loadSessionFile(filePath) {
1545
2201
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
1546
2202
  try {
1547
- const content = fs.readFileSync(filePath, "utf8");
2203
+ const content = fs2.readFileSync(filePath, "utf8");
1548
2204
  const lines = content.split("\n").filter((l) => l.trim());
1549
2205
  if (lines.length === 0) return false;
1550
2206
  const rawEvents = [];
@@ -1560,13 +2216,13 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1560
2216
  return this.loadCronRunFile(rawEvents, filePath);
1561
2217
  }
1562
2218
  const sessionEvent = rawEvents.find((e) => e.type === "session");
1563
- const sessionId = (sessionEvent == null ? void 0 : sessionEvent.id) || path.basename(filePath, ".jsonl");
2219
+ const sessionId = (sessionEvent == null ? void 0 : sessionEvent.id) || path2.basename(filePath, ".jsonl");
1564
2220
  const sessionTimestamp = (sessionEvent == null ? void 0 : sessionEvent.timestamp) || ((_a = rawEvents[0]) == null ? void 0 : _a.timestamp);
1565
2221
  const startTime = sessionTimestamp ? new Date(sessionTimestamp).getTime() : 0;
1566
2222
  if (!startTime) return false;
1567
- const parentDir = path.basename(path.dirname(filePath));
1568
- const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
1569
- const greatGrandParentDir = path.basename(path.dirname(path.dirname(path.dirname(filePath))));
2223
+ const parentDir = path2.basename(path2.dirname(filePath));
2224
+ const grandParentDir = path2.basename(path2.dirname(path2.dirname(filePath)));
2225
+ const greatGrandParentDir = path2.basename(path2.dirname(path2.dirname(path2.dirname(filePath))));
1570
2226
  let agentName;
1571
2227
  if (parentDir === "sessions" && greatGrandParentDir === "agents") {
1572
2228
  agentName = grandParentDir;
@@ -1585,6 +2241,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1585
2241
  }
1586
2242
  }
1587
2243
  }
2244
+ if (this.sessionAgentMap.has(sessionId)) {
2245
+ agentId = this.sessionAgentMap.get(sessionId);
2246
+ }
1588
2247
  const modelEvent = rawEvents.find((e) => e.type === "model_change");
1589
2248
  const provider = (modelEvent == null ? void 0 : modelEvent.provider) || "";
1590
2249
  const modelId = (modelEvent == null ? void 0 : modelEvent.modelId) || "";
@@ -1788,7 +2447,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1788
2447
  if (role === "toolResult") {
1789
2448
  const toolCallId = ((_j = contentBlocks[0]) == null ? void 0 : _j.toolCallId) || evt.parentId;
1790
2449
  const resultContent = contentBlocks.map((b) => b.text || b.content || "").join("\n");
1791
- const hasError = contentBlocks.some((b) => b.isError || b.error);
2450
+ const hasError = contentBlocks.some(
2451
+ (b) => b.isError || b.error
2452
+ );
1792
2453
  const errorText = hasError ? resultContent : void 0;
1793
2454
  sessionEvents.push({
1794
2455
  type: "tool_result",
@@ -1816,7 +2477,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1816
2477
  }
1817
2478
  }
1818
2479
  }
1819
- const fileStat = fs.statSync(filePath);
2480
+ const fileStat = fs2.statSync(filePath);
1820
2481
  const fileAge = Date.now() - fileStat.mtime.getTime();
1821
2482
  const lastEvt = rawEvents[rawEvents.length - 1];
1822
2483
  const hasToolError = sessionEvents.some((e) => e.type === "tool_result" && e.toolError);
@@ -1864,7 +2525,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1864
2525
  "gen_ai.request.model": modelId
1865
2526
  }
1866
2527
  });
1867
- const filename = path.basename(filePath);
2528
+ const filename = path2.basename(filePath);
1868
2529
  const trace = {
1869
2530
  id: sessionId,
1870
2531
  nodes,
@@ -1880,7 +2541,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1880
2541
  filename,
1881
2542
  lastModified: fileStat.mtime.getTime(),
1882
2543
  sourceType: "session",
1883
- sourceDir: path.dirname(filePath),
2544
+ sourceDir: path2.dirname(filePath),
1884
2545
  sessionEvents,
1885
2546
  tokenUsage,
1886
2547
  metadata: {
@@ -1894,93 +2555,180 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1894
2555
  sessionVersion: sessionEvent == null ? void 0 : sessionEvent.version
1895
2556
  }
1896
2557
  };
1897
- this.traces.set(this.traceKey(filePath), trace);
2558
+ this.traces.set(this.traceKey(filePath, agentId), trace);
1898
2559
  return true;
1899
2560
  } catch {
1900
2561
  return false;
1901
2562
  }
1902
2563
  }
1903
- /** Parse cron run JSONL files (ts, jobId, action, status format). */
2564
+ /** Parse cron run JSONL files creates one trace per execution run. */
1904
2565
  loadCronRunFile(rawEvents, filePath) {
1905
- var _a, _b, _c;
2566
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1906
2567
  try {
1907
- const filename = path.basename(filePath);
1908
- const jobId = ((_a = rawEvents[0]) == null ? void 0 : _a.jobId) || path.basename(filePath, ".jsonl");
1909
- const fileStat = fs.statSync(filePath);
1910
- const sessionEvents = [];
1911
- let lastStatus = "completed";
2568
+ const filename = path2.basename(filePath);
2569
+ const fileJobId = String(((_a = rawEvents[0]) == null ? void 0 : _a.jobId) || path2.basename(filePath, ".jsonl"));
2570
+ const fileStat = fs2.statSync(filePath);
2571
+ const agentId = `openclaw:${fileJobId}`;
2572
+ let loaded = 0;
1912
2573
  for (const evt of rawEvents) {
1913
2574
  const ts = evt.ts || Date.now();
1914
- const action = evt.action || "unknown";
1915
- const status = evt.status || "ok";
1916
- if (status !== "ok") lastStatus = "failed";
1917
- sessionEvents.push({
1918
- type: action === "finished" ? "assistant" : "system",
1919
- timestamp: ts,
1920
- name: `${jobId}: ${action}`,
1921
- content: evt.summary || evt.error || `${action} (${status})`,
1922
- id: `cron-${ts}`
2575
+ const runAtMs = evt.runAtMs || ts;
2576
+ const durationMs = evt.durationMs || 0;
2577
+ const status = evt.status === "ok" ? "completed" : "failed";
2578
+ const sessionId = String(evt.sessionId || `${fileJobId}-${ts}`);
2579
+ const summary = String(evt.summary || evt.error || "");
2580
+ const model = String(evt.model || "");
2581
+ const usage = evt.usage || {};
2582
+ const rootId = `cron-${sessionId.slice(0, 12)}`;
2583
+ const nodes = /* @__PURE__ */ new Map();
2584
+ const children = [];
2585
+ nodes.set(rootId, {
2586
+ id: rootId,
2587
+ type: "agent",
2588
+ name: `${fileJobId} run`,
2589
+ startTime: runAtMs,
2590
+ endTime: runAtMs + durationMs,
2591
+ status,
2592
+ parentId: void 0,
2593
+ children,
2594
+ metadata: {
2595
+ jobId: fileJobId,
2596
+ model,
2597
+ sessionId,
2598
+ durationMs
2599
+ }
1923
2600
  });
1924
- }
1925
- const firstTs = ((_b = rawEvents[0]) == null ? void 0 : _b.ts) || fileStat.mtime.getTime();
1926
- const lastTs = ((_c = rawEvents[rawEvents.length - 1]) == null ? void 0 : _c.ts) || fileStat.mtime.getTime();
1927
- const rootId = `cron-${jobId.slice(0, 12)}`;
1928
- const nodes = /* @__PURE__ */ new Map();
1929
- nodes.set(rootId, {
1930
- id: rootId,
1931
- type: "agent",
1932
- name: jobId,
1933
- startTime: firstTs,
1934
- endTime: lastTs,
1935
- status: lastStatus,
1936
- parentId: void 0,
1937
- children: [],
1938
- metadata: { jobId, runs: rawEvents.length }
1939
- });
1940
- const trace = {
1941
- id: jobId,
1942
- nodes,
1943
- edges: [],
1944
- events: [],
1945
- startTime: firstTs,
1946
- agentId: "openclaw-cron",
1947
- trigger: "cron",
1948
- name: jobId,
1949
- traceId: jobId,
1950
- spanId: jobId,
1951
- filename,
1952
- lastModified: fileStat.mtime.getTime(),
1953
- sourceType: "session",
1954
- sourceDir: path.dirname(filePath),
1955
- sessionEvents,
1956
- tokenUsage: { input: 0, output: 0, total: 0, cost: 0 },
1957
- metadata: { jobId, source: "cron-run" }
1958
- };
1959
- this.traces.set(this.traceKey(filePath), trace);
1960
- return true;
2601
+ let stepIdx = 0;
2602
+ const tablePattern = /\|\s*\*?\*?(\d+)\.\s*\*?\*?([^|]+)\*?\*?\s*\|\s*([^|]+)\s*\|\s*([^|]*)\|/g;
2603
+ let m;
2604
+ while ((m = tablePattern.exec(summary)) !== null) {
2605
+ stepIdx++;
2606
+ const stepName = ((_b = m[2]) == null ? void 0 : _b.trim().replace(/\*+/g, "")) || `Step ${stepIdx}`;
2607
+ const stepDetail = ((_c = m[4]) == null ? void 0 : _c.trim()) || "";
2608
+ const nodeId = `step-${stepIdx}`;
2609
+ children.push(nodeId);
2610
+ nodes.set(nodeId, {
2611
+ id: nodeId,
2612
+ type: "tool",
2613
+ name: stepName,
2614
+ startTime: runAtMs + (stepIdx - 1) * Math.floor(durationMs / Math.max(1, stepIdx + 1)),
2615
+ endTime: runAtMs + stepIdx * Math.floor(durationMs / Math.max(1, stepIdx + 1)),
2616
+ status: ((_d = m[3]) == null ? void 0 : _d.includes("\u274C")) || ((_e = m[3]) == null ? void 0 : _e.includes("Fail")) ? "failed" : "completed",
2617
+ parentId: rootId,
2618
+ children: [],
2619
+ metadata: { detail: stepDetail }
2620
+ });
2621
+ }
2622
+ if (stepIdx === 0) {
2623
+ const listPattern = /^\s*(\d+)\.\s*\*?\*?([^*\n]+)\*?\*?\s*(?:[-—]\s*(.+))?$/gm;
2624
+ while ((m = listPattern.exec(summary)) !== null) {
2625
+ if ((_f = m[2]) == null ? void 0 : _f.trim().startsWith("#")) continue;
2626
+ stepIdx++;
2627
+ const stepName = ((_g = m[2]) == null ? void 0 : _g.trim().replace(/\*+/g, "")) || `Step ${stepIdx}`;
2628
+ const stepDetail = ((_h = m[3]) == null ? void 0 : _h.trim()) || "";
2629
+ const nodeId = `step-${stepIdx}`;
2630
+ children.push(nodeId);
2631
+ nodes.set(nodeId, {
2632
+ id: nodeId,
2633
+ type: "tool",
2634
+ name: stepName,
2635
+ startTime: runAtMs + (stepIdx - 1) * Math.floor(durationMs / Math.max(1, stepIdx + 1)),
2636
+ endTime: runAtMs + stepIdx * Math.floor(durationMs / Math.max(1, stepIdx + 1)),
2637
+ status: stepDetail.toLowerCase().includes("fail") ? "failed" : "completed",
2638
+ parentId: rootId,
2639
+ children: [],
2640
+ metadata: { detail: stepDetail }
2641
+ });
2642
+ }
2643
+ }
2644
+ if (stepIdx === 0 && summary) {
2645
+ const sumId = "summary-0";
2646
+ children.push(sumId);
2647
+ nodes.set(sumId, {
2648
+ id: sumId,
2649
+ type: "tool",
2650
+ name: status === "completed" ? "Execution" : evt.error ? String(evt.error) : "Execution",
2651
+ startTime: runAtMs,
2652
+ endTime: runAtMs + durationMs,
2653
+ status,
2654
+ parentId: rootId,
2655
+ children: [],
2656
+ metadata: { summary: summary.slice(0, 500) }
2657
+ });
2658
+ }
2659
+ const sessionEvents = [
2660
+ {
2661
+ type: "system",
2662
+ timestamp: runAtMs,
2663
+ name: `${fileJobId} started`,
2664
+ content: `Model: ${model}`,
2665
+ id: `start-${ts}`
2666
+ }
2667
+ ];
2668
+ if (summary) {
2669
+ sessionEvents.push({
2670
+ type: "assistant",
2671
+ timestamp: runAtMs + durationMs,
2672
+ name: status === "completed" ? "Completed" : "Failed",
2673
+ content: summary,
2674
+ id: `result-${ts}`
2675
+ });
2676
+ }
2677
+ const trace = {
2678
+ id: sessionId,
2679
+ nodes,
2680
+ edges: [],
2681
+ events: [],
2682
+ startTime: runAtMs,
2683
+ agentId,
2684
+ trigger: "cron",
2685
+ name: `${fileJobId} ${new Date(runAtMs).toISOString().slice(0, 16)}`,
2686
+ traceId: sessionId,
2687
+ spanId: sessionId,
2688
+ filename,
2689
+ lastModified: fileStat.mtime.getTime(),
2690
+ sourceType: "session",
2691
+ sourceDir: path2.dirname(filePath),
2692
+ sessionEvents,
2693
+ tokenUsage: {
2694
+ input: usage.promptTokens || usage.input || 0,
2695
+ output: usage.completionTokens || usage.output || 0,
2696
+ total: usage.totalTokens || usage.total || 0,
2697
+ cost: 0
2698
+ },
2699
+ metadata: { jobId: fileJobId, model, source: "cron-run", sessionId }
2700
+ };
2701
+ const key = `${this.traceKey(filePath, agentId)}-${sessionId.slice(0, 12)}`;
2702
+ this.traces.set(key, trace);
2703
+ loaded++;
2704
+ }
2705
+ return loaded > 0;
1961
2706
  } catch {
1962
2707
  return false;
1963
2708
  }
1964
2709
  }
1965
- /** Unique key for a file across directories. */
1966
- traceKey(filePath) {
2710
+ /** Unique key for a file across directories. Includes agentId to prevent collisions between agents. */
2711
+ traceKey(filePath, agentId) {
2712
+ let fileKey;
1967
2713
  for (const dir of this.allWatchDirs) {
1968
2714
  if (filePath.startsWith(dir)) {
1969
- const dirParts = dir.split(path.sep).filter(Boolean);
2715
+ const dirParts = dir.split(path2.sep).filter(Boolean);
1970
2716
  const dirSuffix = dirParts.slice(-2).join("/");
1971
- return `${path.relative(dir, filePath).replace(/\\/g, "/")}@${dirSuffix}`;
2717
+ fileKey = `${path2.relative(dir, filePath).replace(/\\/g, "/")}@${dirSuffix}`;
2718
+ return agentId ? `${fileKey}#${agentId}` : fileKey;
1972
2719
  }
1973
2720
  }
1974
- return filePath;
2721
+ fileKey = filePath;
2722
+ return agentId ? `${fileKey}#${agentId}` : fileKey;
1975
2723
  }
1976
2724
  startWatching() {
1977
2725
  for (const dir of this.allWatchDirs) {
1978
- if (!fs.existsSync(dir)) continue;
2726
+ if (!fs2.existsSync(dir)) continue;
1979
2727
  const patterns = [
1980
- path.join(dir, "**/*.json"),
1981
- path.join(dir, "**/*.jsonl"),
1982
- path.join(dir, "**/*.log"),
1983
- path.join(dir, "**/*.trace")
2728
+ path2.join(dir, "**/*.json"),
2729
+ path2.join(dir, "**/*.jsonl"),
2730
+ path2.join(dir, "**/*.log"),
2731
+ path2.join(dir, "**/*.trace")
1984
2732
  ];
1985
2733
  const watcher = chokidar.watch(patterns, {
1986
2734
  ignored: [
@@ -2006,9 +2754,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
2006
2754
  // Allow deep nesting for OpenClaw agents/*/sessions/
2007
2755
  });
2008
2756
  watcher.on("add", (filePath) => {
2009
- if (this.isSupportedFile(path.basename(filePath))) {
2010
- const relativePath = path.relative(dir, filePath);
2011
- console.log(`New file: ${relativePath} (in ${path.basename(dir)})`);
2757
+ if (this.isSupportedFile(path2.basename(filePath))) {
2758
+ const relativePath = path2.relative(dir, filePath);
2759
+ console.log(`New file: ${relativePath} (in ${path2.basename(dir)})`);
2012
2760
  if (this.loadFile(filePath)) {
2013
2761
  const key = this.traceKey(filePath);
2014
2762
  const trace = this.traces.get(key);
@@ -2019,7 +2767,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
2019
2767
  }
2020
2768
  });
2021
2769
  watcher.on("change", (filePath) => {
2022
- if (this.isSupportedFile(path.basename(filePath))) {
2770
+ if (this.isSupportedFile(path2.basename(filePath))) {
2023
2771
  if (this.loadFile(filePath)) {
2024
2772
  const key = this.traceKey(filePath);
2025
2773
  const trace = this.traces.get(key);
@@ -2030,7 +2778,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
2030
2778
  }
2031
2779
  });
2032
2780
  watcher.on("unlink", (filePath) => {
2033
- if (this.isSupportedFile(path.basename(filePath))) {
2781
+ if (this.isSupportedFile(path2.basename(filePath))) {
2034
2782
  const key = this.traceKey(filePath);
2035
2783
  this.traces.delete(key);
2036
2784
  this.emit("trace-removed", key);
@@ -2054,6 +2802,10 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
2054
2802
  let candidates = [];
2055
2803
  const exact = this.traces.get(filename);
2056
2804
  if (exact) candidates.push(exact);
2805
+ if (agentId && !filename.includes("#")) {
2806
+ const agentKeyed = this.traces.get(`${filename}#${agentId}`);
2807
+ if (agentKeyed) candidates.push(agentKeyed);
2808
+ }
2057
2809
  if (filename.includes("::")) {
2058
2810
  const [fname, startTimeStr] = filename.split("::");
2059
2811
  const startTime = Number(startTimeStr);
@@ -2074,19 +2826,21 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
2074
2826
  candidates.push(trace);
2075
2827
  }
2076
2828
  }
2829
+ candidates = [...new Set(candidates)];
2077
2830
  if (candidates.length === 0) return void 0;
2078
2831
  if (candidates.length === 1) return candidates[0];
2079
2832
  if (agentId) {
2080
2833
  const agentMatches = candidates.filter((c) => c.agentId === agentId);
2081
- if (agentMatches.length > 0) {
2082
- candidates = agentMatches;
2083
- }
2834
+ if (agentMatches.length === 1) return agentMatches[0];
2835
+ if (agentMatches.length > 1) candidates = agentMatches;
2836
+ if (agentMatches.length === 0) return void 0;
2084
2837
  }
2085
- if (candidates.length === 1) return candidates[0];
2086
2838
  let best = candidates[0];
2839
+ if (!best) return void 0;
2087
2840
  let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
2088
2841
  for (let i = 1; i < candidates.length; i++) {
2089
2842
  const c = candidates[i];
2843
+ if (!c) continue;
2090
2844
  const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
2091
2845
  if (nc > bestNodeCount) {
2092
2846
  best = c;
@@ -2137,12 +2891,14 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
2137
2891
  };
2138
2892
 
2139
2893
  // src/cli.ts
2140
- import * as fs2 from "fs";
2894
+ import * as fs3 from "fs";
2141
2895
  import * as os from "os";
2142
- import * as path2 from "path";
2896
+ import * as path3 from "path";
2143
2897
  import { fileURLToPath } from "url";
2144
- var __cliDirname = path2.dirname(fileURLToPath(import.meta.url));
2145
- var VERSION = JSON.parse(fs2.readFileSync(path2.resolve(__cliDirname, "../package.json"), "utf-8")).version;
2898
+ var __cliDirname = path3.dirname(fileURLToPath(import.meta.url));
2899
+ var VERSION = JSON.parse(
2900
+ fs3.readFileSync(path3.resolve(__cliDirname, "../package.json"), "utf-8")
2901
+ ).version;
2146
2902
  function getLanAddress() {
2147
2903
  const interfaces = os.networkInterfaces();
2148
2904
  for (const name of Object.keys(interfaces)) {
@@ -2246,9 +3002,9 @@ async function startDashboard() {
2246
3002
  if (!config.somaVault && process.env.SOMA_VAULT) {
2247
3003
  config.somaVault = process.env.SOMA_VAULT;
2248
3004
  }
2249
- const tracesPath = path2.resolve(config.tracesDir);
2250
- if (!fs2.existsSync(tracesPath)) {
2251
- fs2.mkdirSync(tracesPath, { recursive: true });
3005
+ const tracesPath = path3.resolve(config.tracesDir);
3006
+ if (!fs3.existsSync(tracesPath)) {
3007
+ fs3.mkdirSync(tracesPath, { recursive: true });
2252
3008
  }
2253
3009
  config.tracesDir = tracesPath;
2254
3010
  console.log("\nStarting AgentFlow Dashboard...\n");
@@ -2320,13 +3076,77 @@ Examples:
2320
3076
 
2321
3077
  // src/server.ts
2322
3078
  var __filename = fileURLToPath2(import.meta.url);
2323
- var __dirname = path3.dirname(__filename);
3079
+ var __dirname = path4.dirname(__filename);
3080
+ function safePath(segment) {
3081
+ return path4.basename(segment.replace(/\.\./g, ""));
3082
+ }
3083
+ function parseVaultFrontmatter(content) {
3084
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
3085
+ if (!fmMatch) return null;
3086
+ const fm = {};
3087
+ const lines = fmMatch[1].split("\n");
3088
+ let currentKey = "";
3089
+ let collectingList = null;
3090
+ for (const line of lines) {
3091
+ if (line.match(/^\s*-\s/) && currentKey) {
3092
+ if (!collectingList) collectingList = [];
3093
+ const val2 = line.replace(/^\s*-\s*/, "").trim().replace(/^["']|["']$/g, "");
3094
+ collectingList.push(val2);
3095
+ continue;
3096
+ }
3097
+ if (collectingList && currentKey) {
3098
+ fm[currentKey] = collectingList;
3099
+ collectingList = null;
3100
+ }
3101
+ const colonIdx = line.indexOf(":");
3102
+ if (colonIdx === -1 || line.startsWith(" ")) continue;
3103
+ currentKey = line.slice(0, colonIdx).trim();
3104
+ let val = line.slice(colonIdx + 1).trim();
3105
+ if (val === "") continue;
3106
+ if (val.startsWith("[") && val.endsWith("]")) {
3107
+ try {
3108
+ fm[currentKey] = JSON.parse(val);
3109
+ } catch {
3110
+ fm[currentKey] = val;
3111
+ }
3112
+ currentKey = "";
3113
+ continue;
3114
+ }
3115
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
3116
+ val = val.slice(1, -1);
3117
+ }
3118
+ if (val === "true") fm[currentKey] = true;
3119
+ else if (val === "false") fm[currentKey] = false;
3120
+ else if (/^\d+(\.\d+)?$/.test(val)) fm[currentKey] = Number(val);
3121
+ else fm[currentKey] = val;
3122
+ currentKey = "";
3123
+ }
3124
+ if (collectingList && currentKey) {
3125
+ fm[currentKey] = collectingList;
3126
+ }
3127
+ for (const key of ["tags", "related", "evidence", "evidence_links", "sourceIds"]) {
3128
+ if (fm[key] && !Array.isArray(fm[key])) {
3129
+ const str = String(fm[key]);
3130
+ if (str.startsWith("[")) {
3131
+ try {
3132
+ fm[key] = JSON.parse(str);
3133
+ } catch {
3134
+ fm[key] = [str];
3135
+ }
3136
+ } else {
3137
+ fm[key] = [str];
3138
+ }
3139
+ }
3140
+ if (!fm[key]) fm[key] = [];
3141
+ }
3142
+ return fm;
3143
+ }
2324
3144
  function serializeTrace(trace) {
2325
3145
  if (!trace) return trace;
2326
3146
  const obj = { ...trace };
2327
- if (obj.nodes instanceof Map) {
3147
+ if (trace.nodes instanceof Map) {
2328
3148
  const nodesObj = {};
2329
- for (const [key, value] of obj.nodes) {
3149
+ for (const [key, value] of trace.nodes) {
2330
3150
  nodesObj[key] = value;
2331
3151
  }
2332
3152
  obj.nodes = nodesObj;
@@ -2340,11 +3160,11 @@ var DashboardServer = class {
2340
3160
  this.userConfig = userCfg;
2341
3161
  this.configPath = cfgPath;
2342
3162
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
2343
- const dashConfigPath = path3.join(home, ".agentflow/dashboard-config.json");
3163
+ const dashConfigPath = path4.join(home, ".agentflow/dashboard-config.json");
2344
3164
  if (!config.dataDirs) config.dataDirs = [];
2345
3165
  try {
2346
- if (fs3.existsSync(dashConfigPath)) {
2347
- const saved = JSON.parse(fs3.readFileSync(dashConfigPath, "utf-8"));
3166
+ if (fs4.existsSync(dashConfigPath)) {
3167
+ const saved = JSON.parse(fs4.readFileSync(dashConfigPath, "utf-8"));
2348
3168
  const extraDirs = saved.extraDirs ?? [];
2349
3169
  for (const d of extraDirs) {
2350
3170
  if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
@@ -2353,7 +3173,7 @@ var DashboardServer = class {
2353
3173
  } catch {
2354
3174
  }
2355
3175
  for (const p of getDiscoveryPaths(this.userConfig)) {
2356
- if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
3176
+ if (fs4.existsSync(p) && !config.dataDirs.includes(p)) {
2357
3177
  config.dataDirs.push(p);
2358
3178
  }
2359
3179
  }
@@ -2364,8 +3184,9 @@ var DashboardServer = class {
2364
3184
  });
2365
3185
  this.stats = new AgentStats();
2366
3186
  this.knowledgeStore = createKnowledgeStore({
2367
- baseDir: path3.join(config.tracesDir, "..", ".agentflow", "knowledge")
3187
+ baseDir: path4.join(config.tracesDir, "..", ".agentflow", "knowledge")
2368
3188
  });
3189
+ this.commandExecutor = createCommandExecutor(this.userConfig);
2369
3190
  this.setupExpress();
2370
3191
  this.setupWebSocket();
2371
3192
  this.setupTraceWatcher();
@@ -2396,9 +3217,21 @@ var DashboardServer = class {
2396
3217
  ts: 0
2397
3218
  };
2398
3219
  knowledgeStore;
3220
+ commandExecutor;
2399
3221
  userConfig;
2400
3222
  configPath;
2401
3223
  setupExpress() {
3224
+ this.app.use(
3225
+ "/api/",
3226
+ rateLimit({
3227
+ windowMs: 60 * 1e3,
3228
+ // 1 minute
3229
+ max: 300,
3230
+ // 300 requests per minute per IP
3231
+ standardHeaders: true,
3232
+ legacyHeaders: false
3233
+ })
3234
+ );
2402
3235
  if (this.config.enableCors) {
2403
3236
  this.app.use((_req, res, next) => {
2404
3237
  res.header("Access-Control-Allow-Origin", "*");
@@ -2409,11 +3242,11 @@ var DashboardServer = class {
2409
3242
  next();
2410
3243
  });
2411
3244
  }
2412
- const pkgDir = path3.join(__dirname, "..");
2413
- const clientDir = path3.join(pkgDir, "dist/client");
2414
- const clientIndex = path3.join(clientDir, "index.html");
2415
- const srcDir = path3.join(pkgDir, "src/client");
2416
- const needsBuild = !fs3.existsSync(clientIndex) || fs3.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
3245
+ const pkgDir = path4.join(__dirname, "..");
3246
+ const clientDir = path4.join(pkgDir, "dist/client");
3247
+ const clientIndex = path4.join(clientDir, "index.html");
3248
+ const srcDir = path4.join(pkgDir, "src/client");
3249
+ const needsBuild = !fs4.existsSync(clientIndex) || fs4.existsSync(srcDir) && this.isClientStale(srcDir, clientDir);
2417
3250
  if (needsBuild) {
2418
3251
  try {
2419
3252
  console.log("Building dashboard client...");
@@ -2422,7 +3255,7 @@ var DashboardServer = class {
2422
3255
  console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
2423
3256
  }
2424
3257
  }
2425
- if (fs3.existsSync(clientDir)) {
3258
+ if (fs4.existsSync(clientDir)) {
2426
3259
  this.app.use(express.static(clientDir));
2427
3260
  }
2428
3261
  this.app.get("/api/traces", (req, res) => {
@@ -2469,6 +3302,20 @@ var DashboardServer = class {
2469
3302
  res.status(500).json({ error: "Failed to load trace events" });
2470
3303
  }
2471
3304
  });
3305
+ this.app.get("/api/traces/:filename/receipt", (req, res) => {
3306
+ try {
3307
+ const trace = this.watcher.getTrace(req.params.filename);
3308
+ if (!trace) {
3309
+ return res.status(404).json({ error: "Trace not found" });
3310
+ }
3311
+ const serialized = serializeTrace(trace);
3312
+ const graph = loadGraph2(serialized);
3313
+ const receipt = toReceipt(graph);
3314
+ res.json(receipt);
3315
+ } catch (_error) {
3316
+ res.status(500).json({ error: "Failed to generate receipt" });
3317
+ }
3318
+ });
2472
3319
  this.app.get("/api/agents", (req, res) => {
2473
3320
  try {
2474
3321
  const raw = this.stats.getAgentsList();
@@ -2591,19 +3438,37 @@ var DashboardServer = class {
2591
3438
  res.status(500).json({ error: "Failed to build process graph" });
2592
3439
  }
2593
3440
  });
2594
- this.app.get("/api/agents/:agentId/variants", (req, res) => {
3441
+ this.app.get("/api/agents/:agentId/variants", async (req, res) => {
2595
3442
  try {
2596
3443
  const agentId = req.params.agentId;
3444
+ const byModel = req.query.by === "model";
2597
3445
  const graphs = this.getGraphTraces(agentId);
2598
3446
  if (graphs.length === 0) {
2599
- return res.json({ agentId, totalTraces: 0, variants: [] });
3447
+ return res.json({ agentId, totalTraces: 0, variants: [], modelVariants: [] });
2600
3448
  }
2601
3449
  const variants = findVariants(graphs).map((v) => ({
2602
3450
  pathSignature: v.pathSignature,
2603
3451
  count: v.count,
2604
3452
  percentage: v.percentage
2605
3453
  }));
2606
- res.json({ agentId, totalTraces: graphs.length, variants });
3454
+ let modelVariants = [];
3455
+ if (byModel) {
3456
+ try {
3457
+ const { findVariantsWithModel } = await import(
3458
+ /* webpackIgnore: true */
3459
+ "./ops-intel-PL6GGIKL.js"
3460
+ );
3461
+ modelVariants = findVariantsWithModel(graphs, { includeModel: true }).map(
3462
+ (v) => ({
3463
+ pathSignature: v.pathSignature,
3464
+ count: v.count,
3465
+ percentage: v.percentage
3466
+ })
3467
+ );
3468
+ } catch {
3469
+ }
3470
+ }
3471
+ res.json({ agentId, totalTraces: graphs.length, variants, modelVariants });
2607
3472
  } catch (error) {
2608
3473
  console.error("Variants error:", error);
2609
3474
  res.status(500).json({ error: "Failed to compute variants" });
@@ -2628,6 +3493,147 @@ var DashboardServer = class {
2628
3493
  res.status(500).json({ error: "Failed to compute bottlenecks" });
2629
3494
  }
2630
3495
  });
3496
+ this.app.get("/api/agents/:agentId/health-briefing", async (req, res) => {
3497
+ const somaVault = this.config.somaVault;
3498
+ if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
3499
+ try {
3500
+ const agentId = req.params.agentId;
3501
+ const agentFile = path4.join(
3502
+ somaVault,
3503
+ "agent",
3504
+ `${safePath(agentId.replace(/:/g, "-"))}.md`
3505
+ );
3506
+ let agentData = {};
3507
+ if (fs4.existsSync(agentFile)) {
3508
+ agentData = parseVaultFrontmatter(fs4.readFileSync(agentFile, "utf-8")) ?? {};
3509
+ }
3510
+ const totalExecutions = Number(agentData.totalExecutions ?? 0);
3511
+ const failureRate = Number(agentData.failureRate ?? 0);
3512
+ const failureCount = Number(agentData.failureCount ?? 0);
3513
+ const status = failureRate > 0.5 ? "critical" : failureRate > 0.1 ? "degraded" : "healthy";
3514
+ const knowledgeTypes = ["decision", "insight", "constraint", "contradiction", "policy"];
3515
+ const intelligence = [];
3516
+ for (const kt of knowledgeTypes) {
3517
+ const dir = path4.join(somaVault, kt);
3518
+ if (!fs4.existsSync(dir)) continue;
3519
+ for (const f of fs4.readdirSync(dir)) {
3520
+ if (!f.endsWith(".md")) continue;
3521
+ try {
3522
+ const content = fs4.readFileSync(path4.join(dir, f), "utf-8");
3523
+ if (!content.includes(agentId) && !content.includes(agentId.replace(/:/g, "-")))
3524
+ continue;
3525
+ const parsed = parseVaultFrontmatter(content);
3526
+ if (!parsed) continue;
3527
+ intelligence.push({
3528
+ type: String(parsed.type ?? kt),
3529
+ name: String(parsed.name ?? f.replace(".md", "")),
3530
+ claim: String(parsed.claim ?? "").slice(0, 150),
3531
+ confidence: parsed.confidence
3532
+ });
3533
+ } catch {
3534
+ }
3535
+ }
3536
+ }
3537
+ const agentDir = path4.join(somaVault, "agent");
3538
+ const peers = [];
3539
+ if (fs4.existsSync(agentDir)) {
3540
+ for (const f of fs4.readdirSync(agentDir)) {
3541
+ if (!f.endsWith(".md")) continue;
3542
+ const p = parseVaultFrontmatter(fs4.readFileSync(path4.join(agentDir, f), "utf-8"));
3543
+ if (!p) continue;
3544
+ const runs = Number(p.totalExecutions ?? 0);
3545
+ if (runs > 0) {
3546
+ peers.push({
3547
+ name: String(p.name ?? p.agentId ?? f.replace(".md", "")),
3548
+ successRate: 1 - Number(p.failureRate ?? 0),
3549
+ runs
3550
+ });
3551
+ }
3552
+ }
3553
+ }
3554
+ peers.sort((a, b) => b.successRate - a.successRate);
3555
+ let drift = null;
3556
+ try {
3557
+ const historyPath = path4.join(somaVault, "..", "conformance-history.json");
3558
+ if (fs4.existsSync(historyPath)) {
3559
+ const history = JSON.parse(fs4.readFileSync(historyPath, "utf-8"));
3560
+ const agentHistory = history.filter((e) => e.agentId === agentId);
3561
+ if (agentHistory.length >= 10) {
3562
+ try {
3563
+ const { detectDrift: dd } = await import(
3564
+ /* webpackIgnore: true */
3565
+ "./ops-intel-PL6GGIKL.js"
3566
+ );
3567
+ drift = dd(agentHistory);
3568
+ } catch {
3569
+ drift = { status: "stable", dataPoints: agentHistory.length };
3570
+ }
3571
+ } else {
3572
+ drift = { status: "insufficient_data", dataPoints: agentHistory.length };
3573
+ }
3574
+ }
3575
+ } catch {
3576
+ }
3577
+ res.json({
3578
+ agentId,
3579
+ status,
3580
+ totalExecutions,
3581
+ failureRate,
3582
+ failureCount,
3583
+ intelligence: {
3584
+ total: intelligence.length,
3585
+ byType: Object.fromEntries(
3586
+ knowledgeTypes.map((t) => [t, intelligence.filter((i) => i.type === t)])
3587
+ )
3588
+ },
3589
+ peers,
3590
+ drift
3591
+ });
3592
+ } catch (error) {
3593
+ console.error("Health briefing error:", error);
3594
+ res.status(500).json({ error: "Failed to generate briefing" });
3595
+ }
3596
+ });
3597
+ this.app.get("/api/traces/:filename/decisions", (req, res) => {
3598
+ try {
3599
+ const trace = this.watcher.getTrace(req.params.filename);
3600
+ if (!trace) return res.status(404).json({ error: "Trace not found" });
3601
+ const serialized = serializeTrace(trace);
3602
+ const sessionEvents = trace.sessionEvents;
3603
+ let decisions = [];
3604
+ if (sessionEvents && sessionEvents.length > 0) {
3605
+ try {
3606
+ import("./ops-intel-PL6GGIKL.js").then(({ extractDecisionsFromSession, computePatternSignature }) => {
3607
+ decisions = extractDecisionsFromSession(sessionEvents);
3608
+ res.json({
3609
+ decisions,
3610
+ pattern: computePatternSignature(
3611
+ decisions
3612
+ )
3613
+ });
3614
+ }).catch(() => {
3615
+ res.json({ decisions: [], pattern: "" });
3616
+ });
3617
+ return;
3618
+ } catch {
3619
+ }
3620
+ }
3621
+ const graph = loadGraph2(serialized);
3622
+ const nodes = [...graph.nodes.values()];
3623
+ const toolNodes = nodes.filter((n) => n.type === "tool" || n.type === "action").sort((a, b) => (a.startTime ?? 0) - (b.startTime ?? 0));
3624
+ decisions = toolNodes.map((n, i) => ({
3625
+ action: n.name,
3626
+ tool: n.name,
3627
+ outcome: n.status === "failed" ? "failed" : "ok",
3628
+ durationMs: n.endTime != null ? n.endTime - n.startTime : void 0,
3629
+ index: i
3630
+ }));
3631
+ const pattern = decisions.map((d) => d.action).join("\u2192");
3632
+ res.json({ decisions, pattern });
3633
+ } catch {
3634
+ res.status(500).json({ error: "Failed to extract decisions" });
3635
+ }
3636
+ });
2631
3637
  this.app.get("/api/agents/:agentId/profile", (req, res) => {
2632
3638
  try {
2633
3639
  const profile = this.knowledgeStore.getAgentProfile(req.params.agentId);
@@ -2641,6 +3647,7 @@ var DashboardServer = class {
2641
3647
  }
2642
3648
  });
2643
3649
  this.app.get("/api/process-model/:agentId", (req, res) => {
3650
+ var _a, _b;
2644
3651
  try {
2645
3652
  const agentId = req.params.agentId;
2646
3653
  const allTraces = this.watcher.getTracesByAgent(agentId);
@@ -2658,8 +3665,8 @@ var DashboardServer = class {
2658
3665
  const nodeArr = Object.values(nodes);
2659
3666
  const sorted = nodeArr.filter((n) => n.name && typeof n.startTime === "number" && n.startTime > 0).sort((a, b) => (a.startTime ?? 0) - (b.startTime ?? 0));
2660
3667
  for (let i = 0; i < sorted.length - 1; i++) {
2661
- const from = sorted[i].name;
2662
- const to = sorted[i + 1].name;
3668
+ const from = ((_a = sorted[i]) == null ? void 0 : _a.name) ?? "";
3669
+ const to = ((_b = sorted[i + 1]) == null ? void 0 : _b.name) ?? "";
2663
3670
  const key = `${from}|||${to}`;
2664
3671
  transMap.set(key, (transMap.get(key) ?? 0) + 1);
2665
3672
  }
@@ -2682,7 +3689,7 @@ var DashboardServer = class {
2682
3689
  const model = {
2683
3690
  transitions: [...transMap.entries()].map(([key, count]) => {
2684
3691
  const [from, to] = key.split("|||");
2685
- return { from, to, count };
3692
+ return { from: from ?? "", to: to ?? "", count };
2686
3693
  }),
2687
3694
  nodeTypes: Object.fromEntries(nodeTypeMap)
2688
3695
  };
@@ -2735,11 +3742,11 @@ var DashboardServer = class {
2735
3742
  return res.json({ tier: "teaser", somaVault: false, governanceAvailable: false });
2736
3743
  }
2737
3744
  try {
2738
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
2739
- if (!fs3.existsSync(reportPath)) {
3745
+ const reportPath = path4.join(somaVault, "..", "soma-report.json");
3746
+ if (!fs4.existsSync(reportPath)) {
2740
3747
  return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
2741
3748
  }
2742
- const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
3749
+ const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
2743
3750
  const hasGovernance = report.governance && typeof report.governance.pending === "number";
2744
3751
  return res.json({
2745
3752
  tier: hasGovernance ? "pro" : "free",
@@ -2756,11 +3763,15 @@ var DashboardServer = class {
2756
3763
  return res.json({ available: false, teaser: true });
2757
3764
  }
2758
3765
  try {
2759
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
2760
- if (!fs3.existsSync(reportPath)) {
2761
- return res.json({ available: false, teaser: false, message: "No report file yet. Run soma watch." });
3766
+ const reportPath = path4.join(somaVault, "..", "soma-report.json");
3767
+ if (!fs4.existsSync(reportPath)) {
3768
+ return res.json({
3769
+ available: false,
3770
+ teaser: false,
3771
+ message: "No report file yet. Run soma watch."
3772
+ });
2762
3773
  }
2763
- const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
3774
+ const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
2764
3775
  res.json(report);
2765
3776
  } catch (error) {
2766
3777
  console.error("Soma report error:", error);
@@ -2773,17 +3784,21 @@ var DashboardServer = class {
2773
3784
  return res.json({ available: false });
2774
3785
  }
2775
3786
  try {
2776
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
2777
- if (!fs3.existsSync(reportPath)) {
3787
+ const reportPath = path4.join(somaVault, "..", "soma-report.json");
3788
+ if (!fs4.existsSync(reportPath)) {
2778
3789
  return res.json({ available: false, message: "No report file. Run soma report." });
2779
3790
  }
2780
- const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
3791
+ const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
2781
3792
  res.json({
2782
3793
  available: true,
2783
3794
  layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
2784
3795
  governance: report.governance ?? { pending: 0, promoted: 0, rejected: 0 },
2785
- insights: (report.insights ?? []).filter((i) => i.layer === "emerging" && i.proposal_status === "pending"),
2786
- canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
3796
+ insights: (report.insights ?? []).filter(
3797
+ (i) => i.layer === "emerging" && i.proposal_status === "pending"
3798
+ ),
3799
+ canon: (report.insights ?? []).filter(
3800
+ (i) => i.layer === "canon"
3801
+ ),
2787
3802
  generatedAt: report.generatedAt
2788
3803
  });
2789
3804
  } catch (error) {
@@ -2791,24 +3806,28 @@ var DashboardServer = class {
2791
3806
  res.status(500).json({ available: false, message: "Failed to read governance data" });
2792
3807
  }
2793
3808
  });
2794
- const sanitizeArg = (s) => s.replace(/[^a-zA-Z0-9_\-.:]/g, "");
2795
- const sanitizeReason = (s) => s.replace(/["`$\\]/g, "").slice(0, 500);
3809
+ const isValidId = (s) => /^[a-zA-Z0-9_\-.:]+$/.test(s);
2796
3810
  this.app.post("/api/soma/governance/promote", (req, res) => {
2797
3811
  var _a;
2798
3812
  const somaVault = this.config.somaVault;
2799
3813
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2800
3814
  const { entryId } = req.body ?? {};
2801
- if (!entryId) return res.status(400).json({ error: "entryId required" });
3815
+ if (!entryId || !isValidId(String(entryId)))
3816
+ return res.status(400).json({ error: "Invalid entryId" });
2802
3817
  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
- });
3818
+ const result = execFileSync(
3819
+ "npx",
3820
+ ["soma", "governance", "promote", String(entryId), "--vault", somaVault],
3821
+ {
3822
+ encoding: "utf-8",
3823
+ timeout: 1e4
3824
+ }
3825
+ );
2809
3826
  res.json({ success: true, message: result.trim() });
2810
3827
  } catch (error) {
2811
- res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
3828
+ res.status(400).json({
3829
+ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message
3830
+ });
2812
3831
  }
2813
3832
  });
2814
3833
  this.app.post("/api/soma/governance/reject", (req, res) => {
@@ -2816,43 +3835,62 @@ var DashboardServer = class {
2816
3835
  const somaVault = this.config.somaVault;
2817
3836
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2818
3837
  const { entryId, reason } = req.body ?? {};
2819
- if (!entryId || !reason) return res.status(400).json({ error: "entryId and reason required" });
3838
+ if (!entryId || !isValidId(String(entryId)))
3839
+ return res.status(400).json({ error: "Invalid entryId" });
3840
+ if (!reason || typeof reason !== "string")
3841
+ return res.status(400).json({ error: "reason required" });
2820
3842
  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
- });
3843
+ const result = execFileSync(
3844
+ "npx",
3845
+ [
3846
+ "soma",
3847
+ "governance",
3848
+ "reject",
3849
+ String(entryId),
3850
+ String(reason).slice(0, 500),
3851
+ "--vault",
3852
+ somaVault
3853
+ ],
3854
+ {
3855
+ encoding: "utf-8",
3856
+ timeout: 1e4
3857
+ }
3858
+ );
2828
3859
  res.json({ success: true, message: result.trim() });
2829
3860
  } catch (error) {
2830
- res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
3861
+ res.status(400).json({
3862
+ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message
3863
+ });
2831
3864
  }
2832
3865
  });
2833
3866
  this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
2834
3867
  var _a;
2835
3868
  const somaVault = this.config.somaVault;
2836
3869
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
3870
+ if (!isValidId(String(req.params.id))) return res.status(400).json({ error: "Invalid id" });
2837
3871
  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
- });
3872
+ const result = execFileSync(
3873
+ "npx",
3874
+ ["soma", "governance", "show", String(req.params.id), "--vault", somaVault],
3875
+ {
3876
+ encoding: "utf-8",
3877
+ timeout: 1e4
3878
+ }
3879
+ );
2844
3880
  res.json({ available: true, output: result.trim() });
2845
3881
  } catch (error) {
2846
- res.status(404).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
3882
+ res.status(404).json({
3883
+ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message
3884
+ });
2847
3885
  }
2848
3886
  });
2849
3887
  this.app.get("/api/soma/policies", (_req, res) => {
2850
3888
  const somaVault = this.config.somaVault;
2851
3889
  if (!somaVault) return res.json({ policies: [] });
2852
3890
  try {
2853
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
2854
- if (!fs3.existsSync(reportPath)) return res.json({ policies: [] });
2855
- const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
3891
+ const reportPath = path4.join(somaVault, "..", "soma-report.json");
3892
+ if (!fs4.existsSync(reportPath)) return res.json({ policies: [] });
3893
+ const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
2856
3894
  res.json({ policies: report.policies ?? [] });
2857
3895
  } catch {
2858
3896
  res.json({ policies: [] });
@@ -2863,57 +3901,102 @@ var DashboardServer = class {
2863
3901
  const somaVault = this.config.somaVault;
2864
3902
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
2865
3903
  const { name, enforcement, scope, conditions } = req.body ?? {};
2866
- if (!name) return res.status(400).json({ error: "name required" });
3904
+ if (!name || !isValidId(String(name)))
3905
+ return res.status(400).json({ error: "Invalid policy name" });
3906
+ const enf = String(enforcement || "warn");
3907
+ if (!isValidId(enf)) return res.status(400).json({ error: "Invalid enforcement value" });
2867
3908
  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
- );
3909
+ const args = [
3910
+ "soma",
3911
+ "policy",
3912
+ "create",
3913
+ String(name),
3914
+ "--enforcement",
3915
+ enf,
3916
+ "--vault",
3917
+ somaVault
3918
+ ];
3919
+ if (scope) args.push("--scope", String(scope).slice(0, 500));
3920
+ if (conditions) args.push("--conditions", String(conditions).slice(0, 500));
3921
+ const result = execFileSync("npx", args, { encoding: "utf-8", timeout: 1e4 });
2876
3922
  res.json({ success: true, message: result.trim() });
2877
3923
  } catch (error) {
2878
- res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
3924
+ res.status(400).json({
3925
+ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message
3926
+ });
2879
3927
  }
2880
3928
  });
2881
3929
  this.app.delete("/api/soma/policies/:name", (req, res) => {
2882
3930
  var _a;
2883
3931
  const somaVault = this.config.somaVault;
2884
3932
  if (!somaVault) return res.status(400).json({ error: "Soma vault not configured" });
3933
+ if (!isValidId(String(req.params.name)))
3934
+ return res.status(400).json({ error: "Invalid policy name" });
2885
3935
  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 }
3936
+ const result = execFileSync(
3937
+ "npx",
3938
+ ["soma", "policy", "delete", String(req.params.name), "--vault", somaVault],
3939
+ {
3940
+ encoding: "utf-8",
3941
+ timeout: 1e4
3942
+ }
2890
3943
  );
2891
3944
  res.json({ success: true, message: result.trim() });
2892
3945
  } catch (error) {
2893
- res.status(400).json({ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message });
3946
+ res.status(400).json({
3947
+ error: ((_a = error.stderr) == null ? void 0 : _a.trim()) || error.message
3948
+ });
2894
3949
  }
2895
3950
  });
2896
3951
  this.app.get("/api/soma/vault/entities", (req, res) => {
2897
3952
  const somaVault = this.config.somaVault;
2898
3953
  if (!somaVault) return res.json({ entities: [], total: 0 });
2899
3954
  try {
2900
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
2901
- if (!fs3.existsSync(reportPath)) return res.json({ entities: [], total: 0 });
2902
- const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2903
- let entities = [
2904
- ...(report.agents ?? []).map((a) => ({ ...a, type: "agent", id: a.name })),
2905
- ...(report.insights ?? []).map((i, idx) => {
2906
- var _a;
2907
- return { ...i, type: i.type || "insight", id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}` };
2908
- }),
2909
- ...(report.policies ?? []).map((p) => ({ ...p, type: "policy", id: p.name }))
3955
+ const entityTypes = [
3956
+ "agent",
3957
+ "decision",
3958
+ "insight",
3959
+ "constraint",
3960
+ "contradiction",
3961
+ "policy",
3962
+ "archetype"
2910
3963
  ];
2911
- const { type, layer, q, limit: limitStr, offset: offsetStr } = req.query;
3964
+ let entities = [];
3965
+ for (const entityType of entityTypes) {
3966
+ const dir = path4.join(somaVault, entityType);
3967
+ if (!fs4.existsSync(dir)) continue;
3968
+ for (const file of fs4.readdirSync(dir)) {
3969
+ if (!file.endsWith(".md")) continue;
3970
+ try {
3971
+ const content = fs4.readFileSync(path4.join(dir, file), "utf-8");
3972
+ const parsed = parseVaultFrontmatter(content);
3973
+ if (!parsed) continue;
3974
+ const body = content.slice(content.indexOf("---", 4) + 3).trim().slice(0, 500);
3975
+ entities.push({
3976
+ ...parsed,
3977
+ type: parsed.type || entityType,
3978
+ id: parsed.id || file.replace(".md", ""),
3979
+ name: parsed.name || file.replace(".md", ""),
3980
+ body
3981
+ });
3982
+ } catch {
3983
+ }
3984
+ }
3985
+ }
3986
+ const {
3987
+ type,
3988
+ layer,
3989
+ q,
3990
+ limit: limitStr,
3991
+ offset: offsetStr
3992
+ } = req.query;
2912
3993
  if (type) entities = entities.filter((e) => e.type === type);
2913
3994
  if (layer) entities = entities.filter((e) => e.layer === layer);
2914
3995
  if (q) {
2915
3996
  const lq = q.toLowerCase();
2916
- entities = entities.filter((e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq));
3997
+ entities = entities.filter(
3998
+ (e) => (e.name || e.title || "").toLowerCase().includes(lq) || (e.claim || e.body || "").toLowerCase().includes(lq)
3999
+ );
2917
4000
  }
2918
4001
  const total = entities.length;
2919
4002
  const offset = parseInt(offsetStr || "0", 10);
@@ -2929,35 +4012,414 @@ var DashboardServer = class {
2929
4012
  const somaVault = this.config.somaVault;
2930
4013
  if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
2931
4014
  try {
2932
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
2933
- const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2934
- const { type, id } = req.params;
2935
- let entity = null;
4015
+ const type = safePath(req.params.type);
4016
+ const id = safePath(req.params.id);
4017
+ const filePath = path4.join(somaVault, type, `${id}.md`);
4018
+ if (!path4.resolve(filePath).startsWith(path4.resolve(somaVault))) {
4019
+ return res.status(400).json({ error: "Invalid path" });
4020
+ }
4021
+ if (!fs4.existsSync(filePath)) return res.status(404).json({ error: "Entity not found" });
4022
+ const content = fs4.readFileSync(filePath, "utf-8");
4023
+ const fm = parseVaultFrontmatter(content);
4024
+ if (!fm) return res.status(404).json({ error: "Entity not found" });
4025
+ const body = content.slice(content.indexOf("---", 4) + 3).trim();
4026
+ const agentKnowledge = [];
2936
4027
  if (type === "agent") {
2937
- entity = (report.agents ?? []).find((a) => a.name === id);
2938
- } else if (type === "policy") {
2939
- entity = (report.policies ?? []).find((p) => p.name === id);
2940
- } else {
2941
- entity = (report.insights ?? []).find(
2942
- (i) => {
2943
- var _a;
2944
- return (((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || "") === id || i.title === id;
4028
+ const agentName = fm.name || fm.agentId || id;
4029
+ const knowledgeTypes = ["decision", "insight", "constraint", "contradiction", "policy"];
4030
+ for (const kt of knowledgeTypes) {
4031
+ const ktDir = path4.join(somaVault, kt);
4032
+ if (!fs4.existsSync(ktDir)) continue;
4033
+ for (const f of fs4.readdirSync(ktDir)) {
4034
+ if (!f.endsWith(".md")) continue;
4035
+ try {
4036
+ const c = fs4.readFileSync(path4.join(ktDir, f), "utf-8");
4037
+ if (!c.includes(String(agentName))) continue;
4038
+ const parsed = parseVaultFrontmatter(c);
4039
+ if (!parsed) continue;
4040
+ agentKnowledge.push({
4041
+ type: parsed.type || kt,
4042
+ id: parsed.id || f.replace(".md", ""),
4043
+ name: parsed.name || f.replace(".md", ""),
4044
+ claim: parsed.claim || "",
4045
+ confidence: parsed.confidence || "",
4046
+ layer: parsed.layer || ""
4047
+ });
4048
+ } catch {
4049
+ }
2945
4050
  }
2946
- );
4051
+ }
2947
4052
  }
2948
- if (!entity) return res.status(404).json({ error: "Entity not found" });
2949
4053
  res.json({
2950
- ...entity,
4054
+ ...fm,
2951
4055
  type,
2952
4056
  id,
2953
- body: entity.claim || entity.conditions || "",
2954
- tags: entity.tags ?? [],
2955
- related: entity.related ?? []
4057
+ name: fm.name || id,
4058
+ body: type === "agent" && !body ? `Agent with ${fm.totalExecutions ?? 0} executions, ${((1 - Number(fm.failureRate || 0)) * 100).toFixed(1)}% success rate.` : body,
4059
+ knowledge: agentKnowledge
2956
4060
  });
2957
4061
  } catch {
2958
4062
  res.status(404).json({ error: "Entity not found" });
2959
4063
  }
2960
4064
  });
4065
+ this.app.get("/api/aicp/preflight", async (req, res) => {
4066
+ const agentId = req.query.agentId;
4067
+ if (!agentId) {
4068
+ return res.status(400).json({ error: "agentId query parameter required" });
4069
+ }
4070
+ const somaVault = this.config.somaVault;
4071
+ if (!somaVault) {
4072
+ return res.json({
4073
+ proceed: true,
4074
+ warnings: [],
4075
+ recommendations: [],
4076
+ available: false,
4077
+ _meta: { durationMs: 0 }
4078
+ });
4079
+ }
4080
+ try {
4081
+ const { evaluatePreflight } = await import(
4082
+ /* webpackIgnore: true */
4083
+ "./dist-H4QMTTY3.js"
4084
+ );
4085
+ const { createVault } = await import(
4086
+ /* webpackIgnore: true */
4087
+ "./dist-H4QMTTY3.js"
4088
+ );
4089
+ const vault = createVault({ baseDir: somaVault });
4090
+ const result = evaluatePreflight(vault, safePath(agentId));
4091
+ res.json(result);
4092
+ } catch {
4093
+ res.json({
4094
+ proceed: true,
4095
+ warnings: [],
4096
+ recommendations: [],
4097
+ available: false,
4098
+ _meta: { durationMs: 0 }
4099
+ });
4100
+ }
4101
+ });
4102
+ this.app.get("/api/soma/efficiency", async (_req, res) => {
4103
+ try {
4104
+ const allTraces = this.watcher.getAllTraces().map(serializeTrace);
4105
+ const graphs = [];
4106
+ for (const t of allTraces) {
4107
+ try {
4108
+ if (t.sourceType === "session" || t.sourceType === "log") continue;
4109
+ if (!t.rootNodeId && !t.rootId) continue;
4110
+ const nodes = t.nodes;
4111
+ if (!nodes || typeof nodes === "object" && Object.keys(nodes).length === 0) continue;
4112
+ graphs.push(loadGraph2(t));
4113
+ } catch {
4114
+ }
4115
+ }
4116
+ try {
4117
+ const { getEfficiency } = await import(
4118
+ /* webpackIgnore: true */
4119
+ "./ops-intel-PL6GGIKL.js"
4120
+ );
4121
+ const report2 = getEfficiency(graphs);
4122
+ return res.json(report2);
4123
+ } catch {
4124
+ }
4125
+ const somaVault = this.config.somaVault;
4126
+ if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
4127
+ const reportPath = path4.join(somaVault, "..", "soma-report.json");
4128
+ if (!fs4.existsSync(reportPath)) {
4129
+ return res.status(404).json({ error: "No SOMA report found" });
4130
+ }
4131
+ const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
4132
+ const agents = report.agents ?? [];
4133
+ const runs = agents.map((a) => ({
4134
+ graphId: a.name,
4135
+ agentId: a.name,
4136
+ totalTokenCost: a.totalTokenCost ?? 0,
4137
+ completedNodes: a.totalRuns ?? 0,
4138
+ costPerNode: (a.totalTokenCost ?? 0) / Math.max(1, a.totalRuns ?? 1)
4139
+ }));
4140
+ const costs = runs.map((r) => r.costPerNode).filter((c) => c > 0).sort((a, b) => a - b);
4141
+ const mean = costs.length > 0 ? costs.reduce((a, b) => a + b, 0) / costs.length : 0;
4142
+ const median = costs.length > 0 ? costs[Math.floor(costs.length / 2)] : 0;
4143
+ const p95 = costs.length > 0 ? costs[Math.min(costs.length - 1, Math.ceil(costs.length * 0.95) - 1)] : 0;
4144
+ res.json({
4145
+ runs,
4146
+ aggregate: { mean, median, p95 },
4147
+ flags: [],
4148
+ nodeCosts: [],
4149
+ dataCoverage: agents.length > 0 ? 1 : 0
4150
+ });
4151
+ } catch {
4152
+ res.status(500).json({ error: "Failed to compute efficiency" });
4153
+ }
4154
+ });
4155
+ this.app.get("/api/soma/drift", async (req, res) => {
4156
+ const agentId = req.query.agentId;
4157
+ if (!agentId) return res.status(400).json({ error: "agentId query parameter required" });
4158
+ try {
4159
+ const somaVault = this.config.somaVault;
4160
+ if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
4161
+ const historyPath = path4.join(somaVault, "..", "conformance-history.json");
4162
+ let history = [];
4163
+ if (fs4.existsSync(historyPath)) {
4164
+ history = JSON.parse(fs4.readFileSync(historyPath, "utf-8"));
4165
+ }
4166
+ const agentHistory = history.filter((e) => e.agentId === agentId);
4167
+ try {
4168
+ const { detectDrift } = await import(
4169
+ /* webpackIgnore: true */
4170
+ "./ops-intel-PL6GGIKL.js"
4171
+ );
4172
+ const driftReport = detectDrift(agentHistory);
4173
+ return res.json({ drift: driftReport, points: agentHistory });
4174
+ } catch {
4175
+ }
4176
+ const n = agentHistory.length;
4177
+ if (n < 10) {
4178
+ return res.json({
4179
+ drift: { status: "insufficient_data", slope: 0, r2: 0, windowSize: n, dataPoints: n },
4180
+ points: agentHistory
4181
+ });
4182
+ }
4183
+ let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
4184
+ for (let i = 0; i < n; i++) {
4185
+ const y = agentHistory[i].score;
4186
+ sumX += i;
4187
+ sumY += y;
4188
+ sumXY += i * y;
4189
+ sumX2 += i * i;
4190
+ }
4191
+ const denom = n * sumX2 - sumX * sumX;
4192
+ const slope = denom !== 0 ? (n * sumXY - sumX * sumY) / denom : 0;
4193
+ const intercept = (sumY - slope * sumX) / n;
4194
+ const meanY = sumY / n;
4195
+ let ssRes = 0, ssTot = 0;
4196
+ for (let i = 0; i < n; i++) {
4197
+ const y = agentHistory[i].score;
4198
+ ssRes += (y - (intercept + slope * i)) ** 2;
4199
+ ssTot += (y - meanY) ** 2;
4200
+ }
4201
+ const r2 = ssTot > 0 ? 1 - ssRes / ssTot : 0;
4202
+ const status = r2 > 0.3 ? slope < 0 ? "degrading" : "improving" : "stable";
4203
+ res.json({
4204
+ drift: { status, slope, r2, windowSize: n, dataPoints: n },
4205
+ points: agentHistory
4206
+ });
4207
+ } catch {
4208
+ res.status(404).json({ error: "Drift detection not available" });
4209
+ }
4210
+ });
4211
+ this.app.get("/api/soma/cross-agent", (_req, res) => {
4212
+ const somaVault = this.config.somaVault;
4213
+ if (!somaVault) return res.json({ insights: [], pairs: [] });
4214
+ try {
4215
+ const insightDir = path4.join(somaVault, "insight");
4216
+ if (!fs4.existsSync(insightDir)) return res.json({ insights: [], pairs: [] });
4217
+ const crossAgent = [];
4218
+ for (const file of fs4.readdirSync(insightDir)) {
4219
+ if (!file.endsWith(".md")) continue;
4220
+ try {
4221
+ const content = fs4.readFileSync(path4.join(insightDir, file), "utf-8");
4222
+ const parsed = parseVaultFrontmatter(content);
4223
+ if (!parsed) continue;
4224
+ const sa = parsed.source_agents;
4225
+ if (!sa || !Array.isArray(sa) || sa.length < 2) continue;
4226
+ crossAgent.push({
4227
+ name: String(parsed.name ?? file.replace(".md", "")),
4228
+ claim: String(parsed.claim ?? "").slice(0, 200),
4229
+ sourceAgents: sa,
4230
+ tags: parsed.tags ?? []
4231
+ });
4232
+ } catch {
4233
+ }
4234
+ }
4235
+ const pairMap = /* @__PURE__ */ new Map();
4236
+ for (const insight of crossAgent) {
4237
+ const key = [...insight.sourceAgents].sort().join(" \u2194 ");
4238
+ if (!pairMap.has(key)) pairMap.set(key, []);
4239
+ pairMap.get(key).push(insight);
4240
+ }
4241
+ const pairs = [...pairMap.entries()].map(([agents, insights]) => ({
4242
+ agents,
4243
+ count: insights.length,
4244
+ insights: insights.slice(0, 5)
4245
+ }));
4246
+ res.json({ total: crossAgent.length, pairs });
4247
+ } catch {
4248
+ res.json({ insights: [], pairs: [] });
4249
+ }
4250
+ });
4251
+ this.app.get("/api/external/commands", (_req, res) => {
4252
+ try {
4253
+ const { commands, errors } = getValidatedExternalCommands(this.userConfig);
4254
+ const commandList = Object.entries(commands).map(([id, command]) => ({
4255
+ id,
4256
+ name: command.name,
4257
+ description: command.description,
4258
+ category: command.category,
4259
+ allowConcurrent: command.allowConcurrent,
4260
+ timeout: command.timeout
4261
+ }));
4262
+ res.json({
4263
+ commands: commandList,
4264
+ configErrors: errors,
4265
+ total: commandList.length
4266
+ });
4267
+ } catch (error) {
4268
+ res.status(500).json({
4269
+ error: "Failed to load external commands",
4270
+ message: error.message
4271
+ });
4272
+ }
4273
+ });
4274
+ this.app.post("/api/external/commands/:commandId/execute", express.json(), async (req, res) => {
4275
+ try {
4276
+ const { commandId } = req.params;
4277
+ const { additionalArgs, timeout, context } = req.body;
4278
+ if (!isValidId(commandId)) {
4279
+ return res.status(400).json({ error: "Invalid command ID" });
4280
+ }
4281
+ const executionResult = await this.commandExecutor.executeCommand({
4282
+ commandId,
4283
+ additionalArgs: Array.isArray(additionalArgs) ? additionalArgs : void 0,
4284
+ timeout: typeof timeout === "number" ? timeout : void 0,
4285
+ context: context && typeof context === "object" ? context : void 0
4286
+ });
4287
+ res.json({
4288
+ executionId: executionResult.executionId,
4289
+ started: executionResult.started,
4290
+ status: executionResult.status,
4291
+ commandName: executionResult.command.name,
4292
+ pid: executionResult.pid,
4293
+ startedAt: executionResult.startedAt,
4294
+ error: executionResult.error
4295
+ });
4296
+ } catch (error) {
4297
+ res.status(500).json({
4298
+ error: "Command execution failed",
4299
+ message: error.message
4300
+ });
4301
+ }
4302
+ });
4303
+ this.app.get("/api/external/executions/:executionId", (req, res) => {
4304
+ try {
4305
+ const { executionId } = req.params;
4306
+ if (!isValidId(executionId)) {
4307
+ return res.status(400).json({ error: "Invalid execution ID" });
4308
+ }
4309
+ const execution = this.commandExecutor.getExecution(executionId);
4310
+ if (!execution) {
4311
+ return res.status(404).json({ error: "Execution not found" });
4312
+ }
4313
+ res.json({
4314
+ executionId: execution.executionId,
4315
+ started: execution.started,
4316
+ status: execution.status,
4317
+ commandName: execution.command.name,
4318
+ pid: execution.pid,
4319
+ startedAt: execution.startedAt,
4320
+ completedAt: execution.completedAt,
4321
+ duration: execution.duration,
4322
+ exitCode: execution.exitCode,
4323
+ hasOutput: execution.stdout.length > 0,
4324
+ hasError: execution.stderr.length > 0,
4325
+ // Limit output size for API response
4326
+ stdout: execution.stdout.slice(-2e3),
4327
+ // Last 2KB
4328
+ stderr: execution.stderr.slice(-1e3),
4329
+ // Last 1KB
4330
+ error: execution.error
4331
+ });
4332
+ } catch (error) {
4333
+ res.status(500).json({
4334
+ error: "Failed to get execution status",
4335
+ message: error.message
4336
+ });
4337
+ }
4338
+ });
4339
+ this.app.post("/api/external/executions/:executionId/kill", (req, res) => {
4340
+ try {
4341
+ const { executionId } = req.params;
4342
+ if (!isValidId(executionId)) {
4343
+ return res.status(400).json({ error: "Invalid execution ID" });
4344
+ }
4345
+ const killed = this.commandExecutor.killExecution(executionId, "manual");
4346
+ if (!killed) {
4347
+ return res.status(404).json({ error: "Execution not found or not running" });
4348
+ }
4349
+ res.json({ success: true, message: "Execution killed" });
4350
+ } catch (error) {
4351
+ res.status(500).json({
4352
+ error: "Failed to kill execution",
4353
+ message: error.message
4354
+ });
4355
+ }
4356
+ });
4357
+ this.app.get("/api/external/executions", (req, res) => {
4358
+ try {
4359
+ const limit = Math.min(parseInt(String(req.query.limit)) || 50, 100);
4360
+ const status = req.query.status;
4361
+ let executions = this.commandExecutor.getAllExecutions(limit);
4362
+ if (status && ["running", "completed", "failed", "timeout", "killed"].includes(status)) {
4363
+ executions = executions.filter((exec) => exec.status === status);
4364
+ }
4365
+ const executionSummaries = executions.map((exec) => ({
4366
+ executionId: exec.executionId,
4367
+ commandName: exec.command.name,
4368
+ status: exec.status,
4369
+ startedAt: exec.startedAt,
4370
+ completedAt: exec.completedAt,
4371
+ duration: exec.duration,
4372
+ exitCode: exec.exitCode,
4373
+ hasOutput: exec.stdout.length > 0,
4374
+ hasError: exec.stderr.length > 0,
4375
+ error: exec.error
4376
+ }));
4377
+ res.json({
4378
+ executions: executionSummaries,
4379
+ total: executionSummaries.length,
4380
+ running: this.commandExecutor.getRunningExecutions().length
4381
+ });
4382
+ } catch (error) {
4383
+ res.status(500).json({
4384
+ error: "Failed to get executions",
4385
+ message: error.message
4386
+ });
4387
+ }
4388
+ });
4389
+ this.app.get("/api/external/executions/:executionId/audit", (req, res) => {
4390
+ try {
4391
+ const { executionId } = req.params;
4392
+ if (!isValidId(executionId)) {
4393
+ return res.status(400).json({ error: "Invalid execution ID" });
4394
+ }
4395
+ const auditTrail = this.commandExecutor.getAuditTrail(executionId);
4396
+ res.json({
4397
+ executionId,
4398
+ auditEntries: auditTrail,
4399
+ total: auditTrail.length
4400
+ });
4401
+ } catch (error) {
4402
+ res.status(500).json({
4403
+ error: "Failed to get audit trail",
4404
+ message: error.message
4405
+ });
4406
+ }
4407
+ });
4408
+ this.app.get("/api/external/audit/stats", (req, res) => {
4409
+ try {
4410
+ const days = Math.min(parseInt(String(req.query.days)) || 7, 30);
4411
+ const stats = this.commandExecutor.getAuditStats(days);
4412
+ res.json({
4413
+ period: `${days} days`,
4414
+ ...stats
4415
+ });
4416
+ } catch (error) {
4417
+ res.status(500).json({
4418
+ error: "Failed to get audit statistics",
4419
+ message: error.message
4420
+ });
4421
+ }
4422
+ });
2961
4423
  this.app.get("/api/process-health", (_req, res) => {
2962
4424
  var _a, _b;
2963
4425
  try {
@@ -2967,7 +4429,7 @@ var DashboardServer = class {
2967
4429
  }
2968
4430
  const discoveryDirs = [
2969
4431
  this.config.tracesDir,
2970
- path3.dirname(this.config.tracesDir),
4432
+ path4.dirname(this.config.tracesDir),
2971
4433
  ...this.config.dataDirs || []
2972
4434
  ];
2973
4435
  let configs = discoverAllProcessConfigs(discoveryDirs);
@@ -3004,9 +4466,7 @@ var DashboardServer = class {
3004
4466
  const orphans = uniqueProcesses.filter(
3005
4467
  (p) => !allKnownPids.has(p.pid) && p.pid !== process.pid && p.pid !== process.ppid
3006
4468
  );
3007
- const problems = services.flatMap(
3008
- (s) => s.audit.problems.map((p) => `[${s.name}] ${p}`)
3009
- );
4469
+ const problems = services.flatMap((s) => s.audit.problems.map((p) => `[${s.name}] ${p}`));
3010
4470
  const result = {
3011
4471
  // Backward-compatible fields from primary service
3012
4472
  pidFile: (primary == null ? void 0 : primary.audit.pidFile) ?? null,
@@ -3032,7 +4492,7 @@ var DashboardServer = class {
3032
4492
  // Topology edges: parent-child relationships from process ppid
3033
4493
  topology: uniqueProcesses.map((p) => {
3034
4494
  try {
3035
- const statusContent = fs3.readFileSync(`/proc/${p.pid}/status`, "utf8");
4495
+ const statusContent = fs4.readFileSync(`/proc/${p.pid}/status`, "utf8");
3036
4496
  const ppidMatch = statusContent.match(/^PPid:\s+(\d+)/m);
3037
4497
  const ppid = ppidMatch ? parseInt(ppidMatch[1] ?? "0", 10) : 0;
3038
4498
  if (ppid > 1 && allKnownPids.has(ppid)) {
@@ -3052,34 +4512,39 @@ var DashboardServer = class {
3052
4512
  this.app.get("/api/directories", (_req, res) => {
3053
4513
  try {
3054
4514
  const home = process.env.HOME ?? "/home/trader";
3055
- const configPath = path3.join(home, ".agentflow/dashboard-config.json");
4515
+ const configPath = path4.join(home, ".agentflow/dashboard-config.json");
3056
4516
  let extraDirs = [];
3057
4517
  try {
3058
- if (fs3.existsSync(configPath)) {
3059
- const cfg = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
4518
+ if (fs4.existsSync(configPath)) {
4519
+ const cfg = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
3060
4520
  extraDirs = cfg.extraDirs ?? [];
3061
4521
  }
3062
4522
  } catch {
3063
4523
  }
3064
- const watched = [...new Set([
3065
- this.config.tracesDir,
3066
- ...this.config.dataDirs || [],
3067
- ...extraDirs
3068
- ].map((w) => path3.resolve(w)))];
4524
+ const watched = [
4525
+ ...new Set(
4526
+ [this.config.tracesDir, ...this.config.dataDirs || [], ...extraDirs].map(
4527
+ (w) => path4.resolve(w)
4528
+ )
4529
+ )
4530
+ ];
3069
4531
  const discovered = [];
3070
4532
  const svcNames = getSystemdServices(this.userConfig);
3071
4533
  if (svcNames.length > 0) {
3072
4534
  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 }
4535
+ const raw = execFileSync(
4536
+ "systemctl",
4537
+ ["--user", "show", "--property=ExecStart", "--no-pager", ...svcNames],
4538
+ {
4539
+ encoding: "utf8",
4540
+ timeout: 5e3
4541
+ }
3077
4542
  );
3078
4543
  for (const line of raw.split("\n")) {
3079
4544
  const match = line.match(/path=([^\s;]+)/);
3080
4545
  if (match == null ? void 0 : match[1]) {
3081
- const dir = path3.dirname(match[1]);
3082
- if (fs3.existsSync(dir)) discovered.push(dir);
4546
+ const dir = path4.dirname(match[1]);
4547
+ if (fs4.existsSync(dir)) discovered.push(dir);
3083
4548
  }
3084
4549
  }
3085
4550
  } catch {
@@ -3087,15 +4552,15 @@ var DashboardServer = class {
3087
4552
  }
3088
4553
  const commonPaths = [
3089
4554
  ...getDiscoveryPaths(this.userConfig),
3090
- path3.join(home, ".agentflow/traces")
4555
+ path4.join(home, ".agentflow/traces")
3091
4556
  ];
3092
4557
  for (const p of commonPaths) {
3093
- if (fs3.existsSync(p) && !discovered.includes(p)) {
4558
+ if (fs4.existsSync(p) && !discovered.includes(p)) {
3094
4559
  discovered.push(p);
3095
4560
  }
3096
4561
  }
3097
- const watchedSet = new Set(watched.map((w) => path3.resolve(w)));
3098
- const suggested = discovered.filter((d) => !watchedSet.has(path3.resolve(d)));
4562
+ const watchedSet = new Set(watched.map((w) => path4.resolve(w)));
4563
+ const suggested = discovered.filter((d) => !watchedSet.has(path4.resolve(d)));
3099
4564
  res.json({ watched, discovered, suggested });
3100
4565
  } catch (error) {
3101
4566
  console.error("Directory discovery error:", error);
@@ -3105,14 +4570,23 @@ var DashboardServer = class {
3105
4570
  this.app.post("/api/directories", express.json(), (req, res) => {
3106
4571
  try {
3107
4572
  const { add, remove } = req.body;
3108
- if (add && !fs3.existsSync(add)) {
3109
- return res.status(400).json({ error: `Directory does not exist: ${add}` });
4573
+ if (add) {
4574
+ const resolved = path4.resolve(add);
4575
+ if (resolved !== add || add.includes("..")) {
4576
+ return res.status(400).json({ error: "Invalid directory path" });
4577
+ }
4578
+ if (!fs4.existsSync(resolved)) {
4579
+ return res.status(400).json({ error: `Directory does not exist: ${add}` });
4580
+ }
3110
4581
  }
3111
- const configPath = path3.join(process.env.HOME ?? "/home/trader", ".agentflow/dashboard-config.json");
4582
+ const configPath = path4.join(
4583
+ process.env.HOME ?? "/home/trader",
4584
+ ".agentflow/dashboard-config.json"
4585
+ );
3112
4586
  let config = {};
3113
4587
  try {
3114
- if (fs3.existsSync(configPath)) {
3115
- config = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
4588
+ if (fs4.existsSync(configPath)) {
4589
+ config = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
3116
4590
  }
3117
4591
  } catch {
3118
4592
  }
@@ -3123,8 +4597,8 @@ var DashboardServer = class {
3123
4597
  if (remove) {
3124
4598
  config.extraDirs = config.extraDirs.filter((d) => d !== remove);
3125
4599
  }
3126
- fs3.mkdirSync(path3.dirname(configPath), { recursive: true });
3127
- fs3.writeFileSync(configPath, JSON.stringify(config, null, 2));
4600
+ fs4.mkdirSync(path4.dirname(configPath), { recursive: true });
4601
+ fs4.writeFileSync(configPath, JSON.stringify(config, null, 2));
3128
4602
  res.json({ ok: true, extraDirs: config.extraDirs });
3129
4603
  } catch (error) {
3130
4604
  console.error("Directory config error:", error);
@@ -3166,7 +4640,10 @@ var DashboardServer = class {
3166
4640
  lastModified: Date.now(),
3167
4641
  sourceDir: "http-collector"
3168
4642
  };
3169
- this.watcher.traces.set(`otel:${trace.id}`, watched);
4643
+ this.watcher.traces.set(
4644
+ `otel:${trace.id}`,
4645
+ watched
4646
+ );
3170
4647
  ingested++;
3171
4648
  }
3172
4649
  if (ingested > 0) {
@@ -3190,14 +4667,18 @@ var DashboardServer = class {
3190
4667
  this.app.get("/ready", (_req, res) => {
3191
4668
  res.json({ status: "ready" });
3192
4669
  });
3193
- this.app.get("*", (_req, res) => {
3194
- const clientIndex2 = path3.join(__dirname, "../dist/client/index.html");
3195
- if (fs3.existsSync(clientIndex2)) {
3196
- res.sendFile(clientIndex2);
3197
- } else {
3198
- res.status(404).send("Dashboard not found - public files may not be built");
4670
+ this.app.get(
4671
+ "*",
4672
+ rateLimit({ windowMs: 60 * 1e3, max: 600, standardHeaders: true, legacyHeaders: false }),
4673
+ (_req, res) => {
4674
+ const clientIndex2 = path4.join(__dirname, "../dist/client/index.html");
4675
+ if (fs4.existsSync(clientIndex2)) {
4676
+ res.sendFile(clientIndex2);
4677
+ } else {
4678
+ res.status(404).send("Dashboard not found - public files may not be built");
4679
+ }
3199
4680
  }
3200
- });
4681
+ );
3201
4682
  }
3202
4683
  setupWebSocket() {
3203
4684
  this.wss.on("connection", (ws) => {
@@ -3223,9 +4704,9 @@ var DashboardServer = class {
3223
4704
  setupSomaReportWatcher() {
3224
4705
  const somaVault = this.config.somaVault;
3225
4706
  if (!somaVault) return;
3226
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
3227
- const reportDir = path3.dirname(reportPath);
3228
- if (!fs3.existsSync(reportDir)) return;
4707
+ const reportPath = path4.join(somaVault, "..", "soma-report.json");
4708
+ const reportDir = path4.dirname(reportPath);
4709
+ if (!fs4.existsSync(reportDir)) return;
3229
4710
  let debounceTimer = null;
3230
4711
  const watcher = chokidar2.watch(reportPath, {
3231
4712
  ignoreInitial: true,
@@ -3237,7 +4718,7 @@ var DashboardServer = class {
3237
4718
  debounceTimer = setTimeout(() => {
3238
4719
  var _a, _b;
3239
4720
  try {
3240
- const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
4721
+ const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
3241
4722
  this.broadcast({ type: "soma-report-updated", data: report });
3242
4723
  if (report.generatedAt) {
3243
4724
  this.broadcast({
@@ -3268,7 +4749,10 @@ var DashboardServer = class {
3268
4749
  const nodes = trace.nodes;
3269
4750
  if (!nodes || typeof nodes === "object" && Object.keys(nodes).length === 0) continue;
3270
4751
  const nodeValues = Object.values(nodes);
3271
- if (nodeValues.some((n) => n.type === "log-file" || n.type === "log-entry")) continue;
4752
+ if (nodeValues.some(
4753
+ (n) => n.type === "log-file" || n.type === "log-entry"
4754
+ ))
4755
+ continue;
3272
4756
  graphs.push(loadGraph2(trace));
3273
4757
  } catch {
3274
4758
  }
@@ -3318,13 +4802,31 @@ var DashboardServer = class {
3318
4802
  isVirtual: false
3319
4803
  });
3320
4804
  }
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) {
4805
+ const _rootSteps = new Set(model.steps);
4806
+ const _childSteps = new Set(model.transitions.map((t) => t.to));
4807
+ const _leafSteps = new Set(model.steps);
4808
+ for (const _t of model.transitions) {
3325
4809
  }
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 });
4810
+ nodes.push({
4811
+ id: "[START]",
4812
+ label: "[START]",
4813
+ count: model.totalGraphs,
4814
+ frequency: 1,
4815
+ avgDuration: 0,
4816
+ failRate: 0,
4817
+ p95Duration: 0,
4818
+ isVirtual: true
4819
+ });
4820
+ nodes.push({
4821
+ id: "[END]",
4822
+ label: "[END]",
4823
+ count: model.totalGraphs,
4824
+ frequency: 1,
4825
+ avgDuration: 0,
4826
+ failRate: 0,
4827
+ p95Duration: 0,
4828
+ isVirtual: true
4829
+ });
3328
4830
  const edges = model.transitions.map((t) => ({
3329
4831
  source: t.from,
3330
4832
  target: t.to,
@@ -3388,7 +4890,7 @@ var DashboardServer = class {
3388
4890
  }
3389
4891
  const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
3390
4892
  for (let i = 0; i < seq.length; i++) {
3391
- const act = seq[i];
4893
+ const act = seq[i] ?? "";
3392
4894
  activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
3393
4895
  if (i < seq.length - 1) {
3394
4896
  const key = `${act} \u2192 ${seq[i + 1]}`;
@@ -3487,15 +4989,15 @@ var DashboardServer = class {
3487
4989
  /** Check if any src/client file is newer than the built bundle. */
3488
4990
  isClientStale(srcDir, distDir) {
3489
4991
  try {
3490
- const distIndex = path3.join(distDir, "index.html");
3491
- if (!fs3.existsSync(distIndex)) return true;
3492
- const distMtime = fs3.statSync(distIndex).mtimeMs;
4992
+ const distIndex = path4.join(distDir, "index.html");
4993
+ if (!fs4.existsSync(distIndex)) return true;
4994
+ const distMtime = fs4.statSync(distIndex).mtimeMs;
3493
4995
  const check = (dir) => {
3494
- for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
3495
- const full = path3.join(dir, entry.name);
4996
+ for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
4997
+ const full = path4.join(dir, entry.name);
3496
4998
  if (entry.isDirectory()) {
3497
4999
  if (check(full)) return true;
3498
- } else if (fs3.statSync(full).mtimeMs > distMtime) {
5000
+ } else if (fs4.statSync(full).mtimeMs > distMtime) {
3499
5001
  return true;
3500
5002
  }
3501
5003
  }
@@ -3521,6 +5023,9 @@ var DashboardServer = class {
3521
5023
  getStats() {
3522
5024
  return this.stats.getGlobalStats();
3523
5025
  }
5026
+ getTrace(filename) {
5027
+ return this.watcher.getTrace(filename);
5028
+ }
3524
5029
  getTraces() {
3525
5030
  return this.watcher.getAllTraces();
3526
5031
  }