@blokjs/runner 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/dist/Blok.d.ts +19 -0
  2. package/dist/Blok.js +184 -0
  3. package/dist/Blok.js.map +1 -0
  4. package/dist/BlokResponse.d.ts +16 -0
  5. package/dist/BlokResponse.js +28 -0
  6. package/dist/BlokResponse.js.map +1 -0
  7. package/dist/Configuration.d.ts +37 -0
  8. package/dist/Configuration.js +248 -0
  9. package/dist/Configuration.js.map +1 -0
  10. package/dist/ConfigurationResolver.d.ts +7 -0
  11. package/dist/ConfigurationResolver.js +15 -0
  12. package/dist/ConfigurationResolver.js.map +1 -0
  13. package/dist/DefaultLogger.d.ts +65 -0
  14. package/dist/DefaultLogger.js +101 -0
  15. package/dist/DefaultLogger.js.map +1 -0
  16. package/dist/LocalStorage.d.ts +7 -0
  17. package/dist/LocalStorage.js +56 -0
  18. package/dist/LocalStorage.js.map +1 -0
  19. package/dist/MemoryUsage.d.ts +22 -0
  20. package/dist/MemoryUsage.js +83 -0
  21. package/dist/MemoryUsage.js.map +1 -0
  22. package/dist/NodeMap.d.ts +7 -0
  23. package/dist/NodeMap.js +13 -0
  24. package/dist/NodeMap.js.map +1 -0
  25. package/dist/ResolverBase.d.ts +8 -0
  26. package/dist/ResolverBase.js +18 -0
  27. package/dist/ResolverBase.js.map +1 -0
  28. package/dist/Runner.d.ts +25 -0
  29. package/dist/Runner.js +32 -0
  30. package/dist/Runner.js.map +1 -0
  31. package/dist/RunnerNode.d.ts +9 -0
  32. package/dist/RunnerNode.js +8 -0
  33. package/dist/RunnerNode.js.map +1 -0
  34. package/dist/RunnerNodeBase.d.ts +4 -0
  35. package/dist/RunnerNodeBase.js +3 -0
  36. package/dist/RunnerNodeBase.js.map +1 -0
  37. package/dist/RunnerSteps.d.ts +14 -0
  38. package/dist/RunnerSteps.js +110 -0
  39. package/dist/RunnerSteps.js.map +1 -0
  40. package/dist/RuntimeAdapterNode.d.ts +19 -0
  41. package/dist/RuntimeAdapterNode.js +87 -0
  42. package/dist/RuntimeAdapterNode.js.map +1 -0
  43. package/dist/RuntimeRegistry.d.ts +61 -0
  44. package/dist/RuntimeRegistry.js +87 -0
  45. package/dist/RuntimeRegistry.js.map +1 -0
  46. package/dist/TriggerBase.d.ts +119 -0
  47. package/dist/TriggerBase.js +413 -0
  48. package/dist/TriggerBase.js.map +1 -0
  49. package/dist/adapters/BunRuntimeAdapter.d.ts +38 -0
  50. package/dist/adapters/BunRuntimeAdapter.js +169 -0
  51. package/dist/adapters/BunRuntimeAdapter.js.map +1 -0
  52. package/dist/adapters/DockerRuntimeAdapter.d.ts +85 -0
  53. package/dist/adapters/DockerRuntimeAdapter.js +298 -0
  54. package/dist/adapters/DockerRuntimeAdapter.js.map +1 -0
  55. package/dist/adapters/HttpRuntimeAdapter.d.ts +58 -0
  56. package/dist/adapters/HttpRuntimeAdapter.js +152 -0
  57. package/dist/adapters/HttpRuntimeAdapter.js.map +1 -0
  58. package/dist/adapters/NodeJsRuntimeAdapter.d.ts +23 -0
  59. package/dist/adapters/NodeJsRuntimeAdapter.js +67 -0
  60. package/dist/adapters/NodeJsRuntimeAdapter.js.map +1 -0
  61. package/dist/adapters/RuntimeAdapter.d.ts +42 -0
  62. package/dist/adapters/RuntimeAdapter.js +2 -0
  63. package/dist/adapters/RuntimeAdapter.js.map +1 -0
  64. package/dist/adapters/WasmRuntimeAdapter.d.ts +69 -0
  65. package/dist/adapters/WasmRuntimeAdapter.js +279 -0
  66. package/dist/adapters/WasmRuntimeAdapter.js.map +1 -0
  67. package/dist/cache/NodeResultCache.d.ts +286 -0
  68. package/dist/cache/NodeResultCache.js +499 -0
  69. package/dist/cache/NodeResultCache.js.map +1 -0
  70. package/dist/cache/index.d.ts +1 -0
  71. package/dist/cache/index.js +2 -0
  72. package/dist/cache/index.js.map +1 -0
  73. package/dist/cost/CostEstimator.d.ts +57 -0
  74. package/dist/cost/CostEstimator.js +171 -0
  75. package/dist/cost/CostEstimator.js.map +1 -0
  76. package/dist/cost/index.d.ts +4 -0
  77. package/dist/cost/index.js +3 -0
  78. package/dist/cost/index.js.map +1 -0
  79. package/dist/cost/pricing.d.ts +24 -0
  80. package/dist/cost/pricing.js +169 -0
  81. package/dist/cost/pricing.js.map +1 -0
  82. package/dist/defineNode.d.ts +155 -0
  83. package/dist/defineNode.js +191 -0
  84. package/dist/defineNode.js.map +1 -0
  85. package/dist/graphql/GraphQLSchemaGenerator.d.ts +129 -0
  86. package/dist/graphql/GraphQLSchemaGenerator.js +425 -0
  87. package/dist/graphql/GraphQLSchemaGenerator.js.map +1 -0
  88. package/dist/hmr/FileWatcher.d.ts +62 -0
  89. package/dist/hmr/FileWatcher.js +185 -0
  90. package/dist/hmr/FileWatcher.js.map +1 -0
  91. package/dist/hmr/HmrDevConsole.d.ts +13 -0
  92. package/dist/hmr/HmrDevConsole.js +46 -0
  93. package/dist/hmr/HmrDevConsole.js.map +1 -0
  94. package/dist/hmr/HotReloadManager.d.ts +84 -0
  95. package/dist/hmr/HotReloadManager.js +195 -0
  96. package/dist/hmr/HotReloadManager.js.map +1 -0
  97. package/dist/hmr/index.d.ts +39 -0
  98. package/dist/hmr/index.js +38 -0
  99. package/dist/hmr/index.js.map +1 -0
  100. package/dist/index.d.ts +107 -0
  101. package/dist/index.js +107 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/integrations/APMIntegration.d.ts +141 -0
  104. package/dist/integrations/APMIntegration.js +212 -0
  105. package/dist/integrations/APMIntegration.js.map +1 -0
  106. package/dist/integrations/AzureMonitorIntegration.d.ts +118 -0
  107. package/dist/integrations/AzureMonitorIntegration.js +254 -0
  108. package/dist/integrations/AzureMonitorIntegration.js.map +1 -0
  109. package/dist/integrations/CloudWatchIntegration.d.ts +135 -0
  110. package/dist/integrations/CloudWatchIntegration.js +293 -0
  111. package/dist/integrations/CloudWatchIntegration.js.map +1 -0
  112. package/dist/integrations/SentryIntegration.d.ts +153 -0
  113. package/dist/integrations/SentryIntegration.js +200 -0
  114. package/dist/integrations/SentryIntegration.js.map +1 -0
  115. package/dist/integrations/index.d.ts +19 -0
  116. package/dist/integrations/index.js +16 -0
  117. package/dist/integrations/index.js.map +1 -0
  118. package/dist/marketplace/RuntimeAutoScaler.d.ts +148 -0
  119. package/dist/marketplace/RuntimeAutoScaler.js +366 -0
  120. package/dist/marketplace/RuntimeAutoScaler.js.map +1 -0
  121. package/dist/marketplace/RuntimeCatalog.d.ts +174 -0
  122. package/dist/marketplace/RuntimeCatalog.js +339 -0
  123. package/dist/marketplace/RuntimeCatalog.js.map +1 -0
  124. package/dist/marketplace/RuntimeDiscovery.d.ts +86 -0
  125. package/dist/marketplace/RuntimeDiscovery.js +219 -0
  126. package/dist/marketplace/RuntimeDiscovery.js.map +1 -0
  127. package/dist/marketplace/RuntimeHealthMonitor.d.ts +100 -0
  128. package/dist/marketplace/RuntimeHealthMonitor.js +241 -0
  129. package/dist/marketplace/RuntimeHealthMonitor.js.map +1 -0
  130. package/dist/marketplace/RuntimeMetricsDashboard.d.ts +113 -0
  131. package/dist/marketplace/RuntimeMetricsDashboard.js +293 -0
  132. package/dist/marketplace/RuntimeMetricsDashboard.js.map +1 -0
  133. package/dist/monitoring/CircuitBreaker.d.ts +107 -0
  134. package/dist/monitoring/CircuitBreaker.js +238 -0
  135. package/dist/monitoring/CircuitBreaker.js.map +1 -0
  136. package/dist/monitoring/DistributedTracer.d.ts +125 -0
  137. package/dist/monitoring/DistributedTracer.js +230 -0
  138. package/dist/monitoring/DistributedTracer.js.map +1 -0
  139. package/dist/monitoring/HealthCheck.d.ts +54 -0
  140. package/dist/monitoring/HealthCheck.js +102 -0
  141. package/dist/monitoring/HealthCheck.js.map +1 -0
  142. package/dist/monitoring/PerformanceProfiler.d.ts +63 -0
  143. package/dist/monitoring/PerformanceProfiler.js +229 -0
  144. package/dist/monitoring/PerformanceProfiler.js.map +1 -0
  145. package/dist/monitoring/PrometheusBootstrap.d.ts +30 -0
  146. package/dist/monitoring/PrometheusBootstrap.js +71 -0
  147. package/dist/monitoring/PrometheusBootstrap.js.map +1 -0
  148. package/dist/monitoring/PrometheusMetricsBridge.d.ts +60 -0
  149. package/dist/monitoring/PrometheusMetricsBridge.js +216 -0
  150. package/dist/monitoring/PrometheusMetricsBridge.js.map +1 -0
  151. package/dist/monitoring/RateLimiter.d.ts +58 -0
  152. package/dist/monitoring/RateLimiter.js +128 -0
  153. package/dist/monitoring/RateLimiter.js.map +1 -0
  154. package/dist/monitoring/StructuredLogger.d.ts +131 -0
  155. package/dist/monitoring/StructuredLogger.js +207 -0
  156. package/dist/monitoring/StructuredLogger.js.map +1 -0
  157. package/dist/monitoring/TracingBootstrap.d.ts +69 -0
  158. package/dist/monitoring/TracingBootstrap.js +129 -0
  159. package/dist/monitoring/TracingBootstrap.js.map +1 -0
  160. package/dist/monitoring/TriggerMetricsCollector.d.ts +94 -0
  161. package/dist/monitoring/TriggerMetricsCollector.js +174 -0
  162. package/dist/monitoring/TriggerMetricsCollector.js.map +1 -0
  163. package/dist/monitoring/index.d.ts +9 -0
  164. package/dist/monitoring/index.js +10 -0
  165. package/dist/monitoring/index.js.map +1 -0
  166. package/dist/openapi/OpenAPIGenerator.d.ts +192 -0
  167. package/dist/openapi/OpenAPIGenerator.js +373 -0
  168. package/dist/openapi/OpenAPIGenerator.js.map +1 -0
  169. package/dist/openapi/index.d.ts +20 -0
  170. package/dist/openapi/index.js +20 -0
  171. package/dist/openapi/index.js.map +1 -0
  172. package/dist/security/ABAC.d.ts +224 -0
  173. package/dist/security/ABAC.js +380 -0
  174. package/dist/security/ABAC.js.map +1 -0
  175. package/dist/security/AuditLogger.d.ts +242 -0
  176. package/dist/security/AuditLogger.js +317 -0
  177. package/dist/security/AuditLogger.js.map +1 -0
  178. package/dist/security/AuthMiddleware.d.ts +163 -0
  179. package/dist/security/AuthMiddleware.js +274 -0
  180. package/dist/security/AuthMiddleware.js.map +1 -0
  181. package/dist/security/EncryptionAtRest.d.ts +206 -0
  182. package/dist/security/EncryptionAtRest.js +236 -0
  183. package/dist/security/EncryptionAtRest.js.map +1 -0
  184. package/dist/security/OAuthProvider.d.ts +334 -0
  185. package/dist/security/OAuthProvider.js +719 -0
  186. package/dist/security/OAuthProvider.js.map +1 -0
  187. package/dist/security/PIIDetector.d.ts +233 -0
  188. package/dist/security/PIIDetector.js +354 -0
  189. package/dist/security/PIIDetector.js.map +1 -0
  190. package/dist/security/RBAC.d.ts +143 -0
  191. package/dist/security/RBAC.js +285 -0
  192. package/dist/security/RBAC.js.map +1 -0
  193. package/dist/security/SecretManager.d.ts +652 -0
  194. package/dist/security/SecretManager.js +1146 -0
  195. package/dist/security/SecretManager.js.map +1 -0
  196. package/dist/security/TLSConfig.d.ts +305 -0
  197. package/dist/security/TLSConfig.js +550 -0
  198. package/dist/security/TLSConfig.js.map +1 -0
  199. package/dist/security/index.d.ts +79 -0
  200. package/dist/security/index.js +80 -0
  201. package/dist/security/index.js.map +1 -0
  202. package/dist/testing/TestHarness.d.ts +189 -0
  203. package/dist/testing/TestHarness.js +272 -0
  204. package/dist/testing/TestHarness.js.map +1 -0
  205. package/dist/testing/TestLogger.d.ts +103 -0
  206. package/dist/testing/TestLogger.js +153 -0
  207. package/dist/testing/TestLogger.js.map +1 -0
  208. package/dist/testing/WorkflowTestRunner.d.ts +172 -0
  209. package/dist/testing/WorkflowTestRunner.js +355 -0
  210. package/dist/testing/WorkflowTestRunner.js.map +1 -0
  211. package/dist/testing/index.d.ts +21 -0
  212. package/dist/testing/index.js +22 -0
  213. package/dist/testing/index.js.map +1 -0
  214. package/dist/tracing/InMemoryRunStore.d.ts +44 -0
  215. package/dist/tracing/InMemoryRunStore.js +341 -0
  216. package/dist/tracing/InMemoryRunStore.js.map +1 -0
  217. package/dist/tracing/PostgresRunStore.d.ts +82 -0
  218. package/dist/tracing/PostgresRunStore.js +640 -0
  219. package/dist/tracing/PostgresRunStore.js.map +1 -0
  220. package/dist/tracing/RunStore.d.ts +38 -0
  221. package/dist/tracing/RunStore.js +2 -0
  222. package/dist/tracing/RunStore.js.map +1 -0
  223. package/dist/tracing/RunTracker.d.ts +75 -0
  224. package/dist/tracing/RunTracker.js +374 -0
  225. package/dist/tracing/RunTracker.js.map +1 -0
  226. package/dist/tracing/SqliteRunStore.d.ts +53 -0
  227. package/dist/tracing/SqliteRunStore.js +703 -0
  228. package/dist/tracing/SqliteRunStore.js.map +1 -0
  229. package/dist/tracing/TraceRouter.d.ts +47 -0
  230. package/dist/tracing/TraceRouter.js +904 -0
  231. package/dist/tracing/TraceRouter.js.map +1 -0
  232. package/dist/tracing/TracingLogger.d.ts +21 -0
  233. package/dist/tracing/TracingLogger.js +62 -0
  234. package/dist/tracing/TracingLogger.js.map +1 -0
  235. package/dist/tracing/createStore.d.ts +30 -0
  236. package/dist/tracing/createStore.js +75 -0
  237. package/dist/tracing/createStore.js.map +1 -0
  238. package/dist/tracing/index.d.ts +13 -0
  239. package/dist/tracing/index.js +9 -0
  240. package/dist/tracing/index.js.map +1 -0
  241. package/dist/tracing/sanitize.d.ts +7 -0
  242. package/dist/tracing/sanitize.js +95 -0
  243. package/dist/tracing/sanitize.js.map +1 -0
  244. package/dist/tracing/types.d.ts +178 -0
  245. package/dist/tracing/types.js +3 -0
  246. package/dist/tracing/types.js.map +1 -0
  247. package/dist/types/Average.d.ts +11 -0
  248. package/dist/types/Average.js +2 -0
  249. package/dist/types/Average.js.map +1 -0
  250. package/dist/types/Condition.d.ts +8 -0
  251. package/dist/types/Condition.js +2 -0
  252. package/dist/types/Condition.js.map +1 -0
  253. package/dist/types/Conditions.d.ts +5 -0
  254. package/dist/types/Conditions.js +2 -0
  255. package/dist/types/Conditions.js.map +1 -0
  256. package/dist/types/Config.d.ts +12 -0
  257. package/dist/types/Config.js +2 -0
  258. package/dist/types/Config.js.map +1 -0
  259. package/dist/types/Flow.d.ts +5 -0
  260. package/dist/types/Flow.js +2 -0
  261. package/dist/types/Flow.js.map +1 -0
  262. package/dist/types/GlobalOptions.d.ts +11 -0
  263. package/dist/types/GlobalOptions.js +2 -0
  264. package/dist/types/GlobalOptions.js.map +1 -0
  265. package/dist/types/Inputs.d.ts +5 -0
  266. package/dist/types/Inputs.js +2 -0
  267. package/dist/types/Inputs.js.map +1 -0
  268. package/dist/types/JsonLikeObject.d.ts +3 -0
  269. package/dist/types/JsonLikeObject.js +2 -0
  270. package/dist/types/JsonLikeObject.js.map +1 -0
  271. package/dist/types/Mapper.d.ts +5 -0
  272. package/dist/types/Mapper.js +2 -0
  273. package/dist/types/Mapper.js.map +1 -0
  274. package/dist/types/Node.d.ts +10 -0
  275. package/dist/types/Node.js +2 -0
  276. package/dist/types/Node.js.map +1 -0
  277. package/dist/types/ParamsDictionary.d.ts +3 -0
  278. package/dist/types/ParamsDictionary.js +2 -0
  279. package/dist/types/ParamsDictionary.js.map +1 -0
  280. package/dist/types/Properties.d.ts +5 -0
  281. package/dist/types/Properties.js +2 -0
  282. package/dist/types/Properties.js.map +1 -0
  283. package/dist/types/Targets.d.ts +5 -0
  284. package/dist/types/Targets.js +2 -0
  285. package/dist/types/Targets.js.map +1 -0
  286. package/dist/types/Trigger.d.ts +5 -0
  287. package/dist/types/Trigger.js +2 -0
  288. package/dist/types/Trigger.js.map +1 -0
  289. package/dist/types/TriggerHttp.d.ts +7 -0
  290. package/dist/types/TriggerHttp.js +2 -0
  291. package/dist/types/TriggerHttp.js.map +1 -0
  292. package/dist/types/TriggerResponse.d.ts +6 -0
  293. package/dist/types/TriggerResponse.js +2 -0
  294. package/dist/types/TriggerResponse.js.map +1 -0
  295. package/dist/types/Triggers.d.ts +5 -0
  296. package/dist/types/Triggers.js +2 -0
  297. package/dist/types/Triggers.js.map +1 -0
  298. package/dist/types/TryCatch.d.ts +6 -0
  299. package/dist/types/TryCatch.js +2 -0
  300. package/dist/types/TryCatch.js.map +1 -0
  301. package/dist/visualization/NodeDependencyGraph.d.ts +76 -0
  302. package/dist/visualization/NodeDependencyGraph.js +418 -0
  303. package/dist/visualization/NodeDependencyGraph.js.map +1 -0
  304. package/dist/visualization/WorkflowVisualizer.d.ts +144 -0
  305. package/dist/visualization/WorkflowVisualizer.js +446 -0
  306. package/dist/visualization/WorkflowVisualizer.js.map +1 -0
  307. package/package.json +95 -0
@@ -0,0 +1,904 @@
1
+ import http from "node:http";
2
+ import { RunTracker } from "./RunTracker";
3
+ /**
4
+ * Register trace API routes on an Express-compatible router.
5
+ *
6
+ * This function avoids importing express directly so the runner package
7
+ * doesn't need express as a dependency. The caller passes in a Router
8
+ * instance and the function registers all /__blok/* routes on it.
9
+ *
10
+ * Usage (in HttpTrigger.ts):
11
+ * ```ts
12
+ * import { Router } from "express";
13
+ * import { registerTraceRoutes } from "@blokjs/runner";
14
+ * const traceRouter = Router();
15
+ * registerTraceRoutes(traceRouter);
16
+ * app.use("/__blok", traceRouter);
17
+ * ```
18
+ */
19
+ export function registerTraceRoutes(router, tracker) {
20
+ const t = tracker || RunTracker.getInstance();
21
+ // --- CORS for cross-origin Studio UI ---
22
+ router.use((req, res, next) => {
23
+ res.setHeader("Access-Control-Allow-Origin", "*");
24
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
25
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Last-Event-ID");
26
+ if (req.method === "OPTIONS") {
27
+ res.sendStatus(204);
28
+ return;
29
+ }
30
+ next();
31
+ });
32
+ // === Utility Endpoints ===
33
+ router.get("/health", (_req, res) => {
34
+ res.json({
35
+ status: "ok",
36
+ version: process.env.npm_package_version || "0.0.0",
37
+ uptime: process.uptime(),
38
+ activeRuns: t.getActiveRunCount(),
39
+ });
40
+ });
41
+ router.get("/config", (_req, res) => {
42
+ const summaries = t.getWorkflowSummaries();
43
+ const workflows = summaries.map((s) => s.name);
44
+ const triggers = [...new Set(summaries.flatMap((s) => s.triggerTypes))];
45
+ res.json({ workflows, triggers });
46
+ });
47
+ // === Workflow Endpoints ===
48
+ router.get("/workflows", (_req, res) => {
49
+ const summaries = t.getWorkflowSummaries();
50
+ res.json(summaries);
51
+ });
52
+ router.get("/workflows/:name", (req, res) => {
53
+ const { name } = req.params;
54
+ const summaries = t.getWorkflowSummaries();
55
+ const summary = summaries.find((s) => s.name === name);
56
+ if (!summary) {
57
+ res.status(404).json({ error: `Workflow '${name}' not found` });
58
+ return;
59
+ }
60
+ // Collect node names and runtimes from recent runs
61
+ const { runs } = t.getRuns({ workflow: name, limit: 10 });
62
+ const nodeNames = new Set();
63
+ const runtimes = new Set();
64
+ for (const run of runs) {
65
+ const nodes = t.getNodeRuns(run.id);
66
+ for (const node of nodes) {
67
+ nodeNames.add(node.nodeName);
68
+ if (node.runtimeKind)
69
+ runtimes.add(node.runtimeKind);
70
+ }
71
+ }
72
+ res.json({
73
+ ...summary,
74
+ nodeNames: Array.from(nodeNames),
75
+ runtimes: Array.from(runtimes),
76
+ });
77
+ });
78
+ router.get("/workflows/:name/runs", (req, res) => {
79
+ const { name } = req.params;
80
+ const status = req.query.status;
81
+ const limit = Number.parseInt(req.query.limit || "50", 10);
82
+ const offset = Number.parseInt(req.query.offset || "0", 10);
83
+ const sort = req.query.sort || "desc";
84
+ const result = t.getRuns({
85
+ workflow: name,
86
+ status: status,
87
+ limit,
88
+ offset,
89
+ sort,
90
+ });
91
+ res.json({
92
+ runs: result.runs,
93
+ total: result.total,
94
+ page: Math.floor(offset / limit) + 1,
95
+ });
96
+ });
97
+ // === Diff (before /runs/:runId to avoid param collision) ===
98
+ /**
99
+ * Compare two runs side-by-side.
100
+ * Returns both runs with their nodes for diff view.
101
+ */
102
+ router.get("/runs/diff", (req, res) => {
103
+ const runIdA = req.query.a;
104
+ const runIdB = req.query.b;
105
+ if (!runIdA || !runIdB) {
106
+ res.status(400).json({ error: "Both query params 'a' and 'b' are required" });
107
+ return;
108
+ }
109
+ const runA = t.getRun(runIdA);
110
+ const runB = t.getRun(runIdB);
111
+ if (!runA) {
112
+ res.status(404).json({ error: `Run '${runIdA}' not found` });
113
+ return;
114
+ }
115
+ if (!runB) {
116
+ res.status(404).json({ error: `Run '${runIdB}' not found` });
117
+ return;
118
+ }
119
+ res.json({
120
+ runA: { run: runA, nodes: t.getNodeRuns(runIdA), logs: t.getLogs(runIdA) },
121
+ runB: { run: runB, nodes: t.getNodeRuns(runIdB), logs: t.getLogs(runIdB) },
122
+ });
123
+ });
124
+ // === Tags ===
125
+ router.get("/tags", (_req, res) => {
126
+ res.json({ tags: t.getAllTags() });
127
+ });
128
+ router.post("/runs/:runId/tags", (req, res) => {
129
+ const { runId } = req.params;
130
+ const run = t.getRun(runId);
131
+ if (!run) {
132
+ res.status(404).json({ error: `Run '${runId}' not found` });
133
+ return;
134
+ }
135
+ const body = req.body;
136
+ const tagsToAdd = [];
137
+ if (body?.tag)
138
+ tagsToAdd.push(body.tag);
139
+ if (body?.tags)
140
+ tagsToAdd.push(...body.tags);
141
+ if (tagsToAdd.length === 0) {
142
+ res.status(400).json({ error: "Provide 'tag' or 'tags' in request body" });
143
+ return;
144
+ }
145
+ const added = [];
146
+ for (const tag of tagsToAdd) {
147
+ if (t.addTag(runId, tag.trim())) {
148
+ added.push(tag.trim());
149
+ }
150
+ }
151
+ res.json({ added, tags: run.tags || [] });
152
+ });
153
+ router.delete("/runs/:runId/tags/:tag", (req, res) => {
154
+ const { runId, tag } = req.params;
155
+ const run = t.getRun(runId);
156
+ if (!run) {
157
+ res.status(404).json({ error: `Run '${runId}' not found` });
158
+ return;
159
+ }
160
+ const removed = t.removeTag(runId, tag);
161
+ res.json({ removed, tags: run.tags || [] });
162
+ });
163
+ // === Metrics ===
164
+ router.get("/metrics", (req, res) => {
165
+ const workflow = req.query.workflow;
166
+ const metrics = t.getMetrics(workflow);
167
+ res.json(metrics);
168
+ });
169
+ // === Export ===
170
+ /**
171
+ * Export runs as JSON or CSV.
172
+ * Bulk export: GET /__blok/runs/export?format=json|csv&workflow=...&status=...&limit=1000
173
+ * Must be registered before /runs/:runId to avoid param collision.
174
+ */
175
+ router.get("/runs/export", (req, res) => {
176
+ const format = (req.query.format || "json");
177
+ const workflow = req.query.workflow;
178
+ const status = req.query.status;
179
+ const limit = Number.parseInt(req.query.limit || "1000", 10);
180
+ const result = t.getRuns({
181
+ workflow,
182
+ status: status,
183
+ limit,
184
+ sort: "desc",
185
+ });
186
+ if (format === "csv") {
187
+ const csv = runsToCsv(result.runs);
188
+ res.setHeader("Content-Type", "text/csv");
189
+ res.setHeader("Content-Disposition", `attachment; filename="blok-runs-${Date.now()}.csv"`);
190
+ res.write(csv);
191
+ res.end();
192
+ return;
193
+ }
194
+ // JSON export — include full detail for each run
195
+ const exportData = {
196
+ exportedAt: new Date().toISOString(),
197
+ format: "json",
198
+ total: result.runs.length,
199
+ runs: result.runs.map((run) => ({
200
+ run,
201
+ nodes: t.getNodeRuns(run.id),
202
+ events: t.getEvents(run.id),
203
+ logs: t.getLogs(run.id),
204
+ })),
205
+ };
206
+ res.setHeader("Content-Type", "application/json");
207
+ res.setHeader("Content-Disposition", `attachment; filename="blok-runs-${Date.now()}.json"`);
208
+ res.json(exportData);
209
+ });
210
+ /**
211
+ * Export a single run as JSON or CSV.
212
+ * GET /__blok/runs/:runId/export?format=json|csv
213
+ */
214
+ router.get("/runs/:runId/export", (req, res) => {
215
+ const { runId } = req.params;
216
+ const format = (req.query.format || "json");
217
+ const run = t.getRun(runId);
218
+ if (!run) {
219
+ res.status(404).json({ error: `Run '${runId}' not found` });
220
+ return;
221
+ }
222
+ const nodes = t.getNodeRuns(runId);
223
+ const events = t.getEvents(runId);
224
+ const logs = t.getLogs(runId);
225
+ if (format === "csv") {
226
+ const csv = singleRunToCsv(run, nodes, logs);
227
+ res.setHeader("Content-Type", "text/csv");
228
+ res.setHeader("Content-Disposition", `attachment; filename="blok-run-${runId}.csv"`);
229
+ res.write(csv);
230
+ res.end();
231
+ return;
232
+ }
233
+ const exportData = {
234
+ exportedAt: new Date().toISOString(),
235
+ format: "json",
236
+ run,
237
+ nodes,
238
+ events,
239
+ logs,
240
+ };
241
+ res.setHeader("Content-Type", "application/json");
242
+ res.setHeader("Content-Disposition", `attachment; filename="blok-run-${runId}.json"`);
243
+ res.json(exportData);
244
+ });
245
+ // === Webhooks ===
246
+ /**
247
+ * List registered webhooks.
248
+ */
249
+ router.get("/webhooks", (_req, res) => {
250
+ res.json({ webhooks: t.getWebhooks() });
251
+ });
252
+ /**
253
+ * Register a webhook.
254
+ * Body: { url: string, events?: string[], secret?: string }
255
+ */
256
+ router.post("/webhooks", (req, res) => {
257
+ const body = (req.body || {});
258
+ if (!body.url) {
259
+ res.status(400).json({ error: "Missing required field 'url'" });
260
+ return;
261
+ }
262
+ try {
263
+ new URL(body.url);
264
+ }
265
+ catch {
266
+ res.status(400).json({ error: "Invalid URL" });
267
+ return;
268
+ }
269
+ const webhook = t.registerWebhook({
270
+ url: body.url,
271
+ events: body.events || ["run.completed", "run.failed"],
272
+ secret: body.secret,
273
+ });
274
+ res.status(201).json(webhook);
275
+ });
276
+ /**
277
+ * Remove a webhook.
278
+ */
279
+ router.delete("/webhooks/:id", (req, res) => {
280
+ const { id } = req.params;
281
+ const removed = t.removeWebhook(id);
282
+ if (!removed) {
283
+ res.status(404).json({ error: `Webhook '${id}' not found` });
284
+ return;
285
+ }
286
+ res.json({ removed: true });
287
+ });
288
+ // === Run Endpoints ===
289
+ router.get("/runs", (req, res) => {
290
+ const workflow = req.query.workflow;
291
+ const status = req.query.status;
292
+ const tags = req.query.tags ? req.query.tags.split(",").map((t) => t.trim()) : undefined;
293
+ const limit = Number.parseInt(req.query.limit || "50", 10);
294
+ const offset = Number.parseInt(req.query.offset || "0", 10);
295
+ const sort = req.query.sort || "desc";
296
+ const result = t.getRuns({
297
+ workflow,
298
+ status: status,
299
+ tags,
300
+ limit,
301
+ offset,
302
+ sort,
303
+ });
304
+ res.json({
305
+ runs: result.runs,
306
+ total: result.total,
307
+ page: Math.floor(offset / limit) + 1,
308
+ });
309
+ });
310
+ router.get("/runs/:runId", (req, res) => {
311
+ const { runId } = req.params;
312
+ const run = t.getRun(runId);
313
+ if (!run) {
314
+ res.status(404).json({ error: `Run '${runId}' not found` });
315
+ return;
316
+ }
317
+ const nodes = t.getNodeRuns(runId);
318
+ const logs = t.getLogs(runId);
319
+ res.json({ run, nodes, logs });
320
+ });
321
+ router.get("/runs/:runId/events", (req, res) => {
322
+ const { runId } = req.params;
323
+ const since = req.query.since ? Number.parseInt(req.query.since, 10) : undefined;
324
+ const run = t.getRun(runId);
325
+ if (!run) {
326
+ res.status(404).json({ error: `Run '${runId}' not found` });
327
+ return;
328
+ }
329
+ const events = t.getEvents(runId, since);
330
+ res.json(events);
331
+ });
332
+ router.delete("/runs", (_req, res) => {
333
+ const deleted = t.clearAll();
334
+ res.json({ deleted });
335
+ });
336
+ // === Replay ===
337
+ /**
338
+ * Re-trigger a workflow by replaying a previous run.
339
+ * Makes an HTTP request to the original workflow endpoint.
340
+ */
341
+ router.post("/runs/:runId/replay", (req, res) => {
342
+ const { runId } = req.params;
343
+ const run = t.getRun(runId);
344
+ if (!run) {
345
+ res.status(404).json({ error: `Run '${runId}' not found` });
346
+ return;
347
+ }
348
+ if (run.triggerType !== "http") {
349
+ res.status(400).json({ error: `Replay is only supported for HTTP triggers (got '${run.triggerType}')` });
350
+ return;
351
+ }
352
+ // Parse method and path from triggerSummary (e.g. "GET /countries")
353
+ const parts = run.triggerSummary.split(" ");
354
+ const method = (parts[0] || "GET").toUpperCase();
355
+ const path = parts[1] || "/";
356
+ // Determine the host to call (use the incoming request's Host header)
357
+ const host = req.headers.host || "localhost:4000";
358
+ const protocol = "http";
359
+ const url = `${protocol}://${host}${path}`;
360
+ // Allow overriding method, path, headers, and body via request body
361
+ const overrides = (req.body || {});
362
+ const finalMethod = (overrides.method || method).toUpperCase();
363
+ const finalUrl = overrides.path ? `${protocol}://${host}${overrides.path}` : url;
364
+ const customHeaders = {
365
+ "Content-Type": "application/json",
366
+ ...(overrides.headers || {}),
367
+ };
368
+ const body = overrides.body !== undefined ? JSON.stringify(overrides.body) : undefined;
369
+ // Listen for the next RUN_STARTED event matching this workflow
370
+ const timeout = setTimeout(() => {
371
+ cleanup();
372
+ res.status(504).json({ error: "Replay timed out waiting for new run" });
373
+ }, 10000);
374
+ const cleanup = () => {
375
+ clearTimeout(timeout);
376
+ t.removeListener("RUN_STARTED", onRunStarted);
377
+ };
378
+ const onRunStarted = (event) => {
379
+ if (event.workflowName !== run.workflowName)
380
+ return;
381
+ cleanup();
382
+ res.json({
383
+ newRunId: event.runId,
384
+ originalRunId: runId,
385
+ workflowName: run.workflowName,
386
+ });
387
+ };
388
+ t.on("RUN_STARTED", onRunStarted);
389
+ // Make the HTTP request to re-trigger the workflow
390
+ const parsedUrl = new URL(finalUrl);
391
+ const reqOpts = {
392
+ hostname: parsedUrl.hostname,
393
+ port: parsedUrl.port,
394
+ path: parsedUrl.pathname + parsedUrl.search,
395
+ method: finalMethod,
396
+ headers: customHeaders,
397
+ };
398
+ const httpReq = http.request(reqOpts, (httpRes) => {
399
+ // Consume response body to prevent memory leaks
400
+ const chunks = [];
401
+ httpRes.on("data", (chunk) => chunks.push(chunk));
402
+ httpRes.on("end", () => {
403
+ // If we haven't already responded (via onRunStarted), respond now
404
+ // The RUN_STARTED listener should have fired before the response ends
405
+ });
406
+ });
407
+ httpReq.on("error", (err) => {
408
+ cleanup();
409
+ res.status(502).json({ error: `Replay request failed: ${err.message}` });
410
+ });
411
+ if (body) {
412
+ httpReq.write(body);
413
+ }
414
+ httpReq.end();
415
+ // Cleanup if client disconnects
416
+ req.on("close", cleanup);
417
+ });
418
+ // === AI Error Explanation ===
419
+ /**
420
+ * Explain a run or node error using an LLM.
421
+ * Requires OPENAI_API_KEY environment variable.
422
+ *
423
+ * POST /__blok/runs/:runId/explain
424
+ * Body: { nodeId?: string }
425
+ * Returns: { explanation: string, model: string }
426
+ */
427
+ router.post("/runs/:runId/explain", async (req, res) => {
428
+ const { runId } = req.params;
429
+ const run = t.getRun(runId);
430
+ if (!run) {
431
+ res.status(404).json({ error: `Run '${runId}' not found` });
432
+ return;
433
+ }
434
+ const apiKey = process.env.OPENAI_API_KEY;
435
+ if (!apiKey) {
436
+ res.status(503).json({
437
+ error: "AI explanation unavailable — set OPENAI_API_KEY environment variable",
438
+ });
439
+ return;
440
+ }
441
+ const body = (req.body || {});
442
+ const nodes = t.getNodeRuns(runId);
443
+ const logs = t.getLogs(runId);
444
+ // Build context for the LLM
445
+ let errorContext;
446
+ if (body.nodeId) {
447
+ const node = nodes.find((n) => n.id === body.nodeId);
448
+ if (!node) {
449
+ res.status(404).json({ error: `Node '${body.nodeId}' not found in run` });
450
+ return;
451
+ }
452
+ if (!node.error) {
453
+ res.status(400).json({ error: `Node '${node.nodeName}' has no error` });
454
+ return;
455
+ }
456
+ const nodeLogs = logs.filter((l) => l.nodeId === node.id || l.nodeName === node.nodeName);
457
+ errorContext = buildNodeErrorContext(run, node, nodes, nodeLogs);
458
+ }
459
+ else {
460
+ if (!run.error) {
461
+ res.status(400).json({ error: "This run has no error to explain" });
462
+ return;
463
+ }
464
+ const failedNodes = nodes.filter((n) => n.status === "failed");
465
+ errorContext = buildRunErrorContext(run, nodes, failedNodes, logs);
466
+ }
467
+ try {
468
+ const model = process.env.BLOK_AI_MODEL || "gpt-4o-mini";
469
+ const explanation = await callOpenAI(apiKey, model, errorContext);
470
+ res.json({ explanation, model });
471
+ }
472
+ catch (err) {
473
+ const msg = err instanceof Error ? err.message : "Unknown AI API error";
474
+ res.status(502).json({ error: `AI explanation failed: ${msg}` });
475
+ }
476
+ });
477
+ // === Search ===
478
+ /**
479
+ * Search across workflows and runs.
480
+ * Used by the command palette (Cmd+K).
481
+ */
482
+ router.get("/search", (req, res) => {
483
+ const query = (req.query.q || "").toLowerCase().trim();
484
+ if (!query) {
485
+ res.json({ workflows: [], runs: [] });
486
+ return;
487
+ }
488
+ // Search workflows
489
+ const allWorkflows = t.getWorkflowSummaries();
490
+ const matchedWorkflows = allWorkflows.filter((w) => w.name.toLowerCase().includes(query) ||
491
+ w.path.toLowerCase().includes(query) ||
492
+ w.triggerTypes.some((tt) => tt.toLowerCase().includes(query)));
493
+ // Search runs (by ID, workflow name, trigger summary, or error message)
494
+ const { runs: allRuns } = t.getRuns({ limit: 200 });
495
+ const matchedRuns = allRuns
496
+ .filter((r) => r.id.toLowerCase().includes(query) ||
497
+ r.workflowName.toLowerCase().includes(query) ||
498
+ r.triggerSummary.toLowerCase().includes(query) ||
499
+ r.error?.message.toLowerCase().includes(query) ||
500
+ r.status.toLowerCase().includes(query))
501
+ .slice(0, 20);
502
+ res.json({
503
+ workflows: matchedWorkflows.slice(0, 10),
504
+ runs: matchedRuns,
505
+ });
506
+ });
507
+ // === Custom Dashboards ===
508
+ /**
509
+ * List all dashboards.
510
+ * GET /__blok/dashboards
511
+ */
512
+ router.get("/dashboards", (_req, res) => {
513
+ const dashboards = t.listDashboards();
514
+ res.json({ dashboards });
515
+ });
516
+ /**
517
+ * Get a single dashboard by ID.
518
+ * GET /__blok/dashboards/:dashboardId
519
+ */
520
+ router.get("/dashboards/:dashboardId", (req, res) => {
521
+ const dashboard = t.getDashboard(req.params.dashboardId);
522
+ if (!dashboard) {
523
+ res.status(404).json({ error: `Dashboard '${req.params.dashboardId}' not found` });
524
+ return;
525
+ }
526
+ res.json(dashboard);
527
+ });
528
+ /**
529
+ * Create a new dashboard.
530
+ * POST /__blok/dashboards
531
+ * Body: { name, description?, widgets?, isDefault? }
532
+ */
533
+ router.post("/dashboards", (req, res) => {
534
+ const body = (req.body || {});
535
+ if (!body.name || typeof body.name !== "string" || body.name.trim().length === 0) {
536
+ res.status(400).json({ error: "Dashboard name is required" });
537
+ return;
538
+ }
539
+ const now = Date.now();
540
+ const dashboard = {
541
+ id: `dash_${now.toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
542
+ name: body.name.trim(),
543
+ description: body.description,
544
+ isDefault: body.isDefault ?? false,
545
+ createdAt: now,
546
+ updatedAt: now,
547
+ widgets: Array.isArray(body.widgets) ? body.widgets : [],
548
+ };
549
+ t.saveDashboard(dashboard);
550
+ res.status(201).json(dashboard);
551
+ });
552
+ /**
553
+ * Update an existing dashboard.
554
+ * PUT /__blok/dashboards/:dashboardId
555
+ * Body: { name?, description?, widgets?, isDefault? }
556
+ */
557
+ router.put("/dashboards/:dashboardId", (req, res) => {
558
+ const { dashboardId } = req.params;
559
+ const existing = t.getDashboard(dashboardId);
560
+ if (!existing) {
561
+ res.status(404).json({ error: `Dashboard '${dashboardId}' not found` });
562
+ return;
563
+ }
564
+ const body = (req.body || {});
565
+ t.updateDashboard(dashboardId, body);
566
+ const updated = t.getDashboard(dashboardId);
567
+ res.json(updated);
568
+ });
569
+ /**
570
+ * Delete a dashboard.
571
+ * DELETE /__blok/dashboards/:dashboardId
572
+ */
573
+ router.delete("/dashboards/:dashboardId", (req, res) => {
574
+ const deleted = t.deleteDashboard(req.params.dashboardId);
575
+ if (!deleted) {
576
+ res.status(404).json({ error: `Dashboard '${req.params.dashboardId}' not found` });
577
+ return;
578
+ }
579
+ res.json({ deleted: true });
580
+ });
581
+ /**
582
+ * Duplicate a dashboard.
583
+ * POST /__blok/dashboards/:dashboardId/duplicate
584
+ */
585
+ router.post("/dashboards/:dashboardId/duplicate", (req, res) => {
586
+ const source = t.getDashboard(req.params.dashboardId);
587
+ if (!source) {
588
+ res.status(404).json({ error: `Dashboard '${req.params.dashboardId}' not found` });
589
+ return;
590
+ }
591
+ const now = Date.now();
592
+ const copy = {
593
+ ...source,
594
+ id: `dash_${now.toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
595
+ name: `${source.name} (Copy)`,
596
+ isDefault: false,
597
+ createdAt: now,
598
+ updatedAt: now,
599
+ };
600
+ t.saveDashboard(copy);
601
+ res.status(201).json(copy);
602
+ });
603
+ // === SSE Endpoints ===
604
+ /**
605
+ * SSE stream for a specific run.
606
+ * Sends all past events as a replay, then streams new events live.
607
+ * Auto-closes when the run finishes.
608
+ */
609
+ router.get("/runs/:runId/stream", (req, res) => {
610
+ const { runId } = req.params;
611
+ const run = t.getRun(runId);
612
+ if (!run) {
613
+ res.status(404).json({ error: `Run '${runId}' not found` });
614
+ return;
615
+ }
616
+ // Set SSE headers
617
+ res.setHeader("Content-Type", "text/event-stream");
618
+ res.setHeader("Cache-Control", "no-cache");
619
+ res.setHeader("Connection", "keep-alive");
620
+ res.setHeader("X-Accel-Buffering", "no"); // Disable nginx buffering
621
+ res.flushHeaders();
622
+ // Immediate acknowledgment so the browser fires onopen without waiting
623
+ res.write(`event: connected\ndata: ${JSON.stringify({ runId, timestamp: Date.now() })}\n\n`);
624
+ res.write("retry: 3000\n\n");
625
+ // Replay past events (respecting Last-Event-ID for reconnection).
626
+ // Cap fresh connections to last 50 events to avoid blocking the stream.
627
+ // The client fetches full run state via GET /runs/:runId.
628
+ const MAX_REPLAY_EVENTS = 50;
629
+ const lastEventId = req.headers["last-event-id"];
630
+ const existingEvents = t.getEvents(runId);
631
+ let eventsToReplay;
632
+ if (lastEventId) {
633
+ // Reconnection — replay all events since the last received (uncapped)
634
+ const idx = existingEvents.findIndex((e) => e.id === lastEventId);
635
+ eventsToReplay = idx >= 0 ? existingEvents.slice(idx + 1) : existingEvents;
636
+ }
637
+ else {
638
+ // Fresh connection — only replay the most recent events
639
+ eventsToReplay =
640
+ existingEvents.length > MAX_REPLAY_EVENTS ? existingEvents.slice(-MAX_REPLAY_EVENTS) : existingEvents;
641
+ }
642
+ for (const event of eventsToReplay) {
643
+ writeSSE(res, event);
644
+ }
645
+ // If run already finished, close stream
646
+ if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
647
+ res.write('event: stream-end\ndata: {"reason":"run_finished"}\n\n');
648
+ res.end();
649
+ return;
650
+ }
651
+ // Stream live events
652
+ const onEvent = (event) => {
653
+ if (event.runId !== runId)
654
+ return;
655
+ writeSSE(res, event);
656
+ // Auto-close when run finishes
657
+ if (event.type === "RUN_COMPLETED" || event.type === "RUN_FAILED") {
658
+ res.write('event: stream-end\ndata: {"reason":"run_finished"}\n\n');
659
+ res.end();
660
+ }
661
+ };
662
+ t.on("event", onEvent);
663
+ // Heartbeat to keep connection alive
664
+ const heartbeat = setInterval(() => {
665
+ res.write(":heartbeat\n\n");
666
+ }, 5000);
667
+ // Cleanup on disconnect
668
+ req.on("close", () => {
669
+ t.removeListener("event", onEvent);
670
+ clearInterval(heartbeat);
671
+ });
672
+ });
673
+ /**
674
+ * Global SSE stream for all run events (dashboard live feed).
675
+ * Optionally filtered by workflow names.
676
+ */
677
+ router.get("/stream", (req, res) => {
678
+ const workflowFilter = req.query.workflows ? req.query.workflows.split(",").map((w) => w.trim()) : null;
679
+ // Set SSE headers
680
+ res.setHeader("Content-Type", "text/event-stream");
681
+ res.setHeader("Cache-Control", "no-cache");
682
+ res.setHeader("Connection", "keep-alive");
683
+ res.setHeader("X-Accel-Buffering", "no");
684
+ res.flushHeaders();
685
+ // Immediate acknowledgment so the browser fires onopen without waiting
686
+ res.write(`event: connected\ndata: ${JSON.stringify({ timestamp: Date.now() })}\n\n`);
687
+ res.write("retry: 3000\n\n");
688
+ const onEvent = (event) => {
689
+ if (workflowFilter && !workflowFilter.includes(event.workflowName))
690
+ return;
691
+ writeSSE(res, event);
692
+ };
693
+ t.on("event", onEvent);
694
+ // Heartbeat
695
+ const heartbeat = setInterval(() => {
696
+ res.write(":heartbeat\n\n");
697
+ }, 5000);
698
+ req.on("close", () => {
699
+ t.removeListener("event", onEvent);
700
+ clearInterval(heartbeat);
701
+ });
702
+ });
703
+ }
704
+ function writeSSE(res, event) {
705
+ res.write(`event: ${event.type}\n`);
706
+ res.write(`id: ${event.id}\n`);
707
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
708
+ }
709
+ // === CSV Helpers ===
710
+ function escapeCsv(value) {
711
+ if (value === null || value === undefined)
712
+ return "";
713
+ const str = typeof value === "object" ? JSON.stringify(value) : String(value);
714
+ if (str.includes(",") || str.includes('"') || str.includes("\n")) {
715
+ return `"${str.replace(/"/g, '""')}"`;
716
+ }
717
+ return str;
718
+ }
719
+ function runsToCsv(runs) {
720
+ const headers = [
721
+ "id",
722
+ "workflowName",
723
+ "workflowPath",
724
+ "triggerType",
725
+ "triggerSummary",
726
+ "status",
727
+ "startedAt",
728
+ "finishedAt",
729
+ "durationMs",
730
+ "nodeCount",
731
+ "completedNodes",
732
+ "error",
733
+ "tags",
734
+ ];
735
+ const rows = runs.map((r) => [
736
+ r.id,
737
+ r.workflowName,
738
+ r.workflowPath,
739
+ r.triggerType,
740
+ r.triggerSummary,
741
+ r.status,
742
+ new Date(r.startedAt).toISOString(),
743
+ r.finishedAt ? new Date(r.finishedAt).toISOString() : "",
744
+ r.durationMs ?? "",
745
+ r.nodeCount,
746
+ r.completedNodes,
747
+ r.error?.message ?? "",
748
+ (r.tags || []).join(";"),
749
+ ]);
750
+ return `${[headers.join(","), ...rows.map((row) => row.map(escapeCsv).join(","))].join("\n")}\n`;
751
+ }
752
+ function singleRunToCsv(run, nodes, logs) {
753
+ let csv = "# Run Summary\n";
754
+ csv +=
755
+ "id,workflowName,triggerType,triggerSummary,status,startedAt,finishedAt,durationMs,nodeCount,completedNodes,error\n";
756
+ csv += `${[
757
+ run.id,
758
+ run.workflowName,
759
+ run.triggerType,
760
+ run.triggerSummary,
761
+ run.status,
762
+ new Date(run.startedAt).toISOString(),
763
+ run.finishedAt ? new Date(run.finishedAt).toISOString() : "",
764
+ run.durationMs ?? "",
765
+ run.nodeCount,
766
+ run.completedNodes,
767
+ run.error?.message ?? "",
768
+ ]
769
+ .map(escapeCsv)
770
+ .join(",")}\n`;
771
+ csv += "\n# Nodes\n";
772
+ csv += "id,nodeName,nodeType,runtimeKind,status,startedAt,finishedAt,durationMs,stepIndex,depth,error\n";
773
+ for (const n of nodes) {
774
+ csv += `${[
775
+ n.id,
776
+ n.nodeName,
777
+ n.nodeType,
778
+ n.runtimeKind ?? "",
779
+ n.status,
780
+ new Date(n.startedAt).toISOString(),
781
+ n.finishedAt ? new Date(n.finishedAt).toISOString() : "",
782
+ n.durationMs ?? "",
783
+ n.stepIndex,
784
+ n.depth,
785
+ n.error?.message ?? "",
786
+ ]
787
+ .map(escapeCsv)
788
+ .join(",")}\n`;
789
+ }
790
+ csv += "\n# Logs\n";
791
+ csv += "id,nodeName,level,message,timestamp\n";
792
+ for (const l of logs) {
793
+ csv += `${[l.id, l.nodeName ?? "", l.level, l.message, new Date(l.timestamp).toISOString()]
794
+ .map(escapeCsv)
795
+ .join(",")}\n`;
796
+ }
797
+ return csv;
798
+ }
799
+ // === AI Error Explanation Helpers ===
800
+ function buildNodeErrorContext(run, node, allNodes, nodeLogs) {
801
+ const timeline = allNodes
802
+ .sort((a, b) => a.stepIndex - b.stepIndex)
803
+ .map((n) => ` [${n.stepIndex}] ${n.nodeName} (${n.nodeType}${n.runtimeKind ? `, ${n.runtimeKind}` : ""}) → ${n.status}${n.durationMs ? ` (${n.durationMs}ms)` : ""}`)
804
+ .join("\n");
805
+ const logLines = nodeLogs
806
+ .slice(-20)
807
+ .map((l) => ` [${l.level.toUpperCase()}] ${l.message}`)
808
+ .join("\n");
809
+ return `You are a workflow debugging assistant. A node failed during a Blok workflow execution. Analyze the error and provide:
810
+ 1. A clear explanation of what went wrong
811
+ 2. The likely root cause
812
+ 3. Suggested fixes
813
+
814
+ ## Workflow Context
815
+ - Workflow: ${run.workflowName} (${run.workflowPath})
816
+ - Trigger: ${run.triggerSummary}
817
+ - Status: ${run.status}
818
+
819
+ ## Node Execution Timeline
820
+ ${timeline}
821
+
822
+ ## Failed Node Details
823
+ - Name: ${node.nodeName}
824
+ - Type: ${node.nodeType}${node.runtimeKind ? `\n- Runtime: ${node.runtimeKind}` : ""}
825
+ - Step Index: ${node.stepIndex}
826
+ - Duration: ${node.durationMs ?? "N/A"}ms
827
+
828
+ ## Error
829
+ - Message: ${node.error?.message ?? "Unknown"}${node.error?.code ? `\n- Code: ${node.error.code}` : ""}${node.error?.stack ? `\n- Stack Trace:\n${node.error.stack}` : ""}
830
+
831
+ ## Node Input
832
+ ${node.inputs ? JSON.stringify(node.inputs, null, 2).slice(0, 2000) : "N/A"}
833
+
834
+ ## Node Output (before failure)
835
+ ${node.outputs ? JSON.stringify(node.outputs, null, 2).slice(0, 2000) : "N/A"}
836
+
837
+ ${logLines ? `## Node Logs (last 20)\n${logLines}` : ""}
838
+
839
+ Provide a concise, actionable explanation. Focus on the root cause and how to fix it.`;
840
+ }
841
+ function buildRunErrorContext(run, allNodes, failedNodes, logs) {
842
+ const timeline = allNodes
843
+ .sort((a, b) => a.stepIndex - b.stepIndex)
844
+ .map((n) => ` [${n.stepIndex}] ${n.nodeName} (${n.nodeType}${n.runtimeKind ? `, ${n.runtimeKind}` : ""}) → ${n.status}${n.durationMs ? ` (${n.durationMs}ms)` : ""}`)
845
+ .join("\n");
846
+ const failedDetails = failedNodes
847
+ .map((n) => `### ${n.nodeName}\n- Error: ${n.error?.message || "Unknown"}\n${n.error?.stack ? `- Stack: ${n.error.stack.split("\n").slice(0, 5).join("\n")}` : ""}${n.inputs ? `\n- Input: ${JSON.stringify(n.inputs, null, 2).slice(0, 500)}` : ""}`)
848
+ .join("\n\n");
849
+ const errorLogs = logs
850
+ .filter((l) => l.level === "error" || l.level === "warn")
851
+ .slice(-15)
852
+ .map((l) => ` [${l.level.toUpperCase()}]${l.nodeName ? ` (${l.nodeName})` : ""} ${l.message}`)
853
+ .join("\n");
854
+ return `You are a workflow debugging assistant. A Blok workflow execution failed. Analyze the error and provide:
855
+ 1. A clear explanation of what went wrong
856
+ 2. The likely root cause
857
+ 3. Suggested fixes
858
+
859
+ ## Workflow Context
860
+ - Workflow: ${run.workflowName} (${run.workflowPath})
861
+ - Trigger: ${run.triggerSummary}
862
+ - Duration: ${run.durationMs ?? "N/A"}ms
863
+ - Nodes: ${run.completedNodes}/${run.nodeCount} completed
864
+
865
+ ## Run Error
866
+ - Message: ${run.error?.message ?? "Unknown"}${run.error?.code ? `\n- Code: ${run.error.code}` : ""}${run.error?.stack ? `\n- Stack Trace:\n${run.error.stack}` : ""}
867
+
868
+ ## Node Execution Timeline
869
+ ${timeline}
870
+
871
+ ${failedDetails ? `## Failed Nodes\n${failedDetails}` : ""}
872
+
873
+ ${errorLogs ? `## Error/Warning Logs\n${errorLogs}` : ""}
874
+
875
+ Provide a concise, actionable explanation. Focus on the root cause and how to fix it.`;
876
+ }
877
+ async function callOpenAI(apiKey, model, prompt) {
878
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
879
+ method: "POST",
880
+ headers: {
881
+ "Content-Type": "application/json",
882
+ Authorization: `Bearer ${apiKey}`,
883
+ },
884
+ body: JSON.stringify({
885
+ model,
886
+ messages: [
887
+ {
888
+ role: "system",
889
+ content: "You are an expert workflow debugging assistant for Blok, a workflow orchestration framework. Provide concise, actionable debugging advice. Use markdown formatting for readability.",
890
+ },
891
+ { role: "user", content: prompt },
892
+ ],
893
+ temperature: 0.3,
894
+ max_tokens: 1500,
895
+ }),
896
+ });
897
+ if (!response.ok) {
898
+ const err = await response.json().catch(() => ({}));
899
+ throw new Error(err.error?.message || `OpenAI API returned ${response.status}`);
900
+ }
901
+ const data = (await response.json());
902
+ return data.choices[0]?.message?.content || "No explanation generated.";
903
+ }
904
+ //# sourceMappingURL=TraceRouter.js.map