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,355 @@
1
+ import { callProviderLLM } from './matchers/index.js'
2
+ import {
3
+ scanTools,
4
+ resolveRuntimeModule,
5
+ buildToolArgs,
6
+ runToolInSubprocess,
7
+ } from './execution/tool-runner.js'
8
+ import type { ToolInfo } from './execution/tool-runner.js'
9
+ import type { PortalTask, PortalTaskResult } from './types/portal.js'
10
+ import { debugLog } from './utils/debug.js'
11
+
12
+ type SupportedProvider = 'openai' | 'claude' | 'gemini' | 'grok' | 'kimi'
13
+
14
+ /** Provider → required env var name */
15
+ const PROVIDER_API_KEY_ENV: Record<SupportedProvider, string> = {
16
+ openai: 'OPENAI_API_KEY',
17
+ claude: 'ANTHROPIC_API_KEY',
18
+ gemini: 'GEMINI_API_KEY',
19
+ grok: 'GROK_API_KEY',
20
+ kimi: 'KIMI_API_KEY',
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Prompt extraction (mirrors dashboard-server.ts logic)
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function normalizeMessageContent(content: unknown): string {
28
+ if (typeof content === 'string') return content
29
+ if (Array.isArray(content)) {
30
+ return content
31
+ .map((part) => {
32
+ if (typeof part === 'string') return part
33
+ if (part && typeof part === 'object' && typeof (part as any).text === 'string') return (part as any).text
34
+ try { return JSON.stringify(part) } catch { return String(part) }
35
+ })
36
+ .join('\n')
37
+ }
38
+ if (content && typeof content === 'object') {
39
+ if (typeof (content as any).text === 'string') return (content as any).text
40
+ try { return JSON.stringify(content) } catch { return String(content) }
41
+ }
42
+ return content == null ? '' : String(content)
43
+ }
44
+
45
+ function extractPromptFromInput(input: unknown): { prompt: string; systemPrompt?: string } {
46
+ if (typeof input === 'string') return { prompt: input }
47
+
48
+ const messages = Array.isArray(input)
49
+ ? input
50
+ : input && typeof input === 'object' && Array.isArray((input as any).messages)
51
+ ? (input as any).messages
52
+ : null
53
+
54
+ if (messages && messages.length > 0) {
55
+ const systemParts: string[] = []
56
+ const promptParts: string[] = []
57
+ for (const message of messages as Array<any>) {
58
+ const role = typeof message?.role === 'string' ? message.role : 'user'
59
+ const content = normalizeMessageContent(message?.content).trim()
60
+ if (!content) continue
61
+ if (role === 'system') systemParts.push(content)
62
+ else promptParts.push(`${role}: ${content}`)
63
+ }
64
+ return {
65
+ prompt: promptParts.join('\n\n') || systemParts.join('\n\n') || JSON.stringify(input),
66
+ systemPrompt: systemParts.length > 0 ? systemParts.join('\n\n') : undefined,
67
+ }
68
+ }
69
+
70
+ if (input && typeof input === 'object' && typeof (input as any).prompt === 'string') {
71
+ return {
72
+ prompt: (input as any).prompt,
73
+ systemPrompt: typeof (input as any).systemPrompt === 'string' ? (input as any).systemPrompt : undefined,
74
+ }
75
+ }
76
+
77
+ try { return { prompt: JSON.stringify(input) } }
78
+ catch { return { prompt: String(input ?? '') } }
79
+ }
80
+
81
+ function inferProvider(task: PortalTask): SupportedProvider {
82
+ const provider = task.provider?.toLowerCase()
83
+ if (provider === 'openai' || provider === 'claude' || provider === 'gemini' || provider === 'grok' || provider === 'kimi') {
84
+ return provider
85
+ }
86
+ // Try extracting provider from input (wrapAI attaches it to the event input)
87
+ const inputProvider = task.input && typeof task.input === 'object'
88
+ ? (task.input as Record<string, unknown>).provider
89
+ : undefined
90
+ if (typeof inputProvider === 'string') {
91
+ const ip = inputProvider.toLowerCase()
92
+ if (ip === 'openai' || ip === 'claude' || ip === 'gemini' || ip === 'grok' || ip === 'kimi') return ip
93
+ }
94
+ const model = (task.model ?? task.name)?.toLowerCase() ?? ''
95
+ if (model.includes('claude')) return 'claude'
96
+ if (model.includes('gemini')) return 'gemini'
97
+ if (model.includes('grok')) return 'grok'
98
+ if (model.includes('kimi')) return 'kimi'
99
+ return 'openai'
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Availability checks (used by trigger-executor for pre-validation)
104
+ // ---------------------------------------------------------------------------
105
+
106
+ export interface AvailabilityResult {
107
+ available: boolean
108
+ reason?: string
109
+ }
110
+
111
+ /**
112
+ * Check if a tool is available for execution.
113
+ * Returns { available: true } or { available: false, reason: "..." }.
114
+ */
115
+ export function checkToolAvailability(
116
+ name: string,
117
+ cwd: string,
118
+ tools: ToolInfo[],
119
+ ): AvailabilityResult {
120
+ if (!name) {
121
+ return { available: false, reason: 'Missing tool name.' }
122
+ }
123
+ const toolsModulePath = resolveRuntimeModule(cwd, 'ed_tools')
124
+ if (!toolsModulePath) {
125
+ return { available: false, reason: 'Cannot find ed_tools.ts/js in workspace root.' }
126
+ }
127
+ const toolInfo = tools.find(t => t.name === name)
128
+ if (!toolInfo) {
129
+ const available = tools.map(t => t.name).join(', ') || '(none found)'
130
+ return { available: false, reason: `Tool not found: "${name}". Available tools: ${available}` }
131
+ }
132
+ return { available: true }
133
+ }
134
+
135
+ /**
136
+ * Check if an AI provider is available for execution (API key present).
137
+ * Returns { available: true } or { available: false, reason: "..." }.
138
+ */
139
+ export function checkAIAvailability(
140
+ provider?: string,
141
+ model?: string,
142
+ ): AvailabilityResult {
143
+ const resolved = inferProviderFromArgs(provider, model)
144
+ const supportedProviders: SupportedProvider[] = ['openai', 'claude', 'gemini', 'grok', 'kimi']
145
+ if (!supportedProviders.includes(resolved)) {
146
+ return { available: false, reason: `Unsupported AI provider: "${provider ?? model}". Supported: ${supportedProviders.join(', ')}` }
147
+ }
148
+ const envVar = PROVIDER_API_KEY_ENV[resolved]
149
+ if (!process.env[envVar]) {
150
+ if (resolved === 'gemini' && process.env.GOOGLE_API_KEY) {
151
+ return { available: true }
152
+ }
153
+ return { available: false, reason: `Missing API key for provider "${resolved}". Expected environment variable: ${envVar}` }
154
+ }
155
+ return { available: true }
156
+ }
157
+
158
+ function inferProviderFromArgs(provider?: string, model?: string): SupportedProvider {
159
+ const p = provider?.toLowerCase()
160
+ if (p === 'openai' || p === 'claude' || p === 'gemini' || p === 'grok' || p === 'kimi') return p
161
+ const m = (model ?? '')?.toLowerCase()
162
+ if (m.includes('claude')) return 'claude'
163
+ if (m.includes('gemini')) return 'gemini'
164
+ if (m.includes('grok')) return 'grok'
165
+ if (m.includes('kimi')) return 'kimi'
166
+ return 'openai'
167
+ }
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Public API
171
+ // ---------------------------------------------------------------------------
172
+
173
+ /**
174
+ * Execute a portal task (tool or AI rerun). Returns a structured result with
175
+ * output, duration, usage, and error information.
176
+ *
177
+ * Error handling:
178
+ * - Tool not found in ed_tools → ok:false with descriptive error
179
+ * - ed_tools.ts/js missing → ok:false with descriptive error
180
+ * - AI provider unsupported → ok:false with error
181
+ * - AI provider API key missing → ok:false with error naming the expected env var
182
+ * - Subprocess crash → ok:false with stderr excerpt
183
+ * - Any other exception → ok:false with error message
184
+ */
185
+ export async function executePortalTask(
186
+ task: PortalTask,
187
+ cwd: string,
188
+ tools: ToolInfo[],
189
+ ): Promise<PortalTaskResult> {
190
+ const start = Date.now()
191
+ try {
192
+ if (task.type === 'tool') {
193
+ return await executeToolTask(task, cwd, tools)
194
+ }
195
+ if (task.type === 'ai') {
196
+ return await executeAITask(task)
197
+ }
198
+ return {
199
+ taskId: task.taskId,
200
+ ok: false,
201
+ output: null,
202
+ error: `Unknown task type: ${task.type}`,
203
+ durationMs: Date.now() - start,
204
+ metadata: task.metadata,
205
+ }
206
+ } catch (e) {
207
+ return {
208
+ taskId: task.taskId,
209
+ ok: false,
210
+ output: null,
211
+ error: e instanceof Error ? e.message : String(e),
212
+ durationMs: Date.now() - start,
213
+ metadata: task.metadata,
214
+ }
215
+ }
216
+ }
217
+
218
+ async function executeToolTask(
219
+ task: PortalTask,
220
+ cwd: string,
221
+ tools: ToolInfo[],
222
+ ): Promise<PortalTaskResult> {
223
+ const start = Date.now()
224
+
225
+ if (!task.name) {
226
+ return { taskId: task.taskId, ok: false, output: null, error: 'Missing tool name on task.', durationMs: 0, metadata: task.metadata }
227
+ }
228
+
229
+ // Check tool exists in the scanned tool list
230
+ const toolInfo = tools.find(t => t.name === task.name)
231
+ if (!toolInfo) {
232
+ const available = tools.map(t => t.name).join(', ') || '(none found)'
233
+ return {
234
+ taskId: task.taskId, ok: false, output: null,
235
+ error: `Tool not found: "${task.name}". Available tools: ${available}`,
236
+ durationMs: 0, metadata: task.metadata,
237
+ }
238
+ }
239
+
240
+ // Resolve ed_tools module
241
+ const toolsModulePath = resolveRuntimeModule(cwd, 'ed_tools')
242
+ if (!toolsModulePath) {
243
+ return {
244
+ taskId: task.taskId, ok: false, output: null,
245
+ error: 'Cannot find ed_tools.ts/js in workspace root.',
246
+ durationMs: 0, metadata: task.metadata,
247
+ }
248
+ }
249
+
250
+ // Parse input
251
+ let parsedInput = task.input
252
+ if (typeof parsedInput === 'string') {
253
+ try { parsedInput = JSON.parse(parsedInput) } catch { /* use as-is */ }
254
+ }
255
+
256
+ const args = buildToolArgs(parsedInput, toolInfo)
257
+ debugLog(`[elasticdash portal] Executing tool: ${task.name}`, { args })
258
+
259
+ const result = await runToolInSubprocess(toolsModulePath, task.name, args, task.frozenEvents)
260
+ .catch(err => {
261
+ const errorMsg = err instanceof Error ? err.stack || err.message : String(err)
262
+ debugLog(`[elasticdash portal] Tool execution failed: ${errorMsg}`)
263
+ throw new Error(`Tool execution failed: ${errorMsg}`)
264
+ })
265
+ const durationMs = result.currentDurationMs ?? (Date.now() - start)
266
+
267
+ debugLog(`[elasticdash portal] Tool execution completed: ${task.name}`, { ...result, durationMs })
268
+
269
+ return {
270
+ taskId: task.taskId,
271
+ ok: result.ok,
272
+ output: result.currentOutput ?? null,
273
+ error: result.error,
274
+ durationMs,
275
+ usage: result.currentUsage,
276
+ metadata: task.metadata,
277
+ }
278
+ }
279
+
280
+ async function executeAITask(task: PortalTask): Promise<PortalTaskResult> {
281
+ const start = Date.now()
282
+
283
+ // Infer provider
284
+ const provider = inferProvider(task)
285
+ const supportedProviders: SupportedProvider[] = ['openai', 'claude', 'gemini', 'grok', 'kimi']
286
+ if (!supportedProviders.includes(provider)) {
287
+ return {
288
+ taskId: task.taskId, ok: false, output: null,
289
+ error: `Unsupported AI provider: "${task.provider}". Supported: ${supportedProviders.join(', ')}`,
290
+ durationMs: 0, metadata: task.metadata,
291
+ }
292
+ }
293
+
294
+ // Check API key is available
295
+ const envVar = PROVIDER_API_KEY_ENV[provider]
296
+ if (!process.env[envVar]) {
297
+ // Gemini also accepts GOOGLE_API_KEY
298
+ if (provider === 'gemini' && process.env.GOOGLE_API_KEY) {
299
+ // ok, fallback key available
300
+ } else {
301
+ return {
302
+ taskId: task.taskId, ok: false, output: null,
303
+ error: `Missing API key for provider "${provider}". Expected environment variable: ${envVar}`,
304
+ durationMs: 0, metadata: task.metadata,
305
+ }
306
+ }
307
+ }
308
+
309
+ // Extract prompt from input
310
+ const { prompt, systemPrompt } = extractPromptFromInput(task.input)
311
+ if (!prompt.trim()) {
312
+ return {
313
+ taskId: task.taskId, ok: false, output: null,
314
+ error: 'AI task input is empty; cannot execute.',
315
+ durationMs: 0, metadata: task.metadata,
316
+ }
317
+ }
318
+
319
+ // Prefer explicit model from trigger step, then try extracting from input
320
+ // (wrapAI attaches the actual API model name to the event input), fall back to event name
321
+ const inputModel = task.input && typeof task.input === 'object' && typeof (task.input as Record<string, unknown>).model === 'string'
322
+ ? (task.input as Record<string, unknown>).model as string
323
+ : undefined
324
+ const model = task.model ?? inputModel ?? task.name
325
+ const temperature = typeof task.modelParameters?.temperature === 'number' ? task.modelParameters.temperature : 0
326
+ const maxTokens = typeof task.modelParameters?.max_tokens === 'number' ? task.modelParameters.max_tokens : 512
327
+
328
+ debugLog(`[elasticdash portal] Executing AI: provider=${provider} model=${model}`)
329
+
330
+ try {
331
+ const result = await callProviderLLM(
332
+ prompt,
333
+ { provider, model },
334
+ systemPrompt ?? 'You are a helpful assistant.',
335
+ maxTokens,
336
+ // temperature,
337
+ )
338
+
339
+ return {
340
+ taskId: task.taskId,
341
+ ok: true,
342
+ output: result.content,
343
+ durationMs: result.durationMs ?? (Date.now() - start),
344
+ usage: result.usage,
345
+ metadata: task.metadata,
346
+ }
347
+ } catch (e) {
348
+ return {
349
+ taskId: task.taskId, ok: false, output: null,
350
+ error: `AI execution failed: ${e instanceof Error ? e.message : String(e)}`,
351
+ durationMs: Date.now() - start,
352
+ metadata: task.metadata,
353
+ }
354
+ }
355
+ }
@@ -0,0 +1,304 @@
1
+ import express from 'express'
2
+ import http from 'node:http'
3
+ import { executePortalTask } from './portal-executor.js'
4
+ import { scanTools } from './execution/tool-runner.js'
5
+ import type { ToolInfo } from './execution/tool-runner.js'
6
+ import type {
7
+ PortalTask,
8
+ PortalTaskResult,
9
+ PortalServerOptions,
10
+ PortalServerHandle,
11
+ PortalStatus,
12
+ } from './types/portal.js'
13
+ import { debugLog } from './utils/debug.js'
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Origin allowlist
17
+ // ---------------------------------------------------------------------------
18
+
19
+ function extractHost(url: string): string | null {
20
+ try {
21
+ return new URL(url).host
22
+ } catch {
23
+ return null
24
+ }
25
+ }
26
+
27
+ function buildAllowedHosts(backendUrl: string, extra?: string[]): Set<string> {
28
+ const hosts = new Set<string>()
29
+ // Always allow localhost variants
30
+ hosts.add('localhost')
31
+ hosts.add('127.0.0.1')
32
+ hosts.add('::1')
33
+ // Allow the configured backend
34
+ const backendHost = extractHost(backendUrl)
35
+ if (backendHost) hosts.add(backendHost)
36
+ // Allow explicit extra origins
37
+ if (extra) {
38
+ for (const origin of extra) {
39
+ const h = extractHost(origin)
40
+ if (h) hosts.add(h)
41
+ else hosts.add(origin) // treat raw hostname as-is
42
+ }
43
+ }
44
+ return hosts
45
+ }
46
+
47
+ function isAllowedOrigin(req: express.Request, allowedHosts: Set<string>): boolean {
48
+ // Determine the caller's host from multiple headers
49
+ // 1. Origin header (set by browsers and some HTTP clients)
50
+ const origin = req.headers.origin
51
+ if (origin) {
52
+ const h = extractHost(origin)
53
+ return h !== null && isHostAllowed(h, allowedHosts)
54
+ }
55
+ // 2. Referer header fallback
56
+ const referer = req.headers.referer
57
+ if (referer) {
58
+ const h = extractHost(referer)
59
+ return h !== null && isHostAllowed(h, allowedHosts)
60
+ }
61
+ // 3. X-Forwarded-For / remote IP — check if it's a local request
62
+ const remoteIp = req.ip ?? req.socket?.remoteAddress ?? ''
63
+ if (remoteIp === '127.0.0.1' || remoteIp === '::1' || remoteIp === '::ffff:127.0.0.1') {
64
+ return true
65
+ }
66
+ // 4. No origin info — this is a direct server-to-server call.
67
+ // If API key auth is configured and passes, allow it.
68
+ // If no API key is configured, reject unknown-origin requests.
69
+ return false
70
+ }
71
+
72
+ function isHostAllowed(host: string, allowedHosts: Set<string>): boolean {
73
+ // Exact match (includes port, e.g. "localhost:4573")
74
+ if (allowedHosts.has(host)) return true
75
+ // Match without port
76
+ const hostWithoutPort = host.split(':')[0]
77
+ if (allowedHosts.has(hostWithoutPort)) return true
78
+ // Match subdomains (e.g. "api.elasticdash.com" matches "elasticdash.com")
79
+ for (const allowed of allowedHosts) {
80
+ if (hostWithoutPort.endsWith('.' + allowed)) return true
81
+ }
82
+ return false
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Server
87
+ // ---------------------------------------------------------------------------
88
+
89
+ export async function startPortalServer(options: PortalServerOptions): Promise<PortalServerHandle> {
90
+ const port = options.port ?? 4574
91
+ const backendUrl = options.backendUrl.replace(/\/$/, '')
92
+ const apiKey = options.apiKey
93
+ const cwd = options.cwd ?? process.cwd()
94
+
95
+ // Build allowed-origin set from backendUrl + explicit allowlist + localhost
96
+ const allowedHosts = buildAllowedHosts(backendUrl, options.allowedOrigins)
97
+ console.log(`[elasticdash portal] Allowed origins: ${[...allowedHosts].join(', ')}`)
98
+
99
+ // Scan tools at startup
100
+ const tools: ToolInfo[] = scanTools(cwd)
101
+ console.log(`[elasticdash portal] Scanned ${tools.length} tools: ${tools.map(t => t.name).join(', ') || '(none)'}`)
102
+
103
+ // Queue state
104
+ const queue: PortalTask[] = []
105
+ let processing: string | null = null
106
+ let completed = 0
107
+ let failed = 0
108
+ let draining = false
109
+
110
+ // -------------------------------------------------------------------------
111
+ // Queue processor
112
+ // -------------------------------------------------------------------------
113
+
114
+ async function processQueue(): Promise<void> {
115
+ if (draining || processing) return
116
+ if (queue.length === 0) return
117
+
118
+ const task = queue.shift()!
119
+ processing = task.taskId
120
+ console.log(`[elasticdash portal] Processing task ${task.taskId} (type=${task.type}, name=${task.name}) — ${queue.length} remaining`)
121
+
122
+ const result = await executePortalTask(task, cwd, tools)
123
+
124
+ if (result.ok) {
125
+ completed++
126
+ console.log(`[elasticdash portal] Task ${task.taskId} completed (${result.durationMs}ms)`)
127
+ } else {
128
+ failed++
129
+ console.log(`[elasticdash portal] Task ${task.taskId} failed: ${result.error}`)
130
+ }
131
+
132
+ processing = null
133
+
134
+ // Deliver result to backend
135
+ await deliverResult(result)
136
+
137
+ // Process next task
138
+ processQueue().catch(() => {})
139
+ }
140
+
141
+ async function deliverResult(result: PortalTaskResult): Promise<void> {
142
+ const url = `${backendUrl}/api/portal/results/${result.taskId}`
143
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' }
144
+ if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
145
+
146
+ for (let attempt = 0; attempt < 3; attempt++) {
147
+ try {
148
+ const res = await fetch(url, {
149
+ method: 'POST',
150
+ headers,
151
+ body: JSON.stringify(result),
152
+ })
153
+ if (res.ok || res.status < 500) {
154
+ debugLog(`[elasticdash portal] Result delivered for task ${result.taskId} (status ${res.status})`)
155
+ return
156
+ }
157
+ debugLog(`[elasticdash portal] Result delivery failed (status ${res.status}), attempt ${attempt + 1}/3`)
158
+ } catch (e) {
159
+ debugLog(`[elasticdash portal] Result delivery error, attempt ${attempt + 1}/3: ${e instanceof Error ? e.message : String(e)}`)
160
+ }
161
+ if (attempt < 2) {
162
+ await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000))
163
+ }
164
+ }
165
+ console.warn(`[elasticdash portal] WARNING: Failed to deliver result for task ${result.taskId} after 3 retries`)
166
+ }
167
+
168
+ // -------------------------------------------------------------------------
169
+ // Security middleware: origin allowlist + API key auth
170
+ // -------------------------------------------------------------------------
171
+
172
+ function securityMiddleware(req: express.Request, res: express.Response, next: express.NextFunction): void {
173
+ // Check API key first — a valid key overrides origin checks
174
+ if (apiKey) {
175
+ const authHeader = req.headers.authorization
176
+ if (authHeader === `Bearer ${apiKey}`) return next()
177
+ }
178
+
179
+ // Check origin allowlist
180
+ if (isAllowedOrigin(req, allowedHosts)) {
181
+ // If API key is configured but not provided, still reject
182
+ if (apiKey) {
183
+ console.warn(JSON.stringify({
184
+ event: 'auth.failure',
185
+ reason: 'invalid_api_key',
186
+ ip: req.ip || req.headers['x-forwarded-for'] || 'unknown',
187
+ userAgent: req.headers['user-agent'] || 'unknown',
188
+ timestamp: new Date().toISOString(),
189
+ }))
190
+ res.status(401).json({ ok: false, error: 'unauthorized — valid API key required' })
191
+ return
192
+ }
193
+ return next()
194
+ }
195
+
196
+ // Origin not in allowlist and no valid API key
197
+ const origin = req.headers.origin ?? req.headers.referer ?? req.ip ?? 'unknown'
198
+ console.warn(JSON.stringify({
199
+ event: 'auth.failure',
200
+ reason: 'origin_not_allowed',
201
+ origin,
202
+ ip: req.ip || req.headers['x-forwarded-for'] || 'unknown',
203
+ userAgent: req.headers['user-agent'] || 'unknown',
204
+ timestamp: new Date().toISOString(),
205
+ }))
206
+ res.status(403).json({ ok: false, error: 'forbidden — origin not in allowlist' })
207
+ }
208
+
209
+ // -------------------------------------------------------------------------
210
+ // Express app
211
+ // -------------------------------------------------------------------------
212
+
213
+ const app = express()
214
+ app.use(express.json({ limit: '10mb' }))
215
+
216
+ // POST /api/portal/tasks — enqueue a single task
217
+ app.post('/api/portal/tasks', securityMiddleware, (req, res) => {
218
+ const task = req.body as PortalTask
219
+ if (!task?.taskId || !task?.type || !task?.name) {
220
+ res.status(400).json({ ok: false, error: 'Missing required fields: taskId, type, name' })
221
+ return
222
+ }
223
+ queue.push(task)
224
+ const position = queue.length
225
+ debugLog(`[elasticdash portal] Task ${task.taskId} enqueued at position ${position}`)
226
+ res.status(202).json({ ok: true, taskId: task.taskId, position })
227
+ processQueue().catch(() => {})
228
+ })
229
+
230
+ // POST /api/portal/tasks/batch — enqueue multiple tasks
231
+ app.post('/api/portal/tasks/batch', securityMiddleware, (req, res) => {
232
+ const body = req.body as { tasks?: PortalTask[] }
233
+ if (!Array.isArray(body?.tasks) || body.tasks.length === 0) {
234
+ res.status(400).json({ ok: false, error: 'Missing or empty tasks array' })
235
+ return
236
+ }
237
+ const results: Array<{ taskId: string; position: number }> = []
238
+ for (const task of body.tasks) {
239
+ if (!task?.taskId || !task?.type || !task?.name) continue
240
+ queue.push(task)
241
+ results.push({ taskId: task.taskId, position: queue.length })
242
+ }
243
+ debugLog(`[elasticdash portal] Batch enqueued ${results.length} tasks`)
244
+ res.status(202).json({ ok: true, tasks: results })
245
+ processQueue().catch(() => {})
246
+ })
247
+
248
+ // GET /api/portal/status — health check
249
+ app.get('/api/portal/status', (_req, res) => {
250
+ const status: PortalStatus = {
251
+ ok: true,
252
+ queueLength: queue.length,
253
+ processing,
254
+ completed,
255
+ failed,
256
+ }
257
+ res.json(status)
258
+ })
259
+
260
+ // DELETE /api/portal/tasks/:taskId — cancel a pending task
261
+ app.delete('/api/portal/tasks/:taskId', securityMiddleware, (req, res) => {
262
+ const taskId = req.params.taskId
263
+ const index = queue.findIndex(t => t.taskId === taskId)
264
+ if (index === -1) {
265
+ res.status(404).json({ ok: false, error: 'Task not found in queue (may be already processing or completed)' })
266
+ return
267
+ }
268
+ queue.splice(index, 1)
269
+ res.json({ ok: true, taskId })
270
+ })
271
+
272
+ // -------------------------------------------------------------------------
273
+ // Start server
274
+ // -------------------------------------------------------------------------
275
+
276
+ const server = http.createServer(app)
277
+
278
+ await new Promise<void>((resolve, reject) => {
279
+ server.on('error', reject)
280
+ server.listen(port, () => resolve())
281
+ })
282
+
283
+ const url = `http://localhost:${port}`
284
+
285
+ return {
286
+ port,
287
+ url,
288
+ close: async () => {
289
+ draining = true
290
+ // Wait for current task to finish if processing
291
+ if (processing) {
292
+ console.log(`[elasticdash portal] Waiting for current task ${processing} to finish...`)
293
+ await new Promise<void>((resolve) => {
294
+ const check = setInterval(() => {
295
+ if (!processing) { clearInterval(check); resolve() }
296
+ }, 200)
297
+ })
298
+ }
299
+ await new Promise<void>((resolve, reject) => {
300
+ server.close((err) => err ? reject(err) : resolve())
301
+ })
302
+ },
303
+ }
304
+ }