elasticdash-sdk 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 (349) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +775 -0
  3. package/dist/browser-ui.d.ts +43 -0
  4. package/dist/browser-ui.d.ts.map +1 -0
  5. package/dist/browser-ui.js +246 -0
  6. package/dist/browser-ui.js.map +1 -0
  7. package/dist/capture/event.d.ts +33 -0
  8. package/dist/capture/event.d.ts.map +1 -0
  9. package/dist/capture/event.js +2 -0
  10. package/dist/capture/event.js.map +1 -0
  11. package/dist/capture/index.d.ts +4 -0
  12. package/dist/capture/index.d.ts.map +1 -0
  13. package/dist/capture/index.js +4 -0
  14. package/dist/capture/index.js.map +1 -0
  15. package/dist/capture/recorder.d.ts +24 -0
  16. package/dist/capture/recorder.d.ts.map +1 -0
  17. package/dist/capture/recorder.js +46 -0
  18. package/dist/capture/recorder.js.map +1 -0
  19. package/dist/capture/replay.d.ts +20 -0
  20. package/dist/capture/replay.d.ts.map +1 -0
  21. package/dist/capture/replay.js +47 -0
  22. package/dist/capture/replay.js.map +1 -0
  23. package/dist/ci/api-client.d.ts +38 -0
  24. package/dist/ci/api-client.d.ts.map +1 -0
  25. package/dist/ci/api-client.js +96 -0
  26. package/dist/ci/api-client.js.map +1 -0
  27. package/dist/ci/benchmark.d.ts +33 -0
  28. package/dist/ci/benchmark.d.ts.map +1 -0
  29. package/dist/ci/benchmark.js +213 -0
  30. package/dist/ci/benchmark.js.map +1 -0
  31. package/dist/ci/ed-runner.d.ts +48 -0
  32. package/dist/ci/ed-runner.d.ts.map +1 -0
  33. package/dist/ci/ed-runner.js +260 -0
  34. package/dist/ci/ed-runner.js.map +1 -0
  35. package/dist/ci/executor.d.ts +13 -0
  36. package/dist/ci/executor.d.ts.map +1 -0
  37. package/dist/ci/executor.js +542 -0
  38. package/dist/ci/executor.js.map +1 -0
  39. package/dist/ci/git-info.d.ts +17 -0
  40. package/dist/ci/git-info.d.ts.map +1 -0
  41. package/dist/ci/git-info.js +102 -0
  42. package/dist/ci/git-info.js.map +1 -0
  43. package/dist/ci/index.d.ts +6 -0
  44. package/dist/ci/index.d.ts.map +1 -0
  45. package/dist/ci/index.js +4 -0
  46. package/dist/ci/index.js.map +1 -0
  47. package/dist/ci/measurement.d.ts +9 -0
  48. package/dist/ci/measurement.d.ts.map +1 -0
  49. package/dist/ci/measurement.js +15 -0
  50. package/dist/ci/measurement.js.map +1 -0
  51. package/dist/ci/replay.d.ts +31 -0
  52. package/dist/ci/replay.d.ts.map +1 -0
  53. package/dist/ci/replay.js +96 -0
  54. package/dist/ci/replay.js.map +1 -0
  55. package/dist/ci/reporters/default.d.ts +8 -0
  56. package/dist/ci/reporters/default.d.ts.map +1 -0
  57. package/dist/ci/reporters/default.js +46 -0
  58. package/dist/ci/reporters/default.js.map +1 -0
  59. package/dist/ci/reporters/index.d.ts +8 -0
  60. package/dist/ci/reporters/index.d.ts.map +1 -0
  61. package/dist/ci/reporters/index.js +14 -0
  62. package/dist/ci/reporters/index.js.map +1 -0
  63. package/dist/ci/reporters/json.d.ts +8 -0
  64. package/dist/ci/reporters/json.d.ts.map +1 -0
  65. package/dist/ci/reporters/json.js +14 -0
  66. package/dist/ci/reporters/json.js.map +1 -0
  67. package/dist/ci/reporters/junit.d.ts +8 -0
  68. package/dist/ci/reporters/junit.d.ts.map +1 -0
  69. package/dist/ci/reporters/junit.js +48 -0
  70. package/dist/ci/reporters/junit.js.map +1 -0
  71. package/dist/ci/runner.d.ts +3 -0
  72. package/dist/ci/runner.d.ts.map +1 -0
  73. package/dist/ci/runner.js +187 -0
  74. package/dist/ci/runner.js.map +1 -0
  75. package/dist/ci/test-discovery.d.ts +5 -0
  76. package/dist/ci/test-discovery.d.ts.map +1 -0
  77. package/dist/ci/test-discovery.js +11 -0
  78. package/dist/ci/test-discovery.js.map +1 -0
  79. package/dist/ci/test-loader.d.ts +19 -0
  80. package/dist/ci/test-loader.d.ts.map +1 -0
  81. package/dist/ci/test-loader.js +149 -0
  82. package/dist/ci/test-loader.js.map +1 -0
  83. package/dist/ci/test-registry.d.ts +42 -0
  84. package/dist/ci/test-registry.d.ts.map +1 -0
  85. package/dist/ci/test-registry.js +18 -0
  86. package/dist/ci/test-registry.js.map +1 -0
  87. package/dist/ci/trace-schema.d.ts +30 -0
  88. package/dist/ci/trace-schema.d.ts.map +1 -0
  89. package/dist/ci/trace-schema.js +66 -0
  90. package/dist/ci/trace-schema.js.map +1 -0
  91. package/dist/ci/trace-writer.d.ts +16 -0
  92. package/dist/ci/trace-writer.d.ts.map +1 -0
  93. package/dist/ci/trace-writer.js +108 -0
  94. package/dist/ci/trace-writer.js.map +1 -0
  95. package/dist/ci/types.d.ts +108 -0
  96. package/dist/ci/types.d.ts.map +1 -0
  97. package/dist/ci/types.js +3 -0
  98. package/dist/ci/types.js.map +1 -0
  99. package/dist/ci/upload-client.d.ts +74 -0
  100. package/dist/ci/upload-client.d.ts.map +1 -0
  101. package/dist/ci/upload-client.js +195 -0
  102. package/dist/ci/upload-client.js.map +1 -0
  103. package/dist/cli.d.ts +3 -0
  104. package/dist/cli.d.ts.map +1 -0
  105. package/dist/cli.js +716 -0
  106. package/dist/cli.js.map +1 -0
  107. package/dist/core/agent-state.d.ts +47 -0
  108. package/dist/core/agent-state.d.ts.map +1 -0
  109. package/dist/core/agent-state.js +137 -0
  110. package/dist/core/agent-state.js.map +1 -0
  111. package/dist/core/judge-utils.d.ts +22 -0
  112. package/dist/core/judge-utils.d.ts.map +1 -0
  113. package/dist/core/judge-utils.js +211 -0
  114. package/dist/core/judge-utils.js.map +1 -0
  115. package/dist/core/registry.d.ts +28 -0
  116. package/dist/core/registry.d.ts.map +1 -0
  117. package/dist/core/registry.js +52 -0
  118. package/dist/core/registry.js.map +1 -0
  119. package/dist/dashboard-server.d.ts +65 -0
  120. package/dist/dashboard-server.d.ts.map +1 -0
  121. package/dist/dashboard-server.js +3940 -0
  122. package/dist/dashboard-server.js.map +1 -0
  123. package/dist/execution/tool-runner.d.ts +26 -0
  124. package/dist/execution/tool-runner.d.ts.map +1 -0
  125. package/dist/execution/tool-runner.js +316 -0
  126. package/dist/execution/tool-runner.js.map +1 -0
  127. package/dist/html/dashboard.html +2218 -0
  128. package/dist/http.d.ts +14 -0
  129. package/dist/http.d.ts.map +1 -0
  130. package/dist/http.js +13 -0
  131. package/dist/http.js.map +1 -0
  132. package/dist/index.cjs +8102 -0
  133. package/dist/index.d.ts +61 -0
  134. package/dist/index.d.ts.map +1 -0
  135. package/dist/index.js +67 -0
  136. package/dist/index.js.map +1 -0
  137. package/dist/interceptors/ai-interceptor.d.ts +26 -0
  138. package/dist/interceptors/ai-interceptor.d.ts.map +1 -0
  139. package/dist/interceptors/ai-interceptor.js +756 -0
  140. package/dist/interceptors/ai-interceptor.js.map +1 -0
  141. package/dist/interceptors/db-auto.d.ts +8 -0
  142. package/dist/interceptors/db-auto.d.ts.map +1 -0
  143. package/dist/interceptors/db-auto.js +217 -0
  144. package/dist/interceptors/db-auto.js.map +1 -0
  145. package/dist/interceptors/db.d.ts +23 -0
  146. package/dist/interceptors/db.d.ts.map +1 -0
  147. package/dist/interceptors/db.js +137 -0
  148. package/dist/interceptors/db.js.map +1 -0
  149. package/dist/interceptors/http.d.ts +28 -0
  150. package/dist/interceptors/http.d.ts.map +1 -0
  151. package/dist/interceptors/http.js +356 -0
  152. package/dist/interceptors/http.js.map +1 -0
  153. package/dist/interceptors/side-effects.d.ts +7 -0
  154. package/dist/interceptors/side-effects.d.ts.map +1 -0
  155. package/dist/interceptors/side-effects.js +72 -0
  156. package/dist/interceptors/side-effects.js.map +1 -0
  157. package/dist/interceptors/telemetry-push.d.ts +142 -0
  158. package/dist/interceptors/telemetry-push.d.ts.map +1 -0
  159. package/dist/interceptors/telemetry-push.js +463 -0
  160. package/dist/interceptors/telemetry-push.js.map +1 -0
  161. package/dist/interceptors/tool.d.ts +2 -0
  162. package/dist/interceptors/tool.d.ts.map +1 -0
  163. package/dist/interceptors/tool.js +274 -0
  164. package/dist/interceptors/tool.js.map +1 -0
  165. package/dist/interceptors/workflow-ai.d.ts +5 -0
  166. package/dist/interceptors/workflow-ai.d.ts.map +1 -0
  167. package/dist/interceptors/workflow-ai.js +382 -0
  168. package/dist/interceptors/workflow-ai.js.map +1 -0
  169. package/dist/internals/conditional-recorder.d.ts +21 -0
  170. package/dist/internals/conditional-recorder.d.ts.map +1 -0
  171. package/dist/internals/conditional-recorder.js +54 -0
  172. package/dist/internals/conditional-recorder.js.map +1 -0
  173. package/dist/internals/mock-resolver.d.ts +146 -0
  174. package/dist/internals/mock-resolver.d.ts.map +1 -0
  175. package/dist/internals/mock-resolver.js +427 -0
  176. package/dist/internals/mock-resolver.js.map +1 -0
  177. package/dist/matchers/index.d.ts +96 -0
  178. package/dist/matchers/index.d.ts.map +1 -0
  179. package/dist/matchers/index.js +668 -0
  180. package/dist/matchers/index.js.map +1 -0
  181. package/dist/observability.d.ts +82 -0
  182. package/dist/observability.d.ts.map +1 -0
  183. package/dist/observability.js +471 -0
  184. package/dist/observability.js.map +1 -0
  185. package/dist/portal-executor.d.ts +30 -0
  186. package/dist/portal-executor.d.ts.map +1 -0
  187. package/dist/portal-executor.js +324 -0
  188. package/dist/portal-executor.js.map +1 -0
  189. package/dist/portal-server.d.ts +3 -0
  190. package/dist/portal-server.d.ts.map +1 -0
  191. package/dist/portal-server.js +279 -0
  192. package/dist/portal-server.js.map +1 -0
  193. package/dist/proxy/llm-capture.d.ts +14 -0
  194. package/dist/proxy/llm-capture.d.ts.map +1 -0
  195. package/dist/proxy/llm-capture.js +264 -0
  196. package/dist/proxy/llm-capture.js.map +1 -0
  197. package/dist/reporter.d.ts +3 -0
  198. package/dist/reporter.d.ts.map +1 -0
  199. package/dist/reporter.js +72 -0
  200. package/dist/reporter.js.map +1 -0
  201. package/dist/runWorkflowSubprocess.d.ts +14 -0
  202. package/dist/runWorkflowSubprocess.d.ts.map +1 -0
  203. package/dist/runWorkflowSubprocess.js +66 -0
  204. package/dist/runWorkflowSubprocess.js.map +1 -0
  205. package/dist/runner.d.ts +16 -0
  206. package/dist/runner.d.ts.map +1 -0
  207. package/dist/runner.js +138 -0
  208. package/dist/runner.js.map +1 -0
  209. package/dist/socket-connector.d.ts +22 -0
  210. package/dist/socket-connector.d.ts.map +1 -0
  211. package/dist/socket-connector.js +104 -0
  212. package/dist/socket-connector.js.map +1 -0
  213. package/dist/telemetry-batcher.d.ts +56 -0
  214. package/dist/telemetry-batcher.d.ts.map +1 -0
  215. package/dist/telemetry-batcher.js +143 -0
  216. package/dist/telemetry-batcher.js.map +1 -0
  217. package/dist/test-setup.d.ts +12 -0
  218. package/dist/test-setup.d.ts.map +1 -0
  219. package/dist/test-setup.js +13 -0
  220. package/dist/test-setup.js.map +1 -0
  221. package/dist/tool-registry.d.ts +31 -0
  222. package/dist/tool-registry.d.ts.map +1 -0
  223. package/dist/tool-registry.js +73 -0
  224. package/dist/tool-registry.js.map +1 -0
  225. package/dist/tool-runner-worker.d.ts +2 -0
  226. package/dist/tool-runner-worker.d.ts.map +1 -0
  227. package/dist/tool-runner-worker.js +215 -0
  228. package/dist/tool-runner-worker.js.map +1 -0
  229. package/dist/trace-adapter/context.d.ts +72 -0
  230. package/dist/trace-adapter/context.d.ts.map +1 -0
  231. package/dist/trace-adapter/context.js +80 -0
  232. package/dist/trace-adapter/context.js.map +1 -0
  233. package/dist/tracing.d.ts +2 -0
  234. package/dist/tracing.d.ts.map +1 -0
  235. package/dist/tracing.js +59 -0
  236. package/dist/tracing.js.map +1 -0
  237. package/dist/trigger-executor.d.ts +12 -0
  238. package/dist/trigger-executor.d.ts.map +1 -0
  239. package/dist/trigger-executor.js +130 -0
  240. package/dist/trigger-executor.js.map +1 -0
  241. package/dist/types/portal.d.ts +76 -0
  242. package/dist/types/portal.d.ts.map +1 -0
  243. package/dist/types/portal.js +2 -0
  244. package/dist/types/portal.js.map +1 -0
  245. package/dist/utils/debug.d.ts +3 -0
  246. package/dist/utils/debug.d.ts.map +1 -0
  247. package/dist/utils/debug.js +8 -0
  248. package/dist/utils/debug.js.map +1 -0
  249. package/dist/utils/license-error.d.ts +23 -0
  250. package/dist/utils/license-error.d.ts.map +1 -0
  251. package/dist/utils/license-error.js +42 -0
  252. package/dist/utils/license-error.js.map +1 -0
  253. package/dist/utils/redact.d.ts +7 -0
  254. package/dist/utils/redact.d.ts.map +1 -0
  255. package/dist/utils/redact.js +26 -0
  256. package/dist/utils/redact.js.map +1 -0
  257. package/dist/workflow-runner-worker.d.ts +2 -0
  258. package/dist/workflow-runner-worker.d.ts.map +1 -0
  259. package/dist/workflow-runner-worker.js +329 -0
  260. package/dist/workflow-runner-worker.js.map +1 -0
  261. package/dist/workflow-runner.d.ts +14 -0
  262. package/dist/workflow-runner.d.ts.map +1 -0
  263. package/dist/workflow-runner.js +34 -0
  264. package/dist/workflow-runner.js.map +1 -0
  265. package/docs/agent-coding-instructions.md +138 -0
  266. package/docs/agent-integration-guide.md +564 -0
  267. package/docs/agents.md +140 -0
  268. package/docs/dashboard.md +394 -0
  269. package/docs/deno.md +69 -0
  270. package/docs/instrumentation.md +424 -0
  271. package/docs/langfuse-trace-structure.md +145 -0
  272. package/docs/matchers.md +173 -0
  273. package/docs/observability_contract.md +192 -0
  274. package/docs/observability_mode.md +195 -0
  275. package/docs/quickstart.md +621 -0
  276. package/docs/security-compliance.md +566 -0
  277. package/docs/test-writing-guidelines.md +444 -0
  278. package/docs/tools.md +165 -0
  279. package/docs/workflow-modes.md +253 -0
  280. package/package.json +76 -0
  281. package/src/browser-ui.ts +281 -0
  282. package/src/capture/event.ts +30 -0
  283. package/src/capture/index.ts +3 -0
  284. package/src/capture/recorder.ts +62 -0
  285. package/src/capture/replay.ts +55 -0
  286. package/src/ci/api-client.ts +136 -0
  287. package/src/ci/benchmark.ts +257 -0
  288. package/src/ci/ed-runner.ts +351 -0
  289. package/src/ci/executor.ts +671 -0
  290. package/src/ci/git-info.ts +127 -0
  291. package/src/ci/index.ts +5 -0
  292. package/src/ci/measurement.ts +25 -0
  293. package/src/ci/replay.ts +127 -0
  294. package/src/ci/reporters/default.ts +50 -0
  295. package/src/ci/reporters/index.ts +21 -0
  296. package/src/ci/reporters/json.ts +18 -0
  297. package/src/ci/reporters/junit.ts +61 -0
  298. package/src/ci/runner.ts +208 -0
  299. package/src/ci/test-discovery.ts +16 -0
  300. package/src/ci/test-loader.ts +187 -0
  301. package/src/ci/test-registry.ts +62 -0
  302. package/src/ci/trace-schema.ts +96 -0
  303. package/src/ci/trace-writer.ts +107 -0
  304. package/src/ci/types.ts +115 -0
  305. package/src/ci/upload-client.ts +300 -0
  306. package/src/cli.ts +811 -0
  307. package/src/core/agent-state.ts +162 -0
  308. package/src/core/judge-utils.ts +232 -0
  309. package/src/core/registry.ts +92 -0
  310. package/src/dashboard-server.ts +2047 -0
  311. package/src/execution/tool-runner.ts +352 -0
  312. package/src/html/dashboard.html +2218 -0
  313. package/src/http.ts +13 -0
  314. package/src/index.ts +138 -0
  315. package/src/interceptors/ai-interceptor.ts +798 -0
  316. package/src/interceptors/db-auto.ts +243 -0
  317. package/src/interceptors/db.ts +156 -0
  318. package/src/interceptors/http.ts +393 -0
  319. package/src/interceptors/side-effects.ts +83 -0
  320. package/src/interceptors/telemetry-push.ts +537 -0
  321. package/src/interceptors/tool.ts +287 -0
  322. package/src/interceptors/workflow-ai.ts +419 -0
  323. package/src/internals/conditional-recorder.ts +63 -0
  324. package/src/internals/mock-resolver.ts +492 -0
  325. package/src/matchers/index.ts +824 -0
  326. package/src/observability.ts +501 -0
  327. package/src/portal-executor.ts +355 -0
  328. package/src/portal-server.ts +304 -0
  329. package/src/proxy/llm-capture.ts +301 -0
  330. package/src/reporter.ts +81 -0
  331. package/src/runWorkflowSubprocess.ts +74 -0
  332. package/src/runner.ts +178 -0
  333. package/src/socket-connector.ts +117 -0
  334. package/src/telemetry-batcher.ts +191 -0
  335. package/src/test-setup.ts +16 -0
  336. package/src/tool-registry.ts +94 -0
  337. package/src/tool-runner-worker.ts +244 -0
  338. package/src/trace-adapter/context.ts +156 -0
  339. package/src/tracing.ts +62 -0
  340. package/src/trigger-executor.ts +171 -0
  341. package/src/types/agent.d.ts +63 -0
  342. package/src/types/expect.d.ts +81 -0
  343. package/src/types/modules.d.ts +2 -0
  344. package/src/types/portal.ts +69 -0
  345. package/src/utils/debug.ts +8 -0
  346. package/src/utils/license-error.ts +43 -0
  347. package/src/utils/redact.ts +25 -0
  348. package/src/workflow-runner-worker.ts +386 -0
  349. package/src/workflow-runner.ts +58 -0
@@ -0,0 +1,117 @@
1
+ import { io, Socket } from 'socket.io-client'
2
+ import type { PortalTask, PortalTaskResult } from './types/portal.js'
3
+ import type { TriggerSignal } from './telemetry-batcher.js'
4
+ import { executePortalTask } from './portal-executor.js'
5
+ import { executeTrigger } from './trigger-executor.js'
6
+ import { scanTools, scanWorkflows } from './execution/tool-runner.js'
7
+ import { debugLog } from './utils/debug.js'
8
+
9
+ export interface SocketConnectorOptions {
10
+ serverUrl: string
11
+ apiKey?: string
12
+ sessionId: string
13
+ }
14
+
15
+ let socket: Socket | null = null
16
+
17
+ /**
18
+ * Establish a persistent socket.io connection to the backend.
19
+ *
20
+ * The SDK registers its sessionId and available tools/workflows
21
+ * on connect. The backend can then push portal tasks and trigger signals
22
+ * to this SDK instance for execution.
23
+ *
24
+ * Auto-reconnects on disconnect with exponential backoff (1s–30s).
25
+ * Safe to call multiple times — subsequent calls return the existing socket.
26
+ */
27
+ export function connectToBackend(options: SocketConnectorOptions): Socket {
28
+ if (socket?.connected) return socket
29
+ // If a socket exists but is disconnected/reconnecting, reuse it
30
+ if (socket) return socket
31
+
32
+ const { serverUrl, apiKey, sessionId } = options
33
+ const cwd = process.cwd()
34
+
35
+ socket = io(serverUrl, {
36
+ auth: {
37
+ ...(apiKey ? { apiKey } : {}),
38
+ sessionId,
39
+ },
40
+ transports: ['websocket', 'polling'],
41
+ reconnection: true,
42
+ reconnectionDelay: 1000,
43
+ reconnectionDelayMax: 30_000,
44
+ })
45
+
46
+ socket.on('connect', () => {
47
+ debugLog(`[elasticdash] Socket connected: ${socket!.id}`)
48
+ const tools = scanTools(cwd)
49
+ const workflows = scanWorkflows(cwd)
50
+ socket!.emit('register', {
51
+ sessionId,
52
+ tools: tools.map(t => t.name),
53
+ workflows: workflows.map(w => w.name),
54
+ })
55
+ })
56
+
57
+ socket.on('auth:ok', (data: { projectId: number }) => {
58
+ debugLog(`[elasticdash] Authenticated for project ${data.projectId}`)
59
+ socket!.emit('join', `observability:project:${data.projectId}`)
60
+ })
61
+
62
+ socket.on('portal:task', async (task: PortalTask, ack?: (result: PortalTaskResult) => void) => {
63
+ debugLog(`[elasticdash] Socket received portal task: ${task.taskId} type=${task.type} name=${task.name}`)
64
+ try {
65
+ const tools = scanTools(cwd)
66
+ const result = await executePortalTask(task, cwd, tools)
67
+ if (typeof ack === 'function') {
68
+ ack(result)
69
+ } else {
70
+ socket!.emit('portal:result', result)
71
+ }
72
+ } catch (e) {
73
+ const errorResult: PortalTaskResult = {
74
+ taskId: task.taskId,
75
+ ok: false,
76
+ output: null,
77
+ error: e instanceof Error ? e.message : String(e),
78
+ durationMs: 0,
79
+ metadata: task.metadata,
80
+ }
81
+ if (typeof ack === 'function') {
82
+ ack(errorResult)
83
+ } else {
84
+ socket!.emit('portal:result', errorResult)
85
+ }
86
+ }
87
+ })
88
+
89
+ socket.on('trigger', async (trigger: TriggerSignal) => {
90
+ debugLog(`[elasticdash] Socket received trigger: ${trigger.triggerId} steps=${trigger.steps.length} runs=${trigger.runCount}`)
91
+ try {
92
+ await executeTrigger(serverUrl, apiKey, trigger)
93
+ } catch (e) {
94
+ debugLog(`[elasticdash] Socket trigger execution failed: ${e instanceof Error ? e.message : String(e)}`)
95
+ }
96
+ })
97
+
98
+ socket.on('disconnect', (reason) => {
99
+ debugLog(`[elasticdash] Socket disconnected: ${reason}`)
100
+ })
101
+
102
+ socket.on('connect_error', (err) => {
103
+ debugLog(`[elasticdash] Socket connection error: ${err.message}`)
104
+ })
105
+
106
+ return socket
107
+ }
108
+
109
+ /**
110
+ * Disconnect from the backend and clean up the socket.
111
+ */
112
+ export async function disconnectFromBackend(): Promise<void> {
113
+ if (socket) {
114
+ socket.disconnect()
115
+ socket = null
116
+ }
117
+ }
@@ -0,0 +1,191 @@
1
+ import { Readable } from 'node:stream'
2
+ import { randomUUID } from 'node:crypto'
3
+ import type { WorkflowEvent } from './capture/event.js'
4
+ import { getOriginalFetch } from './interceptors/http.js'
5
+ import { debugLog } from './utils/debug.js'
6
+ import { notifyLicenseError } from './utils/license-error.js'
7
+ import { redactPayload } from './utils/redact.js'
8
+
9
+ export interface TriggerStep {
10
+ eventId: number
11
+ eventType: 'ai' | 'tool' | 'http' | 'db'
12
+ eventName: string
13
+ originalEventDbId: number
14
+ input: unknown
15
+ model?: string
16
+ provider?: string
17
+ }
18
+
19
+ export interface FrozenEvent {
20
+ id: number
21
+ type: string
22
+ name: string
23
+ input: unknown
24
+ output: unknown
25
+ timestamp: number
26
+ durationMs: number | null
27
+ streamed?: boolean
28
+ streamRaw?: string | null
29
+ }
30
+
31
+ export interface TriggerSignal {
32
+ triggerId: number
33
+ runCount: number
34
+ steps: TriggerStep[]
35
+ /** HTTP/DB events from the same trace to freeze (mock) during step reruns */
36
+ frozenEvents?: FrozenEvent[]
37
+ }
38
+
39
+ export interface TelemetryBatcherOptions {
40
+ serverUrl: string
41
+ apiKey?: string
42
+ sessionId: string
43
+ metadata?: Record<string, unknown>
44
+ batchIntervalMs?: number
45
+ maxBatchSize?: number
46
+ redactKeys?: string[]
47
+ onTrigger?: (trigger: TriggerSignal) => Promise<void>
48
+ }
49
+
50
+ export class TelemetryBatcher {
51
+ private buffer: WorkflowEvent[] = []
52
+ private timer: ReturnType<typeof setInterval> | null = null
53
+ private readonly serverUrl: string
54
+ private readonly apiKey: string | undefined
55
+ private readonly sessionId: string
56
+ private readonly metadata: Record<string, unknown> | undefined
57
+ private readonly maxBatchSize: number
58
+ private readonly redactKeys: string[]
59
+ private readonly onTrigger: ((trigger: TriggerSignal) => Promise<void>) | undefined
60
+ private shuttingDown = false
61
+
62
+ constructor(opts: TelemetryBatcherOptions) {
63
+ this.serverUrl = opts.serverUrl.replace(/\/$/, '')
64
+ this.apiKey = opts.apiKey
65
+ this.sessionId = opts.sessionId
66
+ this.metadata = opts.metadata
67
+ this.maxBatchSize = opts.maxBatchSize ?? 50
68
+ this.redactKeys = opts.redactKeys ?? []
69
+ this.onTrigger = opts.onTrigger
70
+
71
+ const intervalMs = opts.batchIntervalMs ?? 2000
72
+ this.timer = setInterval(() => { this.flush().catch(() => {}) }, intervalMs)
73
+ // Allow the process to exit even if the timer is still scheduled
74
+ if (this.timer && typeof this.timer === 'object' && 'unref' in this.timer) {
75
+ this.timer.unref()
76
+ }
77
+ }
78
+
79
+ enqueue(event: WorkflowEvent): void {
80
+ if (this.shuttingDown) return
81
+ const redacted = this.redactKeys.length > 0
82
+ ? { ...event, input: redactPayload(event.input, this.redactKeys), output: redactPayload(event.output, this.redactKeys) }
83
+ : event
84
+ this.buffer.push(redacted as WorkflowEvent)
85
+ if (this.buffer.length >= this.maxBatchSize) {
86
+ this.flush().catch(() => {})
87
+ }
88
+ }
89
+
90
+ async flush(): Promise<void> {
91
+ if (this.buffer.length === 0) return
92
+ const batch = this.buffer.splice(0, this.buffer.length)
93
+ await this.send(batch, 0)
94
+ }
95
+
96
+ private async send(batch: WorkflowEvent[], attempt: number): Promise<void> {
97
+ if (!this.serverUrl) {
98
+ debugLog(`[elasticdash] Dropping ${batch.length} events: serverUrl is empty`)
99
+ return
100
+ }
101
+ const maxRetries = 3
102
+ const url = `${this.serverUrl}/api/observability/events`
103
+ const headers: Record<string, string> = {
104
+ 'Content-Type': 'application/json',
105
+ 'X-Correlation-ID': randomUUID(),
106
+ }
107
+ if (this.apiKey) headers['Authorization'] = `Bearer ${this.apiKey}`
108
+
109
+ try {
110
+ // Stream the JSON body to avoid buffering large payloads (e.g. events
111
+ // with streamRaw can exceed 100 MB). Node.js fetch (undici) supports
112
+ // Readable as body and uses chunked Transfer-Encoding automatically.
113
+ const body = Readable.from(jsonStreamEvents(this.sessionId, batch, this.metadata))
114
+
115
+ const res = await getOriginalFetch()(url, {
116
+ method: 'POST',
117
+ headers,
118
+ body,
119
+ duplex: 'half',
120
+ })
121
+
122
+ if (res.status === 402) {
123
+ notifyLicenseError(res.status, 'telemetry')
124
+ debugLog(`[elasticdash] Dropping ${batch.length} events: no available license (402)`)
125
+ return
126
+ }
127
+
128
+ if (res.status === 429 || res.status >= 500) {
129
+ if (attempt < maxRetries) {
130
+ const delayMs = Math.pow(2, attempt) * 1000
131
+ debugLog(`[elasticdash] Telemetry flush failed (${res.status}), retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})`)
132
+ await new Promise((r) => setTimeout(r, delayMs))
133
+ return this.send(batch, attempt + 1)
134
+ }
135
+ debugLog(`[elasticdash] Dropping ${batch.length} events after ${maxRetries} retries (last status: ${res.status})`)
136
+ return
137
+ }
138
+
139
+ debugLog(`[elasticdash] Flushed ${batch.length} events (status ${res.status})`)
140
+
141
+ // Parse response for trigger signal
142
+ if (res.ok && this.onTrigger) {
143
+ try {
144
+ const body = await res.json() as { trigger?: TriggerSignal }
145
+ if (body.trigger && typeof body.trigger.triggerId === 'number' && Array.isArray(body.trigger.steps)) {
146
+ debugLog(`[elasticdash] Trigger received: id=${body.trigger.triggerId} steps=${body.trigger.steps.length} runCount=${body.trigger.runCount}`)
147
+ // Fire-and-forget — don't block the flush pipeline
148
+ this.onTrigger(body.trigger).catch((err) => {
149
+ debugLog(`[elasticdash] Trigger execution failed: ${err instanceof Error ? err.message : String(err)}`)
150
+ })
151
+ }
152
+ } catch {
153
+ // Response parsing failed — not critical, ignore
154
+ }
155
+ }
156
+ } catch (err) {
157
+ if (attempt < maxRetries) {
158
+ const delayMs = Math.pow(2, attempt) * 1000
159
+ debugLog(`[elasticdash] Telemetry flush error, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})`)
160
+ await new Promise((r) => setTimeout(r, delayMs))
161
+ return this.send(batch, attempt + 1)
162
+ }
163
+ debugLog(`[elasticdash] Dropping ${batch.length} events after ${maxRetries} retries: ${err instanceof Error ? err.message : String(err)}`)
164
+ }
165
+ }
166
+
167
+ async shutdown(): Promise<void> {
168
+ if (this.shuttingDown) return
169
+ this.shuttingDown = true
170
+ if (this.timer) {
171
+ clearInterval(this.timer)
172
+ this.timer = null
173
+ }
174
+ await this.flush()
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Yields JSON fragments for the events payload without building a single
180
+ * giant string. Each event is serialized individually so the process never
181
+ * holds the full serialized body in memory at once.
182
+ */
183
+ function* jsonStreamEvents(sessionId: string, events: WorkflowEvent[], metadata?: Record<string, unknown>): Generator<string> {
184
+ const metaPart = metadata ? `,"metadata":${JSON.stringify(metadata)}` : ''
185
+ yield `{"sessionId":${JSON.stringify(sessionId)}${metaPart},"events":[`
186
+ for (let i = 0; i < events.length; i++) {
187
+ if (i > 0) yield ','
188
+ yield JSON.stringify(events[i])
189
+ }
190
+ yield ']}'
191
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Import this file in your AI test files to get:
3
+ * - aiTest / beforeAll / afterAll / beforeEach / afterEach available as globals (TypeScript types + runtime)
4
+ * - Custom matcher types (toHaveLLMStep, toCallTool, toMatchSemanticOutput)
5
+ *
6
+ * The CLI registers matchers at startup, so this import is for TypeScript
7
+ * type awareness only — no double-registration occurs at runtime.
8
+ */
9
+
10
+ // Side-effect: populates globalThis.aiTest + brings declare global into scope
11
+ import './core/registry.js'
12
+
13
+ // Side-effect: brings declare module 'expect' augmentation into scope
14
+ import './matchers/index.js'
15
+
16
+ export {}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Global registry for tools defined via edTool().
3
+ *
4
+ * Tools registered here are discoverable for reruns regardless of which
5
+ * module they live in. The helper also applies wrapTool() so telemetry
6
+ * fires automatically — symmetric with the Python @ed_tool decorator.
7
+ */
8
+
9
+ import { wrapTool } from './interceptors/tool.js'
10
+
11
+ export interface RegisteredTool {
12
+ name: string
13
+ fn: (...args: unknown[]) => unknown | Promise<unknown>
14
+ wrapped: (...args: unknown[]) => Promise<unknown>
15
+ isAsync: boolean
16
+ signature: string
17
+ sourceFile: string | null
18
+ lineNumber: number | null
19
+ }
20
+
21
+ const _registry: Map<string, RegisteredTool> = new Map()
22
+
23
+ export function getRegisteredTools(): RegisteredTool[] {
24
+ return Array.from(_registry.values())
25
+ }
26
+
27
+ export function getRegisteredTool(name: string): RegisteredTool | undefined {
28
+ return _registry.get(name)
29
+ }
30
+
31
+ export function clearToolRegistry(): void {
32
+ _registry.clear()
33
+ }
34
+
35
+ function inferCallerLocation(): { file: string | null; line: number | null } {
36
+ const stack = new Error().stack
37
+ if (!stack) return { file: null, line: null }
38
+ const lines = stack.split('\n')
39
+ for (let i = 2; i < lines.length; i++) {
40
+ const m = lines[i].match(/\((.*?):(\d+):\d+\)|at\s+(.*?):(\d+):\d+/)
41
+ if (!m) continue
42
+ const file = m[1] ?? m[3]
43
+ const line = parseInt(m[2] ?? m[4], 10)
44
+ if (file && !file.includes('tool-registry')) {
45
+ return { file: file.replace(/^file:\/\//, ''), line: isNaN(line) ? null : line }
46
+ }
47
+ }
48
+ return { file: null, line: null }
49
+ }
50
+
51
+ function inferSignature(fn: Function): string {
52
+ const src = fn.toString()
53
+ const m = src.match(/^[^(]*\(([^)]*)\)/)
54
+ if (!m) return '()'
55
+ const params = m[1]
56
+ .split(',')
57
+ .map(p => p.trim().split(/[\s=:]/)[0])
58
+ .filter(Boolean)
59
+ return `(${params.join(', ')})`
60
+ }
61
+
62
+ /**
63
+ * Register a function as a rerunnable tool.
64
+ *
65
+ * export const myTool = edTool('my_tool', async (query: string) => { ... })
66
+ *
67
+ * The returned function is the telemetry-wrapped version, so calling it from
68
+ * normal code paths still produces traces. The CLI `run-tool` command and the
69
+ * ElasticDash MCP `run_tool` tool will resolve the original by name.
70
+ */
71
+ export function edTool<Args extends unknown[], R>(
72
+ name: string,
73
+ fn: (...args: Args) => R | Promise<R>,
74
+ ): (...args: Args) => Promise<R> {
75
+ const isAsync = fn.constructor.name === 'AsyncFunction'
76
+ const signature = inferSignature(fn)
77
+ const { file, line } = inferCallerLocation()
78
+
79
+ const wrapped = wrapTool(name, fn as (...args: unknown[]) => Promise<R>) as unknown as (...args: Args) => Promise<R>
80
+
81
+ _registry.set(name, {
82
+ name,
83
+ fn: fn as RegisteredTool['fn'],
84
+ wrapped: wrapped as RegisteredTool['wrapped'],
85
+ isAsync,
86
+ signature,
87
+ sourceFile: file,
88
+ lineNumber: line,
89
+ })
90
+
91
+ return wrapped
92
+ }
93
+
94
+ export const defineTool = edTool
@@ -0,0 +1,244 @@
1
+ // Mark this process as an Elasticdash worker before anything else runs
2
+ ;(globalThis as any).__ELASTICDASH_WORKER__ = true
3
+
4
+ // Ensure .env is loaded in the worker subprocess
5
+ import 'dotenv/config'
6
+
7
+ /**
8
+ * tool-runner-worker.ts
9
+ *
10
+ * Subprocess entry point for running a single tool function in an isolated
11
+ * Node.js process, guaranteeing no stale ESM/tsx module cache.
12
+ *
13
+ * Protocol (via stdin/stdout):
14
+ * stdin — one JSON line: { toolsModulePath, toolName, args, frozenEvents? }
15
+ * stdout — prefixed result line: __ELASTICDASH_RESULT__:{...json...}
16
+ *
17
+ * When frozenEvents are provided, sets up an HttpRunContext so that all
18
+ * freezable actions within the tool (wrapAI, wrapDB, and fetch-intercepted
19
+ * HTTP calls) will replay from frozen data instead of hitting real services.
20
+ */
21
+
22
+ import { pathToFileURL } from 'node:url'
23
+
24
+ const RESULT_PREFIX = '__ELASTICDASH_RESULT__:'
25
+
26
+ function writeResult(result: unknown): Promise<void> {
27
+ return new Promise((resolve, reject) => {
28
+ process.stdout.write(RESULT_PREFIX + JSON.stringify(result) + '\n', (err) =>
29
+ err ? reject(err) : resolve()
30
+ )
31
+ })
32
+ }
33
+
34
+ interface FrozenEvent {
35
+ id: number
36
+ type: string
37
+ name: string
38
+ input: unknown
39
+ output: unknown
40
+ timestamp: number
41
+ durationMs: number | null
42
+ streamed?: boolean
43
+ streamRaw?: string | null
44
+ }
45
+
46
+ /**
47
+ * Set up frozen event replay context.
48
+ *
49
+ * For HTTP events: uses **URL-based matching** via a fetch wrapper, which is
50
+ * robust against ID counter drift.
51
+ *
52
+ * For AI and DB events: uses the HttpRunContext frozen map with sequential
53
+ * ID-based matching. All freezable event types (AI + DB) are included in the
54
+ * map and re-indexed in original execution order, so the shared nextId()
55
+ * counter stays aligned across interleaved wrapAI and wrapDB calls.
56
+ */
57
+ async function setupFrozenContext(frozenEvents: FrozenEvent[]): Promise<void> {
58
+ if (frozenEvents.length === 0) return
59
+
60
+ // Always install URL-based fetch replay for HTTP events — this is the
61
+ // reliable matching strategy for tool reruns in subprocesses.
62
+ installFrozenFetchFallback(frozenEvents)
63
+
64
+ // Set up HttpRunContext for all freezable event types (AI + DB).
65
+ // Both wrapAI and wrapDB call httpCtx.nextId() sequentially, so we must
66
+ // include all freezable types in the frozen map, re-indexed in original
67
+ // execution order, to keep the sequential counter aligned.
68
+ const freezableEvents = frozenEvents
69
+ .filter(e => e.type === 'ai' || e.type === 'db')
70
+ .sort((a, b) => a.id - b.id) // preserve original execution order
71
+ if (freezableEvents.length > 0) {
72
+ try {
73
+ const telemetryPush = await import('./interceptors/telemetry-push.js')
74
+ const workflowEvents = freezableEvents.map((e, i) => ({
75
+ id: i + 1,
76
+ type: e.type as 'ai' | 'db',
77
+ name: e.name,
78
+ input: e.input,
79
+ output: e.output,
80
+ timestamp: e.timestamp,
81
+ durationMs: e.durationMs ?? 0,
82
+ streamed: e.streamed,
83
+ streamRaw: e.streamRaw,
84
+ }))
85
+ telemetryPush.setHttpRunContext('frozen-rerun', '')
86
+ const ctx = telemetryPush.getHttpRunContext()
87
+ if (ctx) {
88
+ for (const e of workflowEvents) {
89
+ ctx.frozenEvents.set(e.id, e as any)
90
+ }
91
+ }
92
+ } catch {
93
+ // Frozen replay not available — tool will hit real services
94
+ }
95
+ }
96
+ }
97
+
98
+ /** Saved original fetch so we can restore after the tool runs. */
99
+ let savedOriginalFetch: typeof globalThis.fetch | null = null
100
+
101
+ function restoreFrozenFetch(): void {
102
+ if (savedOriginalFetch) {
103
+ globalThis.fetch = savedOriginalFetch
104
+ savedOriginalFetch = null
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Intercept fetch for HTTP frozen events using URL-based matching.
110
+ * This is the primary replay mechanism for tool reruns — it matches
111
+ * by request URL rather than sequential ID, which is robust against
112
+ * ID counter drift from wrapTool/wrapAI consuming IDs.
113
+ */
114
+ function installFrozenFetchFallback(frozenEvents: FrozenEvent[]): void {
115
+ const httpEvents = frozenEvents.filter(e => e.type === 'http')
116
+
117
+ const frozenUrls = httpEvents.map(e => {
118
+ const inp = e.input as Record<string, unknown> | null
119
+ return inp && typeof inp === 'object' && typeof inp.url === 'string' ? inp.url : '(no url)'
120
+ })
121
+ process.stderr.write(`[elasticdash-worker] Frozen HTTP events: ${httpEvents.length}, URLs: ${JSON.stringify(frozenUrls)}\n`)
122
+
123
+ savedOriginalFetch = globalThis.fetch
124
+ const originalFetch = globalThis.fetch
125
+ const usageCounts = new Map<string, number>()
126
+
127
+ globalThis.fetch = async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
128
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : (input as Request).url
129
+
130
+ const matches = httpEvents.filter(e => {
131
+ const frozenUrl = (e.input && typeof e.input === 'object' && 'url' in (e.input as Record<string, unknown>))
132
+ ? (e.input as Record<string, unknown>).url
133
+ : null
134
+ return frozenUrl === url
135
+ })
136
+
137
+ if (matches.length > 0) {
138
+ const callCount = usageCounts.get(url) || 0
139
+ const match = matches[Math.min(callCount, matches.length - 1)]
140
+ usageCounts.set(url, callCount + 1)
141
+
142
+ process.stderr.write(`[elasticdash-worker] Frozen replay: ${url}\n`)
143
+ const body = typeof match.output === 'string' ? match.output : JSON.stringify(match.output)
144
+ return new Response(body, {
145
+ status: 200,
146
+ headers: { 'Content-Type': 'application/json', 'X-Frozen': 'true' }
147
+ })
148
+ }
149
+
150
+ process.stderr.write(`[elasticdash-worker] Fetch pass-through (no frozen match): ${url}\n`)
151
+ try {
152
+ return await originalFetch(input, init)
153
+ } catch (e) {
154
+ process.stderr.write(`[elasticdash-worker] Fetch FAILED for ${url}: ${(e as Error).stack || (e as Error).message || String(e)}\n`)
155
+ throw e
156
+ }
157
+ }
158
+ }
159
+
160
+ async function main() {
161
+ const originalExit = process.exit.bind(process)
162
+
163
+ // Prevent the SDK's tryAutoInitHttpContext from triggering full observability
164
+ // or dashboard-mode initialization in this subprocess. The subprocess should
165
+ // only execute the tool — observability/dashboard contexts are set up
166
+ // explicitly via setupFrozenContext when frozen events are provided.
167
+ // Without this, inherited env vars cause initObservability() to install
168
+ // interceptors, socket connections, and heartbeats that conflict with
169
+ // the frozen replay setup and can cause "Invalid URL" errors from
170
+ // Langfuse/OTel trying to connect to servers during tool execution.
171
+ delete process.env.ELASTICDASH_API_URL
172
+ delete process.env.ELASTICDASH_SERVER
173
+
174
+ let raw = ''
175
+ for await (const chunk of process.stdin) {
176
+ raw += chunk
177
+ }
178
+
179
+ let payload: { toolsModulePath: string; toolName: string; args: unknown[]; frozenEvents?: FrozenEvent[] }
180
+ try {
181
+ payload = JSON.parse(raw)
182
+ } catch (e) {
183
+ await writeResult({ ok: false, error: `Invalid JSON input: ${(e as Error).message}` })
184
+ originalExit(1)
185
+ return
186
+ }
187
+
188
+ const { toolsModulePath, toolName, args, frozenEvents } = payload
189
+
190
+ // Set up frozen event context before running the tool.
191
+ // Uses URL-based matching for HTTP events (reliable for observability reruns)
192
+ // and ID-based matching for DB events.
193
+ const hasFrozen = frozenEvents && frozenEvents.length > 0
194
+ if (hasFrozen) {
195
+ await setupFrozenContext(frozenEvents)
196
+ }
197
+
198
+ try {
199
+ let mod: any
200
+ try {
201
+ mod = await import(pathToFileURL(toolsModulePath).href)
202
+ } catch (importErr) {
203
+ const ie = importErr as Error
204
+ await writeResult({ ok: false, error: `Failed to import tool module: ${ie.stack || ie.message}` })
205
+ originalExit(1)
206
+ return
207
+ }
208
+
209
+ // Registry first: covers tools defined via edTool() anywhere in the project,
210
+ // as long as their containing module is reachable from toolsModulePath's
211
+ // import graph. Falls back to ed_tools-style module export lookup.
212
+ let fn: ((...a: unknown[]) => unknown) | undefined
213
+ try {
214
+ const reg = await import('./tool-registry.js')
215
+ const registered = reg.getRegisteredTool(toolName)
216
+ if (registered) fn = registered.wrapped
217
+ } catch {
218
+ // Registry module not available (older SDK build); fall through to export lookup.
219
+ }
220
+ if (!fn) {
221
+ const exported = mod[toolName]
222
+ if (typeof exported === 'function') fn = exported
223
+ }
224
+ if (typeof fn !== 'function') {
225
+ await writeResult({ ok: false, error: `"${toolName}" not found via edTool() registry or as an exported function in the module.` })
226
+ originalExit(1)
227
+ return
228
+ }
229
+
230
+ const currentOutput = await fn(...args)
231
+ await writeResult({ ok: true, currentOutput })
232
+ originalExit(0)
233
+ } catch (e) {
234
+ const err = e as Error
235
+ const errorMsg = err.stack || err.message || String(e)
236
+ process.stderr.write(`[elasticdash-worker] Tool execution failed:\n${errorMsg}\n`)
237
+ await writeResult({ ok: false, error: errorMsg })
238
+ originalExit(1)
239
+ } finally {
240
+ if (hasFrozen) restoreFrozenFetch()
241
+ }
242
+ }
243
+
244
+ main()