elasticdash-test 0.1.17 → 0.1.18-alpha

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 (95) hide show
  1. package/dist/capture/event.d.ts +5 -1
  2. package/dist/capture/event.d.ts.map +1 -1
  3. package/dist/cli.js +100 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/evaluators/llm-judge.js +17 -14
  6. package/dist/evaluators/types.d.ts +1 -0
  7. package/dist/execution/tool-runner.d.ts +26 -0
  8. package/dist/execution/tool-runner.d.ts.map +1 -0
  9. package/dist/execution/tool-runner.js +270 -0
  10. package/dist/execution/tool-runner.js.map +1 -0
  11. package/dist/http.d.ts +2 -0
  12. package/dist/http.d.ts.map +1 -1
  13. package/dist/http.js +2 -0
  14. package/dist/http.js.map +1 -1
  15. package/dist/index.cjs +4310 -2672
  16. package/dist/index.d.ts +10 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +7 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/interceptors/ai-interceptor.d.ts.map +1 -1
  21. package/dist/interceptors/ai-interceptor.js +97 -4
  22. package/dist/interceptors/ai-interceptor.js.map +1 -1
  23. package/dist/interceptors/db-auto.d.ts.map +1 -1
  24. package/dist/interceptors/db-auto.js +116 -24
  25. package/dist/interceptors/db-auto.js.map +1 -1
  26. package/dist/interceptors/db.d.ts +5 -0
  27. package/dist/interceptors/db.d.ts.map +1 -1
  28. package/dist/interceptors/db.js +93 -15
  29. package/dist/interceptors/db.js.map +1 -1
  30. package/dist/interceptors/http.d.ts.map +1 -1
  31. package/dist/interceptors/http.js +125 -93
  32. package/dist/interceptors/http.js.map +1 -1
  33. package/dist/interceptors/telemetry-push.d.ts +15 -0
  34. package/dist/interceptors/telemetry-push.d.ts.map +1 -1
  35. package/dist/interceptors/telemetry-push.js +96 -13
  36. package/dist/interceptors/telemetry-push.js.map +1 -1
  37. package/dist/interceptors/tool.d.ts.map +1 -1
  38. package/dist/interceptors/tool.js +42 -5
  39. package/dist/interceptors/tool.js.map +1 -1
  40. package/dist/interceptors/workflow-ai.d.ts.map +1 -1
  41. package/dist/interceptors/workflow-ai.js +46 -2
  42. package/dist/interceptors/workflow-ai.js.map +1 -1
  43. package/dist/observability.d.ts +69 -0
  44. package/dist/observability.d.ts.map +1 -0
  45. package/dist/observability.js +242 -0
  46. package/dist/observability.js.map +1 -0
  47. package/dist/portal-executor.d.ts +30 -0
  48. package/dist/portal-executor.d.ts.map +1 -0
  49. package/dist/portal-executor.js +304 -0
  50. package/dist/portal-executor.js.map +1 -0
  51. package/dist/portal-server.d.ts +3 -0
  52. package/dist/portal-server.d.ts.map +1 -0
  53. package/dist/portal-server.js +265 -0
  54. package/dist/portal-server.js.map +1 -0
  55. package/dist/telemetry-batcher.d.ts +43 -0
  56. package/dist/telemetry-batcher.d.ts.map +1 -0
  57. package/dist/telemetry-batcher.js +111 -0
  58. package/dist/telemetry-batcher.js.map +1 -0
  59. package/dist/trigger-executor.d.ts +12 -0
  60. package/dist/trigger-executor.d.ts.map +1 -0
  61. package/dist/trigger-executor.js +83 -0
  62. package/dist/trigger-executor.js.map +1 -0
  63. package/dist/types/portal.d.ts +64 -0
  64. package/dist/types/portal.d.ts.map +1 -0
  65. package/dist/types/portal.js +2 -0
  66. package/dist/types/portal.js.map +1 -0
  67. package/dist/utils/debug.d.ts +3 -0
  68. package/dist/utils/debug.d.ts.map +1 -0
  69. package/dist/utils/debug.js +8 -0
  70. package/dist/utils/debug.js.map +1 -0
  71. package/dist/utils/redact.d.ts +7 -0
  72. package/dist/utils/redact.d.ts.map +1 -0
  73. package/dist/utils/redact.js +26 -0
  74. package/dist/utils/redact.js.map +1 -0
  75. package/package.json +9 -1
  76. package/src/capture/event.ts +5 -1
  77. package/src/cli.ts +109 -0
  78. package/src/execution/tool-runner.ts +304 -0
  79. package/src/http.ts +2 -0
  80. package/src/index.ts +14 -0
  81. package/src/interceptors/ai-interceptor.ts +110 -4
  82. package/src/interceptors/db-auto.ts +121 -25
  83. package/src/interceptors/db.ts +92 -17
  84. package/src/interceptors/http.ts +145 -107
  85. package/src/interceptors/telemetry-push.ts +113 -13
  86. package/src/interceptors/tool.ts +42 -5
  87. package/src/interceptors/workflow-ai.ts +49 -2
  88. package/src/observability.ts +281 -0
  89. package/src/portal-executor.ts +335 -0
  90. package/src/portal-server.ts +290 -0
  91. package/src/telemetry-batcher.ts +143 -0
  92. package/src/trigger-executor.ts +121 -0
  93. package/src/types/portal.ts +67 -0
  94. package/src/utils/debug.ts +8 -0
  95. package/src/utils/redact.ts +25 -0
@@ -0,0 +1,8 @@
1
+ const DEBUG_KEY = 'ELASTICDASH_DEBUG';
2
+ /** Log only when ELASTICDASH_DEBUG=1 is set. Drop-in replacement for console.log in interceptors. */
3
+ export function debugLog(...args) {
4
+ if (typeof process !== 'undefined' && process.env?.[DEBUG_KEY] === '1') {
5
+ console.log(...args);
6
+ }
7
+ }
8
+ //# sourceMappingURL=debug.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debug.js","sourceRoot":"","sources":["../../src/utils/debug.ts"],"names":[],"mappings":"AAAA,MAAM,SAAS,GAAG,mBAAmB,CAAA;AAErC,qGAAqG;AACrG,MAAM,UAAU,QAAQ,CAAC,GAAG,IAAe;IACzC,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,KAAK,GAAG,EAAE,CAAC;QACvE,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAA;IACtB,CAAC;AACH,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Deep-clones a value and replaces any object property whose key matches
3
+ * one of `keys` (case-insensitive) with "[REDACTED]".
4
+ * Returns the original value when `keys` is empty.
5
+ */
6
+ export declare function redactPayload(value: unknown, keys: string[]): unknown;
7
+ //# sourceMappingURL=redact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/utils/redact.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAIrE"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Deep-clones a value and replaces any object property whose key matches
3
+ * one of `keys` (case-insensitive) with "[REDACTED]".
4
+ * Returns the original value when `keys` is empty.
5
+ */
6
+ export function redactPayload(value, keys) {
7
+ if (keys.length === 0)
8
+ return value;
9
+ const lowerKeys = new Set(keys.map((k) => k.toLowerCase()));
10
+ return redact(value, lowerKeys);
11
+ }
12
+ function redact(value, keys) {
13
+ if (value === null || value === undefined)
14
+ return value;
15
+ if (typeof value !== 'object')
16
+ return value;
17
+ if (Array.isArray(value)) {
18
+ return value.map((item) => redact(item, keys));
19
+ }
20
+ const out = {};
21
+ for (const [k, v] of Object.entries(value)) {
22
+ out[k] = keys.has(k.toLowerCase()) ? '[REDACTED]' : redact(v, keys);
23
+ }
24
+ return out;
25
+ }
26
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/utils/redact.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc,EAAE,IAAc;IAC1D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACnC,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAA;IAC3D,OAAO,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;AACjC,CAAC;AAED,SAAS,MAAM,CAAC,KAAc,EAAE,IAAiB;IAC/C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAE3C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;IAChD,CAAC;IAED,MAAM,GAAG,GAA4B,EAAE,CAAA;IACvC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;QACtE,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IACrE,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elasticdash-test",
3
- "version": "0.1.17",
3
+ "version": "0.1.18-alpha",
4
4
  "description": "AI-native test runner for ElasticDash workflow testing",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,14 @@
17
17
  "./http": {
18
18
  "types": "./dist/http.d.ts",
19
19
  "default": "./dist/http.js"
20
+ },
21
+ "./observability": {
22
+ "types": "./dist/observability.d.ts",
23
+ "default": "./dist/observability.js"
24
+ },
25
+ "./portal": {
26
+ "types": "./dist/portal-server.d.ts",
27
+ "default": "./dist/portal-server.js"
20
28
  }
21
29
  },
22
30
  "files": [
@@ -1,4 +1,4 @@
1
- export type WorkflowEventType = 'ai' | 'tool' | 'http' | 'db' | 'side_effect'
1
+ export type WorkflowEventType = 'ai' | 'tool' | 'http' | 'db' | 'side_effect' | 'workflow'
2
2
 
3
3
  export interface WorkflowEvent {
4
4
  id: number
@@ -18,6 +18,10 @@ export interface WorkflowEvent {
18
18
  streamed?: boolean
19
19
  /** Raw buffered text of a streamed response (used for replay) */
20
20
  streamRaw?: string
21
+ /** Schema version for forward compatibility (default 1) */
22
+ schemaVersion?: number
23
+ /** Optional request-level trace ID for grouping events within a session */
24
+ traceId?: string
21
25
  }
22
26
 
23
27
  export interface WorkflowTrace {
package/src/cli.ts CHANGED
@@ -12,6 +12,8 @@ import { runFiles } from './runner.js'
12
12
  import { reportResults } from './reporter.js'
13
13
  import { startBrowserUiServer, type UiEvent } from './browser-ui.js'
14
14
  import { startDashboardServer } from './dashboard-server.js'
15
+ import { initObservability, shutdownObservability } from './observability.js'
16
+ import { startPortalServer } from './portal-server.js'
15
17
 
16
18
  function stripAnsi(input?: string): string | undefined {
17
19
  if (!input) return input
@@ -306,6 +308,113 @@ async function bootstrap(): Promise<void> {
306
308
  process.once('SIGTERM', cleanup)
307
309
  })
308
310
 
311
+ // elasticdash observe
312
+ program
313
+ .command('observe')
314
+ .description('Start observability mode — stream trace events to ElasticDash backend')
315
+ .option('--server <url>', 'ElasticDash backend API URL', process.env.ELASTICDASH_API_URL)
316
+ .option('--api-key <key>', 'Project API key', process.env.ELASTICDASH_API_KEY)
317
+ .option('--service-id <id>', 'Service identifier', process.env.ELASTICDASH_SERVICE_ID)
318
+ .action(async (options: { server?: string; apiKey?: string; serviceId?: string }) => {
319
+ const serverUrl = options.server
320
+ if (!serverUrl) {
321
+ console.error('[elasticdash] Error: --server or ELASTICDASH_API_URL is required')
322
+ process.exit(1)
323
+ }
324
+
325
+ const handle = initObservability({
326
+ serverUrl,
327
+ apiKey: options.apiKey,
328
+ serviceId: options.serviceId,
329
+ })
330
+
331
+ console.log(`[elasticdash] Observability active`)
332
+ console.log(` Session ID : ${handle.sessionId}`)
333
+ console.log(` Server : ${serverUrl}`)
334
+ console.log(` Service : ${options.serviceId ?? process.env.ELASTICDASH_SERVICE_ID ?? 'unknown-service'}`)
335
+ console.log(`[elasticdash] Press Ctrl+C to stop`)
336
+
337
+ let isShuttingDown = false
338
+ const cleanup = async () => {
339
+ if (isShuttingDown) {
340
+ process.exit(1)
341
+ }
342
+ isShuttingDown = true
343
+ console.log('\n[elasticdash] Shutting down observability...')
344
+ await shutdownObservability()
345
+ process.exit(0)
346
+ }
347
+
348
+ process.once('SIGINT', cleanup)
349
+ process.once('SIGTERM', cleanup)
350
+ })
351
+
352
+ // elasticdash portal
353
+ program
354
+ .command('portal')
355
+ .description('Start a portal server to receive and execute rerun tasks from ElasticDash backend')
356
+ .option('--server <url>', 'ElasticDash backend API URL to POST results to', process.env.ELASTICDASH_API_URL)
357
+ .option('--api-key <key>', 'Project API key', process.env.ELASTICDASH_API_KEY)
358
+ .option('--port <port>', 'Portal server port', (v) => Number(v), process.env.ELASTICDASH_PORTAL_PORT ? Number(process.env.ELASTICDASH_PORTAL_PORT) : 4574)
359
+ .option('--allowed-origins <origins>', 'Comma-separated list of additional allowed origin domains', process.env.ELASTICDASH_ALLOWED_ORIGINS)
360
+ .action(async (options: { server?: string; apiKey?: string; port: number; allowedOrigins?: string }) => {
361
+ const backendUrl = options.server
362
+ if (!backendUrl) {
363
+ console.error('[elasticdash] Error: --server or ELASTICDASH_API_URL is required')
364
+ process.exit(1)
365
+ }
366
+
367
+ const allowedOrigins = options.allowedOrigins
368
+ ? options.allowedOrigins.split(',').map(s => s.trim()).filter(Boolean)
369
+ : undefined
370
+
371
+ const handle = await startPortalServer({
372
+ port: options.port,
373
+ backendUrl,
374
+ apiKey: options.apiKey,
375
+ cwd,
376
+ allowedOrigins,
377
+ })
378
+
379
+ console.log(`[elasticdash] Portal server running`)
380
+ console.log(` URL : ${handle.url}`)
381
+ console.log(` Backend : ${backendUrl}`)
382
+ console.log(` Port : ${handle.port}`)
383
+ console.log(`[elasticdash] Waiting for tasks from backend... Press Ctrl+C to stop`)
384
+
385
+ // Register with backend
386
+ try {
387
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' }
388
+ if (options.apiKey) headers['Authorization'] = `Bearer ${options.apiKey}`
389
+ const res = await fetch(`${backendUrl}/api/portal/register`, {
390
+ method: 'POST',
391
+ headers,
392
+ body: JSON.stringify({ portalUrl: handle.url }),
393
+ })
394
+ if (res.ok) {
395
+ console.log(`[elasticdash] Registered with backend`)
396
+ } else {
397
+ console.warn(`[elasticdash] Backend registration returned ${res.status} — portal will still accept tasks directly`)
398
+ }
399
+ } catch {
400
+ console.warn(`[elasticdash] Could not register with backend — portal will still accept tasks directly`)
401
+ }
402
+
403
+ let isShuttingDown = false
404
+ const cleanup = async () => {
405
+ if (isShuttingDown) {
406
+ process.exit(1)
407
+ }
408
+ isShuttingDown = true
409
+ console.log('\n[elasticdash] Shutting down portal...')
410
+ await handle.close()
411
+ process.exit(0)
412
+ }
413
+
414
+ process.once('SIGINT', cleanup)
415
+ process.once('SIGTERM', cleanup)
416
+ })
417
+
309
418
  await program.parseAsync(process.argv)
310
419
  }
311
420
 
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Shared tool/AI execution helpers for portal and dashboard.
3
+ * These are extracted versions of the helpers in dashboard-server.ts
4
+ * to avoid importing the full dashboard server module.
5
+ */
6
+ import path from 'node:path'
7
+ import { existsSync, readFileSync } from 'node:fs'
8
+ import { spawn } from 'node:child_process'
9
+ import { pathToFileURL } from 'node:url'
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Types
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface ToolInfo {
16
+ name: string
17
+ isAsync: boolean
18
+ signature: string
19
+ filePath: string
20
+ lineNumber?: number
21
+ sourceCode?: string
22
+ }
23
+
24
+ export interface RerunResult {
25
+ ok: boolean
26
+ currentOutput?: unknown
27
+ currentDurationMs?: number
28
+ currentUsage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number }
29
+ error?: string
30
+ }
31
+
32
+ interface ParsedExport {
33
+ name: string
34
+ isAsync: boolean
35
+ signature: string
36
+ filePath: string
37
+ lineNumber?: number
38
+ sourceCode?: string
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Runtime helpers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ export function isDenoProject(dir: string): boolean {
46
+ return existsSync(path.join(dir, 'deno.json')) || existsSync(path.join(dir, 'deno.jsonc'))
47
+ }
48
+
49
+ export function resolveRuntimeModule(cwd: string, baseName: string): string | null {
50
+ for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
51
+ const candidate = path.join(cwd, `${baseName}${ext}`)
52
+ if (existsSync(candidate)) return candidate
53
+ }
54
+ return null
55
+ }
56
+
57
+ function parseSignatureParams(signature?: string): string[] {
58
+ if (!signature) return []
59
+ const trimmed = signature.trim()
60
+ if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) return []
61
+ const body = trimmed.slice(1, -1).trim()
62
+ if (!body) return []
63
+ return body
64
+ .split(',')
65
+ .map(part => part.trim())
66
+ .filter(Boolean)
67
+ .map(part => part.replace(/^\.\.\./, '').split('=')[0].split(':')[0].replace(/\?/g, '').trim())
68
+ .filter(part => /^[$A-Z_][0-9A-Z_$]*$/i.test(part))
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Argument building
73
+ // ---------------------------------------------------------------------------
74
+
75
+ export function buildToolArgs(input: unknown, tool?: ToolInfo): unknown[] {
76
+ if (input === undefined) return []
77
+ if (Array.isArray(input)) return input
78
+ if (input && typeof input === 'object') {
79
+ const argObject = input as Record<string, unknown>
80
+ const paramNames = parseSignatureParams(tool?.signature)
81
+ if (paramNames.length > 0 && paramNames.every(name => Object.prototype.hasOwnProperty.call(argObject, name))) {
82
+ return paramNames.map(name => argObject[name])
83
+ }
84
+ return [input]
85
+ }
86
+ return [input]
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Subprocess execution
91
+ // ---------------------------------------------------------------------------
92
+
93
+ export function runToolInSubprocess(
94
+ toolsModulePath: string,
95
+ toolName: string,
96
+ args: unknown[],
97
+ ): Promise<RerunResult> {
98
+ return new Promise((resolve) => {
99
+ const startMs = Date.now()
100
+ const workerScript = new URL('../tool-runner-worker.js', import.meta.url).pathname
101
+ const projectDir = path.dirname(toolsModulePath)
102
+ const denoProject = isDenoProject(projectDir)
103
+
104
+ const nodeOptions = process.env.NODE_OPTIONS ?? ''
105
+ const tsxFlag = '--import tsx'
106
+ const childNodeOptions = nodeOptions.includes('tsx') ? nodeOptions : `${nodeOptions} ${tsxFlag}`.trim()
107
+ const childEnv = { ...process.env, NODE_OPTIONS: denoProject ? nodeOptions : childNodeOptions }
108
+
109
+ const runtime = denoProject ? 'deno' : process.execPath
110
+ const runtimeArgs = denoProject ? ['run', '--allow-all', workerScript] : [workerScript]
111
+
112
+ const child = spawn(runtime, runtimeArgs, {
113
+ env: childEnv,
114
+ cwd: projectDir,
115
+ stdio: ['pipe', 'pipe', 'pipe'],
116
+ })
117
+
118
+ const RESULT_PREFIX = '__ELASTICDASH_RESULT__:'
119
+ let resultLine = ''
120
+ let stderr = ''
121
+
122
+ child.stdout.on('data', (chunk: Buffer) => {
123
+ const text = chunk.toString()
124
+ for (const line of text.split('\n')) {
125
+ if (line.startsWith(RESULT_PREFIX)) {
126
+ resultLine = line.slice(RESULT_PREFIX.length)
127
+ } else if (line) {
128
+ process.stdout.write(line + '\n')
129
+ }
130
+ }
131
+ })
132
+ child.stderr.on('data', (chunk: Buffer) => {
133
+ stderr += chunk.toString()
134
+ process.stderr.write(chunk)
135
+ })
136
+
137
+ child.on('close', () => {
138
+ const currentDurationMs = Date.now() - startMs
139
+ if (resultLine) {
140
+ try {
141
+ resolve({ ...JSON.parse(resultLine), currentDurationMs })
142
+ return
143
+ } catch { /* fall through */ }
144
+ }
145
+ resolve({ ok: false, error: stderr.trim() || 'Tool subprocess produced no output.', currentDurationMs })
146
+ })
147
+
148
+ child.on('error', (err) => {
149
+ const hint = denoProject && (err as NodeJS.ErrnoException).code === 'ENOENT'
150
+ ? ' (Deno project detected — ensure "deno" is installed and available in PATH)'
151
+ : ''
152
+ resolve({ ok: false, error: `Failed to spawn tool subprocess: ${err.message}${hint}`, currentDurationMs: Date.now() - startMs })
153
+ })
154
+
155
+ const payload = JSON.stringify({
156
+ toolsModulePath: pathToFileURL(toolsModulePath).pathname,
157
+ toolName,
158
+ args,
159
+ })
160
+ child.stdin.write(payload)
161
+ child.stdin.end()
162
+ })
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Tool scanning (static analysis of ed_tools.ts/js)
167
+ // ---------------------------------------------------------------------------
168
+
169
+ function resolveModulePath(fromDir: string, specifier: string): string | null {
170
+ if (!specifier.startsWith('.')) return null
171
+ const exts = ['.ts', '.tsx', '.js', '.jsx', '']
172
+ for (const ext of exts) {
173
+ const candidate = path.resolve(fromDir, specifier + ext)
174
+ if (existsSync(candidate)) return candidate
175
+ }
176
+ return null
177
+ }
178
+
179
+ function lineAt(src: string, index: number): number {
180
+ return src.slice(0, index).split('\n').length
181
+ }
182
+
183
+ function extractSource(src: string, index: number): string {
184
+ const snippet = src.slice(index, index + 2000)
185
+ return snippet.length < 2000 ? snippet : snippet + '\n// (truncated)'
186
+ }
187
+
188
+ function findFunctionInSource(src: string, name: string): { isAsync: boolean; signature: string; lineNumber?: number; sourceCode?: string } {
189
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
190
+ let m = src.match(new RegExp(`export\\s+(async\\s+)?function\\s+${escaped}\\s*(\\([^)]*\\))`))
191
+ if (m) return { isAsync: !!m[1], signature: m[2], lineNumber: lineAt(src, m.index!), sourceCode: extractSource(src, m.index!) }
192
+ m = src.match(new RegExp(`(?:^|\\n)\\s*(?:async\\s+)?function\\s+${escaped}\\s*(\\([^)]*\\))`, 'm'))
193
+ if (m) return {
194
+ isAsync: new RegExp(`async\\s+function\\s+${escaped}`).test(src),
195
+ signature: m[1],
196
+ lineNumber: lineAt(src, m.index!),
197
+ sourceCode: extractSource(src, m.index!),
198
+ }
199
+ m = src.match(new RegExp(`export\\s+const\\s+${escaped}\\s*=\\s*(async\\s*)?(\\([^)]*\\))\\s*=>`))
200
+ if (m) return { isAsync: !!m[1], signature: m[2], lineNumber: lineAt(src, m.index!) }
201
+ m = src.match(new RegExp(`(?:^|\\n)\\s*const\\s+${escaped}\\s*=\\s*(async\\s*)?(\\([^)]*\\))\\s*=>`, 'm'))
202
+ if (m) return { isAsync: !!m[1], signature: m[2], lineNumber: lineAt(src, m.index!) }
203
+ return { isAsync: false, signature: '()' }
204
+ }
205
+
206
+ function extractExportsFromSource(filePath: string): ParsedExport[] {
207
+ let src: string
208
+ try {
209
+ src = readFileSync(filePath, 'utf8')
210
+ } catch {
211
+ return []
212
+ }
213
+ const dir = path.dirname(filePath)
214
+ const results: ParsedExport[] = []
215
+
216
+ // 1. Direct: export [async] function name(params) { … }
217
+ for (const m of src.matchAll(/export\s+(async\s+)?function\s+(\w+)\s*(\([^)]*\))/g)) {
218
+ results.push({
219
+ name: m[2], isAsync: !!m[1], signature: m[3], filePath,
220
+ lineNumber: lineAt(src, m.index!), sourceCode: extractSource(src, m.index!),
221
+ })
222
+ }
223
+
224
+ // 2. Direct: export const name = [async] (params) => …
225
+ for (const m of src.matchAll(/export\s+const\s+(\w+)\s*=\s*(async\s*)?\(([^)]*)\)\s*=>/g)) {
226
+ results.push({
227
+ name: m[1], isAsync: !!m[2], signature: `(${m[3]})`, filePath,
228
+ lineNumber: lineAt(src, m.index!), sourceCode: extractSource(src, m.index!),
229
+ })
230
+ }
231
+
232
+ // 3. Named re-exports: export { X [as Y], … } from './module'
233
+ for (const m of src.matchAll(/export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g)) {
234
+ const modulePath = resolveModulePath(dir, m[2])
235
+ let moduleSrc = ''
236
+ try { if (modulePath) moduleSrc = readFileSync(modulePath, 'utf8') } catch { /* ignore */ }
237
+ for (const spec of m[1].split(',')) {
238
+ const parts = spec.trim().split(/\s+as\s+/)
239
+ const originalName = parts[0].trim()
240
+ const exportedName = (parts[1] ?? parts[0]).trim()
241
+ if (!exportedName || exportedName === 'default') continue
242
+ const info = moduleSrc ? findFunctionInSource(moduleSrc, originalName) : { isAsync: false, signature: '()' }
243
+ results.push({
244
+ name: exportedName, isAsync: info.isAsync, signature: info.signature,
245
+ filePath: modulePath ?? filePath, lineNumber: info.lineNumber, sourceCode: info.sourceCode,
246
+ })
247
+ }
248
+ }
249
+
250
+ // 4. Import + destructure: import { obj } from './m' + export const { a, b } = obj
251
+ for (const imp of src.matchAll(/import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g)) {
252
+ const importedNames = imp[1].split(',').map(s => {
253
+ const parts = s.trim().split(/\s+as\s+/)
254
+ return { original: parts[0].trim(), local: (parts[1] ?? parts[0]).trim() }
255
+ }).filter(n => n.local)
256
+ const modulePath = resolveModulePath(dir, imp[2])
257
+ for (const { local } of importedNames) {
258
+ const destructureRe = new RegExp(`export\\s+const\\s+\\{([^}]+)\\}\\s*=\\s*${local.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
259
+ const dm = src.match(destructureRe)
260
+ if (!dm) continue
261
+ let moduleSrc = ''
262
+ try { if (modulePath) moduleSrc = readFileSync(modulePath, 'utf8') } catch { /* ignore */ }
263
+ for (const member of dm[1].split(',')) {
264
+ const name = member.trim()
265
+ if (!name) continue
266
+ const info = moduleSrc ? findFunctionInSource(moduleSrc, name) : { isAsync: false, signature: '()' }
267
+ results.push({
268
+ name, isAsync: info.isAsync, signature: info.signature,
269
+ filePath: modulePath ?? filePath, lineNumber: info.lineNumber, sourceCode: info.sourceCode,
270
+ })
271
+ }
272
+ }
273
+ }
274
+
275
+ return results
276
+ }
277
+
278
+ export function scanTools(cwd: string): ToolInfo[] {
279
+ for (const candidate of [path.join(cwd, 'ed_tools.ts'), path.join(cwd, 'ed_tools.js')]) {
280
+ if (!existsSync(candidate)) continue
281
+ const exports = extractExportsFromSource(candidate)
282
+ if (exports.length > 0) {
283
+ return exports.map(e => ({
284
+ name: e.name, isAsync: e.isAsync, signature: e.signature,
285
+ filePath: e.filePath, lineNumber: e.lineNumber, sourceCode: e.sourceCode,
286
+ }))
287
+ }
288
+ }
289
+ return []
290
+ }
291
+
292
+ export function scanWorkflows(cwd: string): ToolInfo[] {
293
+ for (const candidate of [path.join(cwd, 'ed_workflows.ts'), path.join(cwd, 'ed_workflows.js')]) {
294
+ if (!existsSync(candidate)) continue
295
+ const exports = extractExportsFromSource(candidate)
296
+ if (exports.length > 0) {
297
+ return exports.map(e => ({
298
+ name: e.name, isAsync: e.isAsync, signature: e.signature,
299
+ filePath: e.filePath, lineNumber: e.lineNumber, sourceCode: e.sourceCode,
300
+ }))
301
+ }
302
+ }
303
+ return []
304
+ }
package/src/http.ts CHANGED
@@ -6,3 +6,5 @@
6
6
  export { setHttpRunContext, initHttpRunContext, getHttpRunContext } from './interceptors/telemetry-push.js'
7
7
  export { wrapTool } from './interceptors/tool.js'
8
8
  export { wrapAI } from './interceptors/workflow-ai.js'
9
+ export { wrapDB, wrapPgClient, wrapKnex, wrapMongoCollection, wrapRedisClient } from './interceptors/db.js'
10
+ export { initObservability, shutdownObservability, startTrace, wrapWorkflow } from './observability.js'
package/src/index.ts CHANGED
@@ -71,6 +71,15 @@ export { interceptRandom, restoreRandom, interceptDateNow, restoreDateNow } from
71
71
  // AI interceptor (monkey-patch based)
72
72
  export { installAIInterceptor, uninstallAIInterceptor } from './interceptors/ai-interceptor.js'
73
73
 
74
+ // Observability
75
+ export { initObservability, shutdownObservability, startTrace, wrapWorkflow } from './observability.js'
76
+ export type { ObservabilityOptions, ObservabilityHandle } from './observability.js'
77
+ export { TelemetryBatcher } from './telemetry-batcher.js'
78
+ export type { TelemetryBatcherOptions, TriggerSignal, TriggerStep } from './telemetry-batcher.js'
79
+ export type { ObservabilityContext } from './interceptors/telemetry-push.js'
80
+ export { checkToolAvailability, checkAIAvailability } from './portal-executor.js'
81
+ export type { AvailabilityResult } from './portal-executor.js'
82
+
74
83
  // LLM proxy
75
84
  export { startLLMProxy, fetchCapturedTrace } from './proxy/llm-capture.js'
76
85
 
@@ -85,3 +94,8 @@ export type { RunWorkflowOptions, WorkflowRunResult } from './workflow-runner.js
85
94
  // Agent mid-trace replay
86
95
  export { serializeAgentState, deserializeAgentState, extractTaskOutputs, resolveTaskInput } from './core/agent-state.js'
87
96
  export type { AgentTask, AgentPlan, AgentState, AgentTaskStatus, AgentPlanStatus } from './types/agent.js'
97
+
98
+ // Portal (remote rerun queue)
99
+ export { startPortalServer } from './portal-server.js'
100
+ export { executePortalTask } from './portal-executor.js'
101
+ export type { PortalTask, PortalTaskResult, PortalServerOptions, PortalServerHandle, PortalStatus } from './types/portal.js'