agentflow-dashboard 0.8.4 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,12 @@
1
+ import {
2
+ __require
3
+ } from "./chunk-3RG5ZIWI.js";
4
+
1
5
  // src/server.ts
2
6
  import { execFileSync, execSync } from "child_process";
3
- import * as fs3 from "fs";
7
+ import * as fs4 from "fs";
4
8
  import { createServer } from "http";
5
- import * as path3 from "path";
9
+ import * as path4 from "path";
6
10
  import { fileURLToPath as fileURLToPath2 } from "url";
7
11
  import {
8
12
  auditProcesses,
@@ -12,7 +16,8 @@ import {
12
16
  discoverProcess,
13
17
  findVariants,
14
18
  getBottlenecks,
15
- loadGraph as loadGraph2
19
+ loadGraph as loadGraph2,
20
+ toReceipt
16
21
  } from "agentflow-core";
17
22
  import chokidar2 from "chokidar";
18
23
  import express from "express";
@@ -89,6 +94,70 @@ function getAgentDetection(config) {
89
94
  function getProcessPreference(config) {
90
95
  return config.processPreference ?? null;
91
96
  }
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
+ }
92
161
 
93
162
  // src/adapters/agentflow.ts
94
163
  var SKIP_FILES = /* @__PURE__ */ new Set([
@@ -167,6 +236,7 @@ var OpenClawAdapter = class {
167
236
  return filePath.includes("/cron/runs/") || filePath.includes("\\cron\\runs\\");
168
237
  }
169
238
  parse(filePath) {
239
+ var _a, _b, _c, _d, _e, _f, _g, _h;
170
240
  const traces = [];
171
241
  try {
172
242
  const content = readFileSync2(filePath, "utf-8");
@@ -186,34 +256,100 @@ var OpenClawAdapter = class {
186
256
  const jobName = (job == null ? void 0 : job.name) ?? jobId;
187
257
  const startTime = entry.runAtMs ?? entry.ts;
188
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
+ };
189
343
  const trace = {
190
344
  id: entry.sessionId ?? `${jobId}-${entry.ts}`,
191
345
  agentId: `openclaw:${jobId}`,
192
346
  name: jobName,
193
- status: entry.status === "ok" ? "completed" : entry.status === "error" ? "failed" : "unknown",
347
+ status: runStatus,
194
348
  startTime,
195
349
  endTime: startTime + duration,
196
350
  trigger: "cron",
197
351
  source: "openclaw",
198
- nodes: {
199
- root: {
200
- id: "root",
201
- type: "cron-job",
202
- name: jobName,
203
- status: entry.status === "ok" ? "completed" : entry.status === "error" ? "failed" : "unknown",
204
- startTime,
205
- endTime: startTime + duration,
206
- parentId: null,
207
- children: [],
208
- metadata: {
209
- jobId,
210
- summary: entry.summary,
211
- error: entry.error,
212
- delivered: entry.delivered,
213
- deliveryStatus: entry.deliveryStatus
214
- }
215
- }
216
- },
352
+ nodes,
217
353
  metadata: {
218
354
  model: entry.model,
219
355
  provider: entry.provider,
@@ -441,8 +577,9 @@ function deduplicateAgents(agents) {
441
577
  const mergedIds = /* @__PURE__ */ new Set();
442
578
  const mergedAgents = [];
443
579
  for (const [_key, group] of suffixGroups) {
444
- const suffix = extractSuffix((_a = group[0]) == null ? void 0 : _a.localId);
445
580
  if (group.length < 2) continue;
581
+ const suffix = extractSuffix(((_a = group[0]) == null ? void 0 : _a.localId) ?? "");
582
+ if (!suffix) continue;
446
583
  const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
447
584
  if (prefixes.size < 2) continue;
448
585
  const longPrefixes = [...prefixes].filter((p) => p !== suffix && p.length > 2);
@@ -519,13 +656,470 @@ function groupAgents(agents) {
519
656
  return { groups };
520
657
  }
521
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
+
522
1116
  // src/stats.ts
523
1117
  import { getFailures, getHungNodes, getStats } from "agentflow-core";
524
1118
  var AgentStats = class {
525
1119
  agentMetrics = /* @__PURE__ */ new Map();
526
1120
  processedTraces = /* @__PURE__ */ new Set();
527
1121
  processTrace(trace) {
528
- const traceKey = `${trace.filename || trace.agentId}-${trace.startTime}`;
1122
+ const traceKey = `${trace.agentId}#${trace.filename || trace.id}-${trace.startTime}`;
529
1123
  if (this.processedTraces.has(traceKey)) {
530
1124
  return;
531
1125
  }
@@ -696,8 +1290,8 @@ var AgentStats = class {
696
1290
 
697
1291
  // src/watcher.ts
698
1292
  import { EventEmitter } from "events";
699
- import * as fs from "fs";
700
- import * as path from "path";
1293
+ import * as fs2 from "fs";
1294
+ import * as path2 from "path";
701
1295
  import { loadGraph } from "agentflow-core";
702
1296
  import chokidar from "chokidar";
703
1297
 
@@ -745,13 +1339,19 @@ function extractKeyValuePairs(line) {
745
1339
  const kvRegex = /(\w+)=('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|\S+)/g;
746
1340
  let match;
747
1341
  while ((match = kvRegex.exec(clean)) !== null) {
748
- if (match[1] === "Z" || match[1] === "m") continue;
749
- 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);
750
1347
  }
751
1348
  return pairs;
752
1349
  }
753
1350
  function detectComponent(action, kvPairs) {
754
- if (action.includes(".")) return action.split(".")[0];
1351
+ if (action.includes(".")) {
1352
+ const parts = action.split(".");
1353
+ return parts[0] || action;
1354
+ }
755
1355
  if (kvPairs.component) return String(kvPairs.component);
756
1356
  if (kvPairs.service) return String(kvPairs.service);
757
1357
  if (kvPairs.module) return String(kvPairs.module);
@@ -789,7 +1389,9 @@ function detectActivityPattern(line) {
789
1389
  const pairs = {};
790
1390
  for (const m of kvMatches) {
791
1391
  const [key, value] = m.split("=", 2);
792
- pairs[key] = parseValue(value);
1392
+ if (key && value !== void 0) {
1393
+ pairs[key] = parseValue(value);
1394
+ }
793
1395
  }
794
1396
  timestamp = parseTimestamp(pairs.timestamp || pairs.time) || Date.now();
795
1397
  level = String(pairs.level || "info");
@@ -801,7 +1403,7 @@ function detectActivityPattern(line) {
801
1403
  const logMatch = line.match(
802
1404
  /^(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)\s+(\w+)?\s*:?\s*(.+)/
803
1405
  );
804
- if (logMatch) {
1406
+ if (logMatch && logMatch[1]) {
805
1407
  timestamp = new Date(logMatch[1]).getTime();
806
1408
  level = logMatch[2] || "info";
807
1409
  action = logMatch[3] || "";
@@ -839,11 +1441,38 @@ function getUniversalNodeStatus(activity) {
839
1441
  if (op.match(/complete|finish|end|done/i)) return "completed";
840
1442
  return "completed";
841
1443
  }
842
- function openClawSessionIdToAgent(sessionId) {
1444
+ function openClawSessionIdToAgent(sessionId, lookupMap) {
1445
+ if (lookupMap == null ? void 0 : lookupMap.has(sessionId)) {
1446
+ return lookupMap.get(sessionId);
1447
+ }
843
1448
  const firstSegment = sessionId.split("-")[0];
844
1449
  if (firstSegment) return firstSegment;
845
1450
  return "openclaw";
846
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
+ }
847
1476
 
848
1477
  // src/watcher.ts
849
1478
  var TraceWatcher = class _TraceWatcher extends EventEmitter {
@@ -854,19 +1483,21 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
854
1483
  allWatchDirs;
855
1484
  maxAgeMs;
856
1485
  userConfig;
1486
+ /** Maps OpenClaw session UUIDs to resolved agentIds (populated from sessions.json) */
1487
+ sessionAgentMap = /* @__PURE__ */ new Map();
857
1488
  constructor(tracesDirOrOptions) {
858
1489
  super();
859
1490
  const defaultMaxAgeMs = 48 * 60 * 60 * 1e3;
860
1491
  const envHours = process.env.AGENTFLOW_TRACE_WINDOW_HOURS;
861
1492
  const envMaxAgeMs = envHours ? parseFloat(envHours) * 60 * 60 * 1e3 : void 0;
862
1493
  if (typeof tracesDirOrOptions === "string") {
863
- this.tracesDir = path.resolve(tracesDirOrOptions);
1494
+ this.tracesDir = path2.resolve(tracesDirOrOptions);
864
1495
  this.dataDirs = [];
865
1496
  this.maxAgeMs = envMaxAgeMs ?? defaultMaxAgeMs;
866
1497
  this.userConfig = {};
867
1498
  } else {
868
- this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
869
- 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));
870
1501
  this.maxAgeMs = envMaxAgeMs ?? tracesDirOrOptions.maxAgeMs ?? defaultMaxAgeMs;
871
1502
  this.userConfig = tracesDirOrOptions.userConfig ?? {};
872
1503
  }
@@ -876,7 +1507,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
876
1507
  ]);
877
1508
  this.userSkipDirs = new Set(getSkipDirectories(this.userConfig));
878
1509
  this.allWatchDirs = [
879
- ...new Set([this.tracesDir, ...this.dataDirs].map((d) => path.resolve(d)))
1510
+ ...new Set([this.tracesDir, ...this.dataDirs].map((d) => path2.resolve(d)))
880
1511
  ];
881
1512
  this.ensureTracesDir();
882
1513
  this.loadExistingFiles();
@@ -889,7 +1520,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
889
1520
  const cutoff = Date.now() - this.maxAgeMs;
890
1521
  const _archived = 0;
891
1522
  for (const dir of this.allWatchDirs) {
892
- if (!fs.existsSync(dir)) continue;
1523
+ if (!fs2.existsSync(dir)) continue;
893
1524
  try {
894
1525
  this.archiveDirectory(dir, cutoff, 0);
895
1526
  } catch (error) {
@@ -899,28 +1530,28 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
899
1530
  }
900
1531
  archiveDirectory(dir, cutoff, depth) {
901
1532
  if (depth > 10) return 0;
902
- if (path.basename(dir) === "archive") return 0;
1533
+ if (path2.basename(dir) === "archive") return 0;
903
1534
  let archived = 0;
904
1535
  try {
905
- const entries = fs.readdirSync(dir, { withFileTypes: true });
1536
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
906
1537
  for (const entry of entries) {
907
1538
  if (entry.name.startsWith(".") || entry.name === "archive" || this.userSkipDirs.has(entry.name))
908
1539
  continue;
909
- const fullPath = path.join(dir, entry.name);
1540
+ const fullPath = path2.join(dir, entry.name);
910
1541
  if (entry.isDirectory()) {
911
1542
  archived += this.archiveDirectory(fullPath, cutoff, depth + 1);
912
1543
  continue;
913
1544
  }
914
1545
  if (!entry.isFile() || !this.isSupportedFile(entry.name)) continue;
915
1546
  try {
916
- const stats = fs.statSync(fullPath);
1547
+ const stats = fs2.statSync(fullPath);
917
1548
  if (stats.mtimeMs >= cutoff) continue;
918
1549
  const mtime = new Date(stats.mtimeMs);
919
1550
  const yearMonth = `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, "0")}`;
920
- const archiveDir = path.join(this.tracesDir, "archive", yearMonth);
921
- fs.mkdirSync(archiveDir, { recursive: true });
922
- const dest = path.join(archiveDir, entry.name);
923
- 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);
924
1555
  const key = this.traceKey(fullPath);
925
1556
  this.traces.delete(key);
926
1557
  archived++;
@@ -932,16 +1563,19 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
932
1563
  return archived;
933
1564
  }
934
1565
  ensureTracesDir() {
935
- if (!fs.existsSync(this.tracesDir)) {
936
- fs.mkdirSync(this.tracesDir, { recursive: true });
937
- 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 {
938
1572
  }
939
1573
  }
940
1574
  loadExistingFiles() {
941
1575
  let totalFiles = 0;
942
1576
  let totalDirectories = 0;
943
1577
  for (const dir of this.allWatchDirs) {
944
- if (!fs.existsSync(dir)) continue;
1578
+ if (!fs2.existsSync(dir)) continue;
945
1579
  try {
946
1580
  totalDirectories++;
947
1581
  const loadedFiles = this.scanDirectoryRecursive(dir);
@@ -959,16 +1593,16 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
959
1593
  if (depth > 10) return 0;
960
1594
  let fileCount = 0;
961
1595
  try {
962
- const entries = fs.readdirSync(dir, { withFileTypes: true });
1596
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
963
1597
  for (const entry of entries) {
964
1598
  if (entry.name.startsWith(".")) continue;
965
1599
  if (entry.name === "archive") continue;
966
1600
  if (this.userSkipDirs.has(entry.name)) continue;
967
- const fullPath = path.join(dir, entry.name);
1601
+ const fullPath = path2.join(dir, entry.name);
968
1602
  if (entry.isFile()) {
969
1603
  if (this.isSupportedFile(entry.name)) {
970
1604
  try {
971
- const mtime = fs.statSync(fullPath).mtimeMs;
1605
+ const mtime = fs2.statSync(fullPath).mtimeMs;
972
1606
  if (Date.now() - mtime > this.maxAgeMs) continue;
973
1607
  } catch {
974
1608
  continue;
@@ -1020,7 +1654,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1020
1654
  ];
1021
1655
  /** Load a file using the adapter registry, falling back to built-in parsing. */
1022
1656
  loadFile(filePath) {
1023
- const filename = path.basename(filePath);
1657
+ const filename = path2.basename(filePath);
1024
1658
  if (this.skipFiles.has(filename)) return false;
1025
1659
  if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
1026
1660
  const adapter = findAdapter(filePath);
@@ -1073,9 +1707,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1073
1707
  metadata: { ...trace.metadata, adapterSource: adapterName },
1074
1708
  sessionEvents: trace.sessionEvents ?? [],
1075
1709
  sourceType: "session",
1076
- filename: path.basename(filePath),
1710
+ filename: path2.basename(filePath),
1077
1711
  lastModified: Date.now(),
1078
- sourceDir: path.dirname(filePath)
1712
+ sourceDir: path2.dirname(filePath)
1079
1713
  };
1080
1714
  const key = `${adapterName}:${trace.id}`;
1081
1715
  this.traces.set(key, watched);
@@ -1088,9 +1722,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1088
1722
  }
1089
1723
  loadLogFile(filePath) {
1090
1724
  try {
1091
- const content = fs.readFileSync(filePath, "utf8");
1092
- const filename = path.basename(filePath);
1093
- const stats = fs.statSync(filePath);
1725
+ const content = fs2.readFileSync(filePath, "utf8");
1726
+ const filename = path2.basename(filePath);
1727
+ const stats = fs2.statSync(filePath);
1094
1728
  if (filename.startsWith("openclaw-") || filePath.includes("openclaw")) {
1095
1729
  const result = this.loadOpenClawLogFile(content, filename, filePath, stats);
1096
1730
  if (result) return true;
@@ -1108,8 +1742,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1108
1742
  trace.filename = filename;
1109
1743
  trace.lastModified = stats.mtime.getTime();
1110
1744
  trace.sourceType = trace.sourceType || "trace";
1111
- trace.sourceDir = path.dirname(filePath);
1112
- 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}`;
1113
1748
  this.traces.set(key, trace);
1114
1749
  }
1115
1750
  return traces.length > 0;
@@ -1170,7 +1805,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1170
1805
  trace.sourceType = "session";
1171
1806
  }
1172
1807
  if (traces.length === 0) {
1173
- const stats = fs.statSync(filePath);
1808
+ const stats = fs2.statSync(filePath);
1174
1809
  traces.push({
1175
1810
  id: "",
1176
1811
  rootNodeId: "root",
@@ -1210,10 +1845,10 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1210
1845
  const pathAgent = this.extractAgentFromPath(filePath);
1211
1846
  const detection = getAgentDetection(this.userConfig);
1212
1847
  if (detection.filePatterns) {
1213
- const basename3 = path.basename(filePath, path.extname(filePath));
1848
+ const basename4 = path2.basename(filePath, path2.extname(filePath));
1214
1849
  for (const [pattern, template] of Object.entries(detection.filePatterns)) {
1215
1850
  const re = new RegExp(`^(${pattern})$`);
1216
- const match = basename3.match(re);
1851
+ const match = basename4.match(re);
1217
1852
  if (match) {
1218
1853
  const resolved = template.replace("${match}", match[1]);
1219
1854
  return this.normaliseAgentId(resolved);
@@ -1223,8 +1858,8 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1223
1858
  return this.normaliseAgentId(pathAgent);
1224
1859
  }
1225
1860
  extractAgentFromPath(filePath) {
1226
- const filename = path.basename(filePath, path.extname(filePath));
1227
- const pathParts = filePath.split(path.sep);
1861
+ const filename = path2.basename(filePath, path2.extname(filePath));
1862
+ const pathParts = filePath.split(path2.sep);
1228
1863
  const detection = getAgentDetection(this.userConfig);
1229
1864
  let pathPrefix = "";
1230
1865
  if (detection.pathPatterns) {
@@ -1295,7 +1930,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1295
1930
  if ((inner == null ? void 0 : inner.payloads) && ((_a = inner == null ? void 0 : inner.meta) == null ? void 0 : _a.agentMeta)) {
1296
1931
  const agentMeta = inner.meta.agentMeta;
1297
1932
  const sessionId = agentMeta.sessionId || "unknown";
1298
- const agentName = openClawSessionIdToAgent(sessionId);
1933
+ const agentName = openClawSessionIdToAgent(sessionId, this.sessionAgentMap);
1299
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();
1300
1935
  const texts = (inner.payloads || []).map((p) => p.text || "").filter(Boolean);
1301
1936
  if (!sessions.has(sessionId)) {
@@ -1319,7 +1954,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1319
1954
  if (parsed.payloads && ((_d = parsed.meta) == null ? void 0 : _d.agentMeta)) {
1320
1955
  const agentMeta = parsed.meta.agentMeta;
1321
1956
  const sessionId = agentMeta.sessionId || "unknown";
1322
- const agentName = openClawSessionIdToAgent(sessionId);
1957
+ const agentName = openClawSessionIdToAgent(sessionId, this.sessionAgentMap);
1323
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();
1324
1959
  const texts = (parsed.payloads || []).map((p) => p.text || "").filter(Boolean);
1325
1960
  if (!sessions.has(sessionId)) {
@@ -1427,7 +2062,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1427
2062
  filename,
1428
2063
  lastModified: stats.mtime.getTime(),
1429
2064
  sourceType: "session",
1430
- sourceDir: path.dirname(filePath),
2065
+ sourceDir: path2.dirname(filePath),
1431
2066
  sessionEvents,
1432
2067
  tokenUsage: {
1433
2068
  input: totalInput,
@@ -1442,7 +2077,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1442
2077
  source: "openclaw-log"
1443
2078
  }
1444
2079
  };
1445
- 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}`;
1446
2081
  this.traces.set(key, trace);
1447
2082
  traceIndex++;
1448
2083
  }
@@ -1450,23 +2085,26 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1450
2085
  }
1451
2086
  loadTraceFile(filePath) {
1452
2087
  try {
1453
- const content = fs.readFileSync(filePath, "utf8");
1454
- const filename = path.basename(filePath);
2088
+ const content = fs2.readFileSync(filePath, "utf8");
2089
+ const filename = path2.basename(filePath);
1455
2090
  if (filename === "sessions.json") {
1456
2091
  return this.loadSessionsIndex(filePath, content);
1457
2092
  }
1458
2093
  const graph = loadGraph(content);
1459
- const stats = fs.statSync(filePath);
2094
+ const stats = fs2.statSync(filePath);
1460
2095
  graph.filename = filename;
1461
2096
  graph.lastModified = stats.mtime.getTime();
1462
2097
  graph.sourceType = "trace";
1463
- 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
+ }
1464
2102
  if (graph.nodes instanceof Map) {
1465
2103
  for (const node of graph.nodes.values()) {
1466
2104
  if (!node.children) node.children = [];
1467
2105
  }
1468
2106
  }
1469
- this.traces.set(this.traceKey(filePath), graph);
2107
+ this.traces.set(this.traceKey(filePath, graph.agentId), graph);
1470
2108
  return true;
1471
2109
  } catch {
1472
2110
  return false;
@@ -1474,11 +2112,12 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1474
2112
  }
1475
2113
  /** Parse sessions.json index to discover agents and their sessions. */
1476
2114
  loadSessionsIndex(filePath, content) {
2115
+ var _a;
1477
2116
  try {
1478
2117
  const data = JSON.parse(content);
1479
2118
  if (typeof data !== "object" || data === null) return false;
1480
- const stats = fs.statSync(filePath);
1481
- const pathParts = filePath.split(path.sep);
2119
+ const stats = fs2.statSync(filePath);
2120
+ const pathParts = filePath.split(path2.sep);
1482
2121
  const agentsIndex = pathParts.lastIndexOf("agents");
1483
2122
  if (agentsIndex === -1 || agentsIndex + 1 >= pathParts.length) return false;
1484
2123
  const agentName = pathParts[agentsIndex + 1];
@@ -1489,11 +2128,19 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1489
2128
  const session = sessionData;
1490
2129
  const sessionId = session.sessionId;
1491
2130
  if (!sessionId) continue;
2131
+ const resolvedAgentId = parseOpenClawSessionKey(sessionKey) ?? agentId;
2132
+ this.sessionAgentMap.set(String(sessionId), resolvedAgentId);
1492
2133
  const existingKey = Array.from(this.traces.keys()).find((k) => {
1493
2134
  const t = this.traces.get(k);
1494
2135
  return (t == null ? void 0 : t.id) === sessionId || (t == null ? void 0 : t.traceId) === sessionId;
1495
2136
  });
1496
- 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
+ }
1497
2144
  const updatedAt = session.updatedAt || stats.mtime.getTime();
1498
2145
  const label = session.label || sessionKey.split(":").pop() || sessionId;
1499
2146
  const chatType = session.chatType || (sessionKey.includes("cron") ? "cron" : "direct");
@@ -1522,7 +2169,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1522
2169
  edges: [],
1523
2170
  events: [],
1524
2171
  startTime: updatedAt,
1525
- agentId,
2172
+ agentId: resolvedAgentId,
1526
2173
  trigger,
1527
2174
  name: label,
1528
2175
  traceId: sessionId,
@@ -1530,7 +2177,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1530
2177
  filename: `${agentName}-${sessionId.slice(0, 8)}.index`,
1531
2178
  lastModified: updatedAt,
1532
2179
  sourceType: "session",
1533
- sourceDir: path.dirname(filePath),
2180
+ sourceDir: path2.dirname(filePath),
1534
2181
  sessionEvents: [],
1535
2182
  tokenUsage: { input: 0, output: 0, total: 0, cost: 0 },
1536
2183
  metadata: {
@@ -1540,7 +2187,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1540
2187
  agentName
1541
2188
  }
1542
2189
  };
1543
- const key = `${this.traceKey(filePath)}-${sessionId.slice(0, 12)}`;
2190
+ const key = `${this.traceKey(filePath, agentId)}-${sessionId.slice(0, 12)}`;
1544
2191
  this.traces.set(key, trace);
1545
2192
  loaded++;
1546
2193
  }
@@ -1553,7 +2200,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1553
2200
  loadSessionFile(filePath) {
1554
2201
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
1555
2202
  try {
1556
- const content = fs.readFileSync(filePath, "utf8");
2203
+ const content = fs2.readFileSync(filePath, "utf8");
1557
2204
  const lines = content.split("\n").filter((l) => l.trim());
1558
2205
  if (lines.length === 0) return false;
1559
2206
  const rawEvents = [];
@@ -1569,13 +2216,13 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1569
2216
  return this.loadCronRunFile(rawEvents, filePath);
1570
2217
  }
1571
2218
  const sessionEvent = rawEvents.find((e) => e.type === "session");
1572
- 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");
1573
2220
  const sessionTimestamp = (sessionEvent == null ? void 0 : sessionEvent.timestamp) || ((_a = rawEvents[0]) == null ? void 0 : _a.timestamp);
1574
2221
  const startTime = sessionTimestamp ? new Date(sessionTimestamp).getTime() : 0;
1575
2222
  if (!startTime) return false;
1576
- const parentDir = path.basename(path.dirname(filePath));
1577
- const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
1578
- 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))));
1579
2226
  let agentName;
1580
2227
  if (parentDir === "sessions" && greatGrandParentDir === "agents") {
1581
2228
  agentName = grandParentDir;
@@ -1594,6 +2241,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1594
2241
  }
1595
2242
  }
1596
2243
  }
2244
+ if (this.sessionAgentMap.has(sessionId)) {
2245
+ agentId = this.sessionAgentMap.get(sessionId);
2246
+ }
1597
2247
  const modelEvent = rawEvents.find((e) => e.type === "model_change");
1598
2248
  const provider = (modelEvent == null ? void 0 : modelEvent.provider) || "";
1599
2249
  const modelId = (modelEvent == null ? void 0 : modelEvent.modelId) || "";
@@ -1797,7 +2447,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1797
2447
  if (role === "toolResult") {
1798
2448
  const toolCallId = ((_j = contentBlocks[0]) == null ? void 0 : _j.toolCallId) || evt.parentId;
1799
2449
  const resultContent = contentBlocks.map((b) => b.text || b.content || "").join("\n");
1800
- const hasError = contentBlocks.some((b) => b.isError || b.error);
2450
+ const hasError = contentBlocks.some(
2451
+ (b) => b.isError || b.error
2452
+ );
1801
2453
  const errorText = hasError ? resultContent : void 0;
1802
2454
  sessionEvents.push({
1803
2455
  type: "tool_result",
@@ -1825,7 +2477,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1825
2477
  }
1826
2478
  }
1827
2479
  }
1828
- const fileStat = fs.statSync(filePath);
2480
+ const fileStat = fs2.statSync(filePath);
1829
2481
  const fileAge = Date.now() - fileStat.mtime.getTime();
1830
2482
  const lastEvt = rawEvents[rawEvents.length - 1];
1831
2483
  const hasToolError = sessionEvents.some((e) => e.type === "tool_result" && e.toolError);
@@ -1873,7 +2525,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1873
2525
  "gen_ai.request.model": modelId
1874
2526
  }
1875
2527
  });
1876
- const filename = path.basename(filePath);
2528
+ const filename = path2.basename(filePath);
1877
2529
  const trace = {
1878
2530
  id: sessionId,
1879
2531
  nodes,
@@ -1889,7 +2541,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1889
2541
  filename,
1890
2542
  lastModified: fileStat.mtime.getTime(),
1891
2543
  sourceType: "session",
1892
- sourceDir: path.dirname(filePath),
2544
+ sourceDir: path2.dirname(filePath),
1893
2545
  sessionEvents,
1894
2546
  tokenUsage,
1895
2547
  metadata: {
@@ -1903,93 +2555,180 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
1903
2555
  sessionVersion: sessionEvent == null ? void 0 : sessionEvent.version
1904
2556
  }
1905
2557
  };
1906
- this.traces.set(this.traceKey(filePath), trace);
2558
+ this.traces.set(this.traceKey(filePath, agentId), trace);
1907
2559
  return true;
1908
2560
  } catch {
1909
2561
  return false;
1910
2562
  }
1911
2563
  }
1912
- /** Parse cron run JSONL files (ts, jobId, action, status format). */
2564
+ /** Parse cron run JSONL files creates one trace per execution run. */
1913
2565
  loadCronRunFile(rawEvents, filePath) {
1914
- var _a, _b, _c;
2566
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1915
2567
  try {
1916
- const filename = path.basename(filePath);
1917
- const jobId = ((_a = rawEvents[0]) == null ? void 0 : _a.jobId) || path.basename(filePath, ".jsonl");
1918
- const fileStat = fs.statSync(filePath);
1919
- const sessionEvents = [];
1920
- 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;
1921
2573
  for (const evt of rawEvents) {
1922
2574
  const ts = evt.ts || Date.now();
1923
- const action = evt.action || "unknown";
1924
- const status = evt.status || "ok";
1925
- if (status !== "ok") lastStatus = "failed";
1926
- sessionEvents.push({
1927
- type: action === "finished" ? "assistant" : "system",
1928
- timestamp: ts,
1929
- name: `${jobId}: ${action}`,
1930
- content: evt.summary || evt.error || `${action} (${status})`,
1931
- 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
+ }
1932
2600
  });
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++;
1933
2704
  }
1934
- const firstTs = ((_b = rawEvents[0]) == null ? void 0 : _b.ts) || fileStat.mtime.getTime();
1935
- const lastTs = ((_c = rawEvents[rawEvents.length - 1]) == null ? void 0 : _c.ts) || fileStat.mtime.getTime();
1936
- const rootId = `cron-${jobId.slice(0, 12)}`;
1937
- const nodes = /* @__PURE__ */ new Map();
1938
- nodes.set(rootId, {
1939
- id: rootId,
1940
- type: "agent",
1941
- name: jobId,
1942
- startTime: firstTs,
1943
- endTime: lastTs,
1944
- status: lastStatus,
1945
- parentId: void 0,
1946
- children: [],
1947
- metadata: { jobId, runs: rawEvents.length }
1948
- });
1949
- const trace = {
1950
- id: jobId,
1951
- nodes,
1952
- edges: [],
1953
- events: [],
1954
- startTime: firstTs,
1955
- agentId: "openclaw-cron",
1956
- trigger: "cron",
1957
- name: jobId,
1958
- traceId: jobId,
1959
- spanId: jobId,
1960
- filename,
1961
- lastModified: fileStat.mtime.getTime(),
1962
- sourceType: "session",
1963
- sourceDir: path.dirname(filePath),
1964
- sessionEvents,
1965
- tokenUsage: { input: 0, output: 0, total: 0, cost: 0 },
1966
- metadata: { jobId, source: "cron-run" }
1967
- };
1968
- this.traces.set(this.traceKey(filePath), trace);
1969
- return true;
2705
+ return loaded > 0;
1970
2706
  } catch {
1971
2707
  return false;
1972
2708
  }
1973
2709
  }
1974
- /** Unique key for a file across directories. */
1975
- traceKey(filePath) {
2710
+ /** Unique key for a file across directories. Includes agentId to prevent collisions between agents. */
2711
+ traceKey(filePath, agentId) {
2712
+ let fileKey;
1976
2713
  for (const dir of this.allWatchDirs) {
1977
2714
  if (filePath.startsWith(dir)) {
1978
- const dirParts = dir.split(path.sep).filter(Boolean);
2715
+ const dirParts = dir.split(path2.sep).filter(Boolean);
1979
2716
  const dirSuffix = dirParts.slice(-2).join("/");
1980
- return `${path.relative(dir, filePath).replace(/\\/g, "/")}@${dirSuffix}`;
2717
+ fileKey = `${path2.relative(dir, filePath).replace(/\\/g, "/")}@${dirSuffix}`;
2718
+ return agentId ? `${fileKey}#${agentId}` : fileKey;
1981
2719
  }
1982
2720
  }
1983
- return filePath;
2721
+ fileKey = filePath;
2722
+ return agentId ? `${fileKey}#${agentId}` : fileKey;
1984
2723
  }
1985
2724
  startWatching() {
1986
2725
  for (const dir of this.allWatchDirs) {
1987
- if (!fs.existsSync(dir)) continue;
2726
+ if (!fs2.existsSync(dir)) continue;
1988
2727
  const patterns = [
1989
- path.join(dir, "**/*.json"),
1990
- path.join(dir, "**/*.jsonl"),
1991
- path.join(dir, "**/*.log"),
1992
- path.join(dir, "**/*.trace")
2728
+ path2.join(dir, "**/*.json"),
2729
+ path2.join(dir, "**/*.jsonl"),
2730
+ path2.join(dir, "**/*.log"),
2731
+ path2.join(dir, "**/*.trace")
1993
2732
  ];
1994
2733
  const watcher = chokidar.watch(patterns, {
1995
2734
  ignored: [
@@ -2015,9 +2754,9 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
2015
2754
  // Allow deep nesting for OpenClaw agents/*/sessions/
2016
2755
  });
2017
2756
  watcher.on("add", (filePath) => {
2018
- if (this.isSupportedFile(path.basename(filePath))) {
2019
- const relativePath = path.relative(dir, filePath);
2020
- 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)})`);
2021
2760
  if (this.loadFile(filePath)) {
2022
2761
  const key = this.traceKey(filePath);
2023
2762
  const trace = this.traces.get(key);
@@ -2028,7 +2767,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
2028
2767
  }
2029
2768
  });
2030
2769
  watcher.on("change", (filePath) => {
2031
- if (this.isSupportedFile(path.basename(filePath))) {
2770
+ if (this.isSupportedFile(path2.basename(filePath))) {
2032
2771
  if (this.loadFile(filePath)) {
2033
2772
  const key = this.traceKey(filePath);
2034
2773
  const trace = this.traces.get(key);
@@ -2039,7 +2778,7 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
2039
2778
  }
2040
2779
  });
2041
2780
  watcher.on("unlink", (filePath) => {
2042
- if (this.isSupportedFile(path.basename(filePath))) {
2781
+ if (this.isSupportedFile(path2.basename(filePath))) {
2043
2782
  const key = this.traceKey(filePath);
2044
2783
  this.traces.delete(key);
2045
2784
  this.emit("trace-removed", key);
@@ -2063,6 +2802,10 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
2063
2802
  let candidates = [];
2064
2803
  const exact = this.traces.get(filename);
2065
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
+ }
2066
2809
  if (filename.includes("::")) {
2067
2810
  const [fname, startTimeStr] = filename.split("::");
2068
2811
  const startTime = Number(startTimeStr);
@@ -2083,19 +2826,21 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
2083
2826
  candidates.push(trace);
2084
2827
  }
2085
2828
  }
2829
+ candidates = [...new Set(candidates)];
2086
2830
  if (candidates.length === 0) return void 0;
2087
2831
  if (candidates.length === 1) return candidates[0];
2088
2832
  if (agentId) {
2089
2833
  const agentMatches = candidates.filter((c) => c.agentId === agentId);
2090
- if (agentMatches.length > 0) {
2091
- candidates = agentMatches;
2092
- }
2834
+ if (agentMatches.length === 1) return agentMatches[0];
2835
+ if (agentMatches.length > 1) candidates = agentMatches;
2836
+ if (agentMatches.length === 0) return void 0;
2093
2837
  }
2094
- if (candidates.length === 1) return candidates[0];
2095
2838
  let best = candidates[0];
2839
+ if (!best) return void 0;
2096
2840
  let bestNodeCount = best.nodes instanceof Map ? best.nodes.size : Object.keys(best.nodes ?? {}).length;
2097
2841
  for (let i = 1; i < candidates.length; i++) {
2098
2842
  const c = candidates[i];
2843
+ if (!c) continue;
2099
2844
  const nc = c.nodes instanceof Map ? c.nodes.size : Object.keys(c.nodes ?? {}).length;
2100
2845
  if (nc > bestNodeCount) {
2101
2846
  best = c;
@@ -2146,13 +2891,13 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
2146
2891
  };
2147
2892
 
2148
2893
  // src/cli.ts
2149
- import * as fs2 from "fs";
2894
+ import * as fs3 from "fs";
2150
2895
  import * as os from "os";
2151
- import * as path2 from "path";
2896
+ import * as path3 from "path";
2152
2897
  import { fileURLToPath } from "url";
2153
- var __cliDirname = path2.dirname(fileURLToPath(import.meta.url));
2898
+ var __cliDirname = path3.dirname(fileURLToPath(import.meta.url));
2154
2899
  var VERSION = JSON.parse(
2155
- fs2.readFileSync(path2.resolve(__cliDirname, "../package.json"), "utf-8")
2900
+ fs3.readFileSync(path3.resolve(__cliDirname, "../package.json"), "utf-8")
2156
2901
  ).version;
2157
2902
  function getLanAddress() {
2158
2903
  const interfaces = os.networkInterfaces();
@@ -2257,9 +3002,9 @@ async function startDashboard() {
2257
3002
  if (!config.somaVault && process.env.SOMA_VAULT) {
2258
3003
  config.somaVault = process.env.SOMA_VAULT;
2259
3004
  }
2260
- const tracesPath = path2.resolve(config.tracesDir);
2261
- if (!fs2.existsSync(tracesPath)) {
2262
- fs2.mkdirSync(tracesPath, { recursive: true });
3005
+ const tracesPath = path3.resolve(config.tracesDir);
3006
+ if (!fs3.existsSync(tracesPath)) {
3007
+ fs3.mkdirSync(tracesPath, { recursive: true });
2263
3008
  }
2264
3009
  config.tracesDir = tracesPath;
2265
3010
  console.log("\nStarting AgentFlow Dashboard...\n");
@@ -2331,13 +3076,77 @@ Examples:
2331
3076
 
2332
3077
  // src/server.ts
2333
3078
  var __filename = fileURLToPath2(import.meta.url);
2334
- 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
+ }
2335
3144
  function serializeTrace(trace) {
2336
3145
  if (!trace) return trace;
2337
3146
  const obj = { ...trace };
2338
- if (obj.nodes instanceof Map) {
3147
+ if (trace.nodes instanceof Map) {
2339
3148
  const nodesObj = {};
2340
- for (const [key, value] of obj.nodes) {
3149
+ for (const [key, value] of trace.nodes) {
2341
3150
  nodesObj[key] = value;
2342
3151
  }
2343
3152
  obj.nodes = nodesObj;
@@ -2351,11 +3160,11 @@ var DashboardServer = class {
2351
3160
  this.userConfig = userCfg;
2352
3161
  this.configPath = cfgPath;
2353
3162
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
2354
- const dashConfigPath = path3.join(home, ".agentflow/dashboard-config.json");
3163
+ const dashConfigPath = path4.join(home, ".agentflow/dashboard-config.json");
2355
3164
  if (!config.dataDirs) config.dataDirs = [];
2356
3165
  try {
2357
- if (fs3.existsSync(dashConfigPath)) {
2358
- const saved = JSON.parse(fs3.readFileSync(dashConfigPath, "utf-8"));
3166
+ if (fs4.existsSync(dashConfigPath)) {
3167
+ const saved = JSON.parse(fs4.readFileSync(dashConfigPath, "utf-8"));
2359
3168
  const extraDirs = saved.extraDirs ?? [];
2360
3169
  for (const d of extraDirs) {
2361
3170
  if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
@@ -2364,7 +3173,7 @@ var DashboardServer = class {
2364
3173
  } catch {
2365
3174
  }
2366
3175
  for (const p of getDiscoveryPaths(this.userConfig)) {
2367
- if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
3176
+ if (fs4.existsSync(p) && !config.dataDirs.includes(p)) {
2368
3177
  config.dataDirs.push(p);
2369
3178
  }
2370
3179
  }
@@ -2375,8 +3184,9 @@ var DashboardServer = class {
2375
3184
  });
2376
3185
  this.stats = new AgentStats();
2377
3186
  this.knowledgeStore = createKnowledgeStore({
2378
- baseDir: path3.join(config.tracesDir, "..", ".agentflow", "knowledge")
3187
+ baseDir: path4.join(config.tracesDir, "..", ".agentflow", "knowledge")
2379
3188
  });
3189
+ this.commandExecutor = createCommandExecutor(this.userConfig);
2380
3190
  this.setupExpress();
2381
3191
  this.setupWebSocket();
2382
3192
  this.setupTraceWatcher();
@@ -2407,6 +3217,7 @@ var DashboardServer = class {
2407
3217
  ts: 0
2408
3218
  };
2409
3219
  knowledgeStore;
3220
+ commandExecutor;
2410
3221
  userConfig;
2411
3222
  configPath;
2412
3223
  setupExpress() {
@@ -2431,11 +3242,11 @@ var DashboardServer = class {
2431
3242
  next();
2432
3243
  });
2433
3244
  }
2434
- const pkgDir = path3.join(__dirname, "..");
2435
- const clientDir = path3.join(pkgDir, "dist/client");
2436
- const clientIndex = path3.join(clientDir, "index.html");
2437
- const srcDir = path3.join(pkgDir, "src/client");
2438
- 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);
2439
3250
  if (needsBuild) {
2440
3251
  try {
2441
3252
  console.log("Building dashboard client...");
@@ -2444,7 +3255,7 @@ var DashboardServer = class {
2444
3255
  console.warn("Client build failed \u2014 dashboard UI may be stale:", err.message);
2445
3256
  }
2446
3257
  }
2447
- if (fs3.existsSync(clientDir)) {
3258
+ if (fs4.existsSync(clientDir)) {
2448
3259
  this.app.use(express.static(clientDir));
2449
3260
  }
2450
3261
  this.app.get("/api/traces", (req, res) => {
@@ -2491,6 +3302,20 @@ var DashboardServer = class {
2491
3302
  res.status(500).json({ error: "Failed to load trace events" });
2492
3303
  }
2493
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
+ });
2494
3319
  this.app.get("/api/agents", (req, res) => {
2495
3320
  try {
2496
3321
  const raw = this.stats.getAgentsList();
@@ -2613,19 +3438,37 @@ var DashboardServer = class {
2613
3438
  res.status(500).json({ error: "Failed to build process graph" });
2614
3439
  }
2615
3440
  });
2616
- this.app.get("/api/agents/:agentId/variants", (req, res) => {
3441
+ this.app.get("/api/agents/:agentId/variants", async (req, res) => {
2617
3442
  try {
2618
3443
  const agentId = req.params.agentId;
3444
+ const byModel = req.query.by === "model";
2619
3445
  const graphs = this.getGraphTraces(agentId);
2620
3446
  if (graphs.length === 0) {
2621
- return res.json({ agentId, totalTraces: 0, variants: [] });
3447
+ return res.json({ agentId, totalTraces: 0, variants: [], modelVariants: [] });
2622
3448
  }
2623
3449
  const variants = findVariants(graphs).map((v) => ({
2624
3450
  pathSignature: v.pathSignature,
2625
3451
  count: v.count,
2626
3452
  percentage: v.percentage
2627
3453
  }));
2628
- 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 });
2629
3472
  } catch (error) {
2630
3473
  console.error("Variants error:", error);
2631
3474
  res.status(500).json({ error: "Failed to compute variants" });
@@ -2650,6 +3493,147 @@ var DashboardServer = class {
2650
3493
  res.status(500).json({ error: "Failed to compute bottlenecks" });
2651
3494
  }
2652
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
+ });
2653
3637
  this.app.get("/api/agents/:agentId/profile", (req, res) => {
2654
3638
  try {
2655
3639
  const profile = this.knowledgeStore.getAgentProfile(req.params.agentId);
@@ -2681,8 +3665,8 @@ var DashboardServer = class {
2681
3665
  const nodeArr = Object.values(nodes);
2682
3666
  const sorted = nodeArr.filter((n) => n.name && typeof n.startTime === "number" && n.startTime > 0).sort((a, b) => (a.startTime ?? 0) - (b.startTime ?? 0));
2683
3667
  for (let i = 0; i < sorted.length - 1; i++) {
2684
- const from = (_a = sorted[i]) == null ? void 0 : _a.name;
2685
- const to = (_b = sorted[i + 1]) == null ? void 0 : _b.name;
3668
+ const from = ((_a = sorted[i]) == null ? void 0 : _a.name) ?? "";
3669
+ const to = ((_b = sorted[i + 1]) == null ? void 0 : _b.name) ?? "";
2686
3670
  const key = `${from}|||${to}`;
2687
3671
  transMap.set(key, (transMap.get(key) ?? 0) + 1);
2688
3672
  }
@@ -2705,7 +3689,7 @@ var DashboardServer = class {
2705
3689
  const model = {
2706
3690
  transitions: [...transMap.entries()].map(([key, count]) => {
2707
3691
  const [from, to] = key.split("|||");
2708
- return { from, to, count };
3692
+ return { from: from ?? "", to: to ?? "", count };
2709
3693
  }),
2710
3694
  nodeTypes: Object.fromEntries(nodeTypeMap)
2711
3695
  };
@@ -2758,11 +3742,11 @@ var DashboardServer = class {
2758
3742
  return res.json({ tier: "teaser", somaVault: false, governanceAvailable: false });
2759
3743
  }
2760
3744
  try {
2761
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
2762
- if (!fs3.existsSync(reportPath)) {
3745
+ const reportPath = path4.join(somaVault, "..", "soma-report.json");
3746
+ if (!fs4.existsSync(reportPath)) {
2763
3747
  return res.json({ tier: "free", somaVault: true, governanceAvailable: false });
2764
3748
  }
2765
- const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
3749
+ const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
2766
3750
  const hasGovernance = report.governance && typeof report.governance.pending === "number";
2767
3751
  return res.json({
2768
3752
  tier: hasGovernance ? "pro" : "free",
@@ -2779,15 +3763,15 @@ var DashboardServer = class {
2779
3763
  return res.json({ available: false, teaser: true });
2780
3764
  }
2781
3765
  try {
2782
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
2783
- if (!fs3.existsSync(reportPath)) {
3766
+ const reportPath = path4.join(somaVault, "..", "soma-report.json");
3767
+ if (!fs4.existsSync(reportPath)) {
2784
3768
  return res.json({
2785
3769
  available: false,
2786
3770
  teaser: false,
2787
3771
  message: "No report file yet. Run soma watch."
2788
3772
  });
2789
3773
  }
2790
- const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
3774
+ const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
2791
3775
  res.json(report);
2792
3776
  } catch (error) {
2793
3777
  console.error("Soma report error:", error);
@@ -2800,11 +3784,11 @@ var DashboardServer = class {
2800
3784
  return res.json({ available: false });
2801
3785
  }
2802
3786
  try {
2803
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
2804
- if (!fs3.existsSync(reportPath)) {
3787
+ const reportPath = path4.join(somaVault, "..", "soma-report.json");
3788
+ if (!fs4.existsSync(reportPath)) {
2805
3789
  return res.json({ available: false, message: "No report file. Run soma report." });
2806
3790
  }
2807
- const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
3791
+ const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
2808
3792
  res.json({
2809
3793
  available: true,
2810
3794
  layers: report.layers ?? { archive: 0, working: 0, emerging: 0, canon: 0 },
@@ -2812,7 +3796,9 @@ var DashboardServer = class {
2812
3796
  insights: (report.insights ?? []).filter(
2813
3797
  (i) => i.layer === "emerging" && i.proposal_status === "pending"
2814
3798
  ),
2815
- canon: (report.insights ?? []).filter((i) => i.layer === "canon"),
3799
+ canon: (report.insights ?? []).filter(
3800
+ (i) => i.layer === "canon"
3801
+ ),
2816
3802
  generatedAt: report.generatedAt
2817
3803
  });
2818
3804
  } catch (error) {
@@ -2839,7 +3825,9 @@ var DashboardServer = class {
2839
3825
  );
2840
3826
  res.json({ success: true, message: result.trim() });
2841
3827
  } catch (error) {
2842
- 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
+ });
2843
3831
  }
2844
3832
  });
2845
3833
  this.app.post("/api/soma/governance/reject", (req, res) => {
@@ -2870,7 +3858,9 @@ var DashboardServer = class {
2870
3858
  );
2871
3859
  res.json({ success: true, message: result.trim() });
2872
3860
  } catch (error) {
2873
- 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
+ });
2874
3864
  }
2875
3865
  });
2876
3866
  this.app.get("/api/soma/governance/evidence/:id", (req, res) => {
@@ -2889,16 +3879,18 @@ var DashboardServer = class {
2889
3879
  );
2890
3880
  res.json({ available: true, output: result.trim() });
2891
3881
  } catch (error) {
2892
- 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
+ });
2893
3885
  }
2894
3886
  });
2895
3887
  this.app.get("/api/soma/policies", (_req, res) => {
2896
3888
  const somaVault = this.config.somaVault;
2897
3889
  if (!somaVault) return res.json({ policies: [] });
2898
3890
  try {
2899
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
2900
- if (!fs3.existsSync(reportPath)) return res.json({ policies: [] });
2901
- 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"));
2902
3894
  res.json({ policies: report.policies ?? [] });
2903
3895
  } catch {
2904
3896
  res.json({ policies: [] });
@@ -2929,7 +3921,9 @@ var DashboardServer = class {
2929
3921
  const result = execFileSync("npx", args, { encoding: "utf-8", timeout: 1e4 });
2930
3922
  res.json({ success: true, message: result.trim() });
2931
3923
  } catch (error) {
2932
- 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
+ });
2933
3927
  }
2934
3928
  });
2935
3929
  this.app.delete("/api/soma/policies/:name", (req, res) => {
@@ -2949,28 +3943,46 @@ var DashboardServer = class {
2949
3943
  );
2950
3944
  res.json({ success: true, message: result.trim() });
2951
3945
  } catch (error) {
2952
- 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
+ });
2953
3949
  }
2954
3950
  });
2955
3951
  this.app.get("/api/soma/vault/entities", (req, res) => {
2956
3952
  const somaVault = this.config.somaVault;
2957
3953
  if (!somaVault) return res.json({ entities: [], total: 0 });
2958
3954
  try {
2959
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
2960
- if (!fs3.existsSync(reportPath)) return res.json({ entities: [], total: 0 });
2961
- const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
2962
- let entities = [
2963
- ...(report.agents ?? []).map((a) => ({ ...a, type: "agent", id: a.name })),
2964
- ...(report.insights ?? []).map((i, idx) => {
2965
- var _a;
2966
- return {
2967
- ...i,
2968
- type: i.type || "insight",
2969
- id: ((_a = i.title) == null ? void 0 : _a.replace(/\s+/g, "-").toLowerCase()) || `insight-${idx}`
2970
- };
2971
- }),
2972
- ...(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"
2973
3963
  ];
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
+ }
2974
3986
  const {
2975
3987
  type,
2976
3988
  layer,
@@ -3000,35 +4012,414 @@ var DashboardServer = class {
3000
4012
  const somaVault = this.config.somaVault;
3001
4013
  if (!somaVault) return res.status(404).json({ error: "Soma vault not configured" });
3002
4014
  try {
3003
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
3004
- const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
3005
- const { type, id } = req.params;
3006
- 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 = [];
3007
4027
  if (type === "agent") {
3008
- entity = (report.agents ?? []).find((a) => a.name === id);
3009
- } else if (type === "policy") {
3010
- entity = (report.policies ?? []).find((p) => p.name === id);
3011
- } else {
3012
- entity = (report.insights ?? []).find(
3013
- (i) => {
3014
- var _a;
3015
- 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
+ }
3016
4050
  }
3017
- );
4051
+ }
3018
4052
  }
3019
- if (!entity) return res.status(404).json({ error: "Entity not found" });
3020
4053
  res.json({
3021
- ...entity,
4054
+ ...fm,
3022
4055
  type,
3023
4056
  id,
3024
- body: entity.claim || entity.conditions || "",
3025
- tags: entity.tags ?? [],
3026
- 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
3027
4060
  });
3028
4061
  } catch {
3029
4062
  res.status(404).json({ error: "Entity not found" });
3030
4063
  }
3031
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
+ });
3032
4423
  this.app.get("/api/process-health", (_req, res) => {
3033
4424
  var _a, _b;
3034
4425
  try {
@@ -3038,7 +4429,7 @@ var DashboardServer = class {
3038
4429
  }
3039
4430
  const discoveryDirs = [
3040
4431
  this.config.tracesDir,
3041
- path3.dirname(this.config.tracesDir),
4432
+ path4.dirname(this.config.tracesDir),
3042
4433
  ...this.config.dataDirs || []
3043
4434
  ];
3044
4435
  let configs = discoverAllProcessConfigs(discoveryDirs);
@@ -3101,7 +4492,7 @@ var DashboardServer = class {
3101
4492
  // Topology edges: parent-child relationships from process ppid
3102
4493
  topology: uniqueProcesses.map((p) => {
3103
4494
  try {
3104
- const statusContent = fs3.readFileSync(`/proc/${p.pid}/status`, "utf8");
4495
+ const statusContent = fs4.readFileSync(`/proc/${p.pid}/status`, "utf8");
3105
4496
  const ppidMatch = statusContent.match(/^PPid:\s+(\d+)/m);
3106
4497
  const ppid = ppidMatch ? parseInt(ppidMatch[1] ?? "0", 10) : 0;
3107
4498
  if (ppid > 1 && allKnownPids.has(ppid)) {
@@ -3121,11 +4512,11 @@ var DashboardServer = class {
3121
4512
  this.app.get("/api/directories", (_req, res) => {
3122
4513
  try {
3123
4514
  const home = process.env.HOME ?? "/home/trader";
3124
- const configPath = path3.join(home, ".agentflow/dashboard-config.json");
4515
+ const configPath = path4.join(home, ".agentflow/dashboard-config.json");
3125
4516
  let extraDirs = [];
3126
4517
  try {
3127
- if (fs3.existsSync(configPath)) {
3128
- const cfg = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
4518
+ if (fs4.existsSync(configPath)) {
4519
+ const cfg = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
3129
4520
  extraDirs = cfg.extraDirs ?? [];
3130
4521
  }
3131
4522
  } catch {
@@ -3133,7 +4524,7 @@ var DashboardServer = class {
3133
4524
  const watched = [
3134
4525
  ...new Set(
3135
4526
  [this.config.tracesDir, ...this.config.dataDirs || [], ...extraDirs].map(
3136
- (w) => path3.resolve(w)
4527
+ (w) => path4.resolve(w)
3137
4528
  )
3138
4529
  )
3139
4530
  ];
@@ -3152,8 +4543,8 @@ var DashboardServer = class {
3152
4543
  for (const line of raw.split("\n")) {
3153
4544
  const match = line.match(/path=([^\s;]+)/);
3154
4545
  if (match == null ? void 0 : match[1]) {
3155
- const dir = path3.dirname(match[1]);
3156
- if (fs3.existsSync(dir)) discovered.push(dir);
4546
+ const dir = path4.dirname(match[1]);
4547
+ if (fs4.existsSync(dir)) discovered.push(dir);
3157
4548
  }
3158
4549
  }
3159
4550
  } catch {
@@ -3161,15 +4552,15 @@ var DashboardServer = class {
3161
4552
  }
3162
4553
  const commonPaths = [
3163
4554
  ...getDiscoveryPaths(this.userConfig),
3164
- path3.join(home, ".agentflow/traces")
4555
+ path4.join(home, ".agentflow/traces")
3165
4556
  ];
3166
4557
  for (const p of commonPaths) {
3167
- if (fs3.existsSync(p) && !discovered.includes(p)) {
4558
+ if (fs4.existsSync(p) && !discovered.includes(p)) {
3168
4559
  discovered.push(p);
3169
4560
  }
3170
4561
  }
3171
- const watchedSet = new Set(watched.map((w) => path3.resolve(w)));
3172
- 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)));
3173
4564
  res.json({ watched, discovered, suggested });
3174
4565
  } catch (error) {
3175
4566
  console.error("Directory discovery error:", error);
@@ -3180,22 +4571,22 @@ var DashboardServer = class {
3180
4571
  try {
3181
4572
  const { add, remove } = req.body;
3182
4573
  if (add) {
3183
- const resolved = path3.resolve(add);
4574
+ const resolved = path4.resolve(add);
3184
4575
  if (resolved !== add || add.includes("..")) {
3185
4576
  return res.status(400).json({ error: "Invalid directory path" });
3186
4577
  }
3187
- if (!fs3.existsSync(resolved)) {
4578
+ if (!fs4.existsSync(resolved)) {
3188
4579
  return res.status(400).json({ error: `Directory does not exist: ${add}` });
3189
4580
  }
3190
4581
  }
3191
- const configPath = path3.join(
4582
+ const configPath = path4.join(
3192
4583
  process.env.HOME ?? "/home/trader",
3193
4584
  ".agentflow/dashboard-config.json"
3194
4585
  );
3195
4586
  let config = {};
3196
4587
  try {
3197
- if (fs3.existsSync(configPath)) {
3198
- config = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
4588
+ if (fs4.existsSync(configPath)) {
4589
+ config = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
3199
4590
  }
3200
4591
  } catch {
3201
4592
  }
@@ -3206,8 +4597,8 @@ var DashboardServer = class {
3206
4597
  if (remove) {
3207
4598
  config.extraDirs = config.extraDirs.filter((d) => d !== remove);
3208
4599
  }
3209
- fs3.mkdirSync(path3.dirname(configPath), { recursive: true });
3210
- 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));
3211
4602
  res.json({ ok: true, extraDirs: config.extraDirs });
3212
4603
  } catch (error) {
3213
4604
  console.error("Directory config error:", error);
@@ -3249,7 +4640,10 @@ var DashboardServer = class {
3249
4640
  lastModified: Date.now(),
3250
4641
  sourceDir: "http-collector"
3251
4642
  };
3252
- this.watcher.traces.set(`otel:${trace.id}`, watched);
4643
+ this.watcher.traces.set(
4644
+ `otel:${trace.id}`,
4645
+ watched
4646
+ );
3253
4647
  ingested++;
3254
4648
  }
3255
4649
  if (ingested > 0) {
@@ -3273,14 +4667,18 @@ var DashboardServer = class {
3273
4667
  this.app.get("/ready", (_req, res) => {
3274
4668
  res.json({ status: "ready" });
3275
4669
  });
3276
- this.app.get("*", (_req, res) => {
3277
- const clientIndex2 = path3.join(__dirname, "../dist/client/index.html");
3278
- if (fs3.existsSync(clientIndex2)) {
3279
- res.sendFile(clientIndex2);
3280
- } else {
3281
- 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
+ }
3282
4680
  }
3283
- });
4681
+ );
3284
4682
  }
3285
4683
  setupWebSocket() {
3286
4684
  this.wss.on("connection", (ws) => {
@@ -3306,9 +4704,9 @@ var DashboardServer = class {
3306
4704
  setupSomaReportWatcher() {
3307
4705
  const somaVault = this.config.somaVault;
3308
4706
  if (!somaVault) return;
3309
- const reportPath = path3.join(somaVault, "..", "soma-report.json");
3310
- const reportDir = path3.dirname(reportPath);
3311
- 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;
3312
4710
  let debounceTimer = null;
3313
4711
  const watcher = chokidar2.watch(reportPath, {
3314
4712
  ignoreInitial: true,
@@ -3320,7 +4718,7 @@ var DashboardServer = class {
3320
4718
  debounceTimer = setTimeout(() => {
3321
4719
  var _a, _b;
3322
4720
  try {
3323
- const report = JSON.parse(fs3.readFileSync(reportPath, "utf-8"));
4721
+ const report = JSON.parse(fs4.readFileSync(reportPath, "utf-8"));
3324
4722
  this.broadcast({ type: "soma-report-updated", data: report });
3325
4723
  if (report.generatedAt) {
3326
4724
  this.broadcast({
@@ -3351,7 +4749,10 @@ var DashboardServer = class {
3351
4749
  const nodes = trace.nodes;
3352
4750
  if (!nodes || typeof nodes === "object" && Object.keys(nodes).length === 0) continue;
3353
4751
  const nodeValues = Object.values(nodes);
3354
- 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;
3355
4756
  graphs.push(loadGraph2(trace));
3356
4757
  } catch {
3357
4758
  }
@@ -3445,10 +4846,7 @@ var DashboardServer = class {
3445
4846
  }
3446
4847
  }
3447
4848
  const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
3448
- const maxNodeCount = Math.max(
3449
- ...nodes.filter((n) => !n.isVirtual).map((n) => n.count),
3450
- 1
3451
- );
4849
+ const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
3452
4850
  return { agentId, totalTraces: model.totalGraphs, nodes, edges, maxEdgeCount, maxNodeCount };
3453
4851
  }
3454
4852
  /**
@@ -3492,7 +4890,7 @@ var DashboardServer = class {
3492
4890
  }
3493
4891
  const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
3494
4892
  for (let i = 0; i < seq.length; i++) {
3495
- const act = seq[i];
4893
+ const act = seq[i] ?? "";
3496
4894
  activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
3497
4895
  if (i < seq.length - 1) {
3498
4896
  const key = `${act} \u2192 ${seq[i + 1]}`;
@@ -3591,15 +4989,15 @@ var DashboardServer = class {
3591
4989
  /** Check if any src/client file is newer than the built bundle. */
3592
4990
  isClientStale(srcDir, distDir) {
3593
4991
  try {
3594
- const distIndex = path3.join(distDir, "index.html");
3595
- if (!fs3.existsSync(distIndex)) return true;
3596
- 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;
3597
4995
  const check = (dir) => {
3598
- for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
3599
- 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);
3600
4998
  if (entry.isDirectory()) {
3601
4999
  if (check(full)) return true;
3602
- } else if (fs3.statSync(full).mtimeMs > distMtime) {
5000
+ } else if (fs4.statSync(full).mtimeMs > distMtime) {
3603
5001
  return true;
3604
5002
  }
3605
5003
  }
@@ -3625,6 +5023,9 @@ var DashboardServer = class {
3625
5023
  getStats() {
3626
5024
  return this.stats.getGlobalStats();
3627
5025
  }
5026
+ getTrace(filename) {
5027
+ return this.watcher.getTrace(filename);
5028
+ }
3628
5029
  getTraces() {
3629
5030
  return this.watcher.getAllTraces();
3630
5031
  }