elasticdash-test 0.1.10-alpha → 0.1.11-alpha-2
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.
- package/README.md +328 -0
- package/dist/browser-ui.js +0 -4
- package/dist/browser-ui.js.map +1 -1
- package/dist/capture/event.d.ts +19 -0
- package/dist/capture/event.d.ts.map +1 -0
- package/dist/capture/event.js +2 -0
- package/dist/capture/event.js.map +1 -0
- package/dist/capture/index.d.ts +4 -0
- package/dist/capture/index.d.ts.map +1 -0
- package/dist/capture/index.js +4 -0
- package/dist/capture/index.js.map +1 -0
- package/dist/capture/recorder.d.ts +19 -0
- package/dist/capture/recorder.d.ts.map +1 -0
- package/dist/capture/recorder.js +36 -0
- package/dist/capture/recorder.js.map +1 -0
- package/dist/capture/replay.d.ts +20 -0
- package/dist/capture/replay.d.ts.map +1 -0
- package/dist/capture/replay.js +47 -0
- package/dist/capture/replay.js.map +1 -0
- package/dist/cli.js +48 -2
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-state.d.ts +47 -0
- package/dist/core/agent-state.d.ts.map +1 -0
- package/dist/core/agent-state.js +137 -0
- package/dist/core/agent-state.js.map +1 -0
- package/dist/dashboard-server.d.ts +36 -0
- package/dist/dashboard-server.d.ts.map +1 -0
- package/dist/dashboard-server.js +2658 -0
- package/dist/dashboard-server.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/ai-interceptor.d.ts.map +1 -1
- package/dist/interceptors/ai-interceptor.js +221 -10
- package/dist/interceptors/ai-interceptor.js.map +1 -1
- package/dist/interceptors/db-auto.d.ts +8 -0
- package/dist/interceptors/db-auto.d.ts.map +1 -0
- package/dist/interceptors/db-auto.js +125 -0
- package/dist/interceptors/db-auto.js.map +1 -0
- package/dist/interceptors/db.d.ts +18 -0
- package/dist/interceptors/db.d.ts.map +1 -0
- package/dist/interceptors/db.js +59 -0
- package/dist/interceptors/db.js.map +1 -0
- package/dist/interceptors/http.d.ts +3 -0
- package/dist/interceptors/http.d.ts.map +1 -0
- package/dist/interceptors/http.js +172 -0
- package/dist/interceptors/http.js.map +1 -0
- package/dist/interceptors/side-effects.d.ts +7 -0
- package/dist/interceptors/side-effects.d.ts.map +1 -0
- package/dist/interceptors/side-effects.js +72 -0
- package/dist/interceptors/side-effects.js.map +1 -0
- package/dist/interceptors/tool.d.ts +2 -0
- package/dist/interceptors/tool.d.ts.map +1 -0
- package/dist/interceptors/tool.js +75 -0
- package/dist/interceptors/tool.js.map +1 -0
- package/dist/interceptors/workflow-ai.d.ts +2 -0
- package/dist/interceptors/workflow-ai.d.ts.map +1 -0
- package/dist/interceptors/workflow-ai.js +26 -0
- package/dist/interceptors/workflow-ai.js.map +1 -0
- package/dist/internals/async-storage.d.ts +21 -0
- package/dist/internals/async-storage.d.ts.map +1 -0
- package/dist/internals/async-storage.js +46 -0
- package/dist/internals/async-storage.js.map +1 -0
- package/dist/internals/conditional-recorder.d.ts +21 -0
- package/dist/internals/conditional-recorder.d.ts.map +1 -0
- package/dist/internals/conditional-recorder.js +54 -0
- package/dist/internals/conditional-recorder.js.map +1 -0
- package/dist/internals/node-crypto.d.ts +12 -0
- package/dist/internals/node-crypto.d.ts.map +1 -0
- package/dist/internals/node-crypto.js +38 -0
- package/dist/internals/node-crypto.js.map +1 -0
- package/dist/matchers/index.d.ts +2 -1
- package/dist/matchers/index.d.ts.map +1 -1
- package/dist/matchers/index.js +28 -1
- package/dist/matchers/index.js.map +1 -1
- package/dist/proxy/llm-capture.d.ts +14 -0
- package/dist/proxy/llm-capture.d.ts.map +1 -0
- package/dist/proxy/llm-capture.js +264 -0
- package/dist/proxy/llm-capture.js.map +1 -0
- package/dist/runWorkflowSubprocess.d.ts +14 -0
- package/dist/runWorkflowSubprocess.d.ts.map +1 -0
- package/dist/runWorkflowSubprocess.js +66 -0
- package/dist/runWorkflowSubprocess.js.map +1 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +36 -2
- package/dist/runner.js.map +1 -1
- package/dist/tool-runner-worker.d.ts +12 -0
- package/dist/tool-runner-worker.d.ts.map +1 -0
- package/dist/tool-runner-worker.js +52 -0
- package/dist/tool-runner-worker.js.map +1 -0
- package/dist/trace-adapter/context.d.ts +2 -0
- package/dist/trace-adapter/context.d.ts.map +1 -1
- package/dist/trace-adapter/context.js +10 -4
- package/dist/trace-adapter/context.js.map +1 -1
- package/dist/tracing-universal.d.ts +13 -0
- package/dist/tracing-universal.d.ts.map +1 -0
- package/dist/tracing-universal.js +33 -0
- package/dist/tracing-universal.js.map +1 -0
- package/dist/tracing.d.ts +2 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/tracing.js +57 -0
- package/dist/tracing.js.map +1 -0
- package/dist/workflow-runner-worker.d.ts +2 -0
- package/dist/workflow-runner-worker.d.ts.map +1 -0
- package/dist/workflow-runner-worker.js +253 -0
- package/dist/workflow-runner-worker.js.map +1 -0
- package/dist/workflow-runner.d.ts +14 -0
- package/dist/workflow-runner.d.ts.map +1 -0
- package/dist/workflow-runner.js +30 -0
- package/dist/workflow-runner.js.map +1 -0
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -285,6 +285,99 @@ aiTest('user lookup flow', async (ctx) => {
|
|
|
285
285
|
|
|
286
286
|
**Libraries using `https.request` directly** (older versions of some SDKs) are not covered by fetch interception. Use manual `ctx.trace.recordLLMStep()` for those.
|
|
287
287
|
|
|
288
|
+
## Automatic Tool Recording
|
|
289
|
+
|
|
290
|
+
Tool functions are automatically wrapped and recorded when imported and called during workflow execution. This gives you tracing and replay support without any manual instrumentation.
|
|
291
|
+
|
|
292
|
+
### How to use tools
|
|
293
|
+
|
|
294
|
+
Define your tools in `ed_tools.ts` and export them:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
// ed_tools.ts
|
|
298
|
+
import { wrapTool } from 'elasticdash-test'
|
|
299
|
+
|
|
300
|
+
export const chargeCard = wrapTool('chargeCard', async (input: { amount: number; cardToken: string }) => {
|
|
301
|
+
// your actual implementation
|
|
302
|
+
return { success: true, transactionId: 'txn-123' }
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
export const fetchOrderStatus = wrapTool('fetchOrderStatus', async (input: { orderId: string }) => {
|
|
306
|
+
// query your database or API
|
|
307
|
+
return { status: 'shipped', tracking: 'track-456' }
|
|
308
|
+
})
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
In your workflows or agents, simply import and call them — the runner automatically wraps them and records each call:
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
import { chargeCard, fetchOrderStatus } from './ed_tools'
|
|
315
|
+
|
|
316
|
+
export async function checkoutFlow(orderId: string, cardToken: string) {
|
|
317
|
+
const order = await fetchOrderStatus({ orderId })
|
|
318
|
+
const charge = await chargeCard({ amount: order.total, cardToken })
|
|
319
|
+
return { orderId, chargeId: charge.transactionId }
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Each call to a tool is automatically recorded to `ctx.trace` with:
|
|
324
|
+
- Tool name
|
|
325
|
+
- Input arguments
|
|
326
|
+
- Output result
|
|
327
|
+
- Timestamp and duration
|
|
328
|
+
|
|
329
|
+
No `ctx.trace.recordToolCall()` needed — it happens behind the scenes.
|
|
330
|
+
|
|
331
|
+
Runtime behavior by mode:
|
|
332
|
+
- Local/normal execution: wrapped tools run normally (no replay context).
|
|
333
|
+
- Trace capture mode: wrapped tools run normally and are recorded.
|
|
334
|
+
- Replay mode (`Run from here`): pre-checkpoint wrapped calls return historical results and skip tool body execution.
|
|
335
|
+
|
|
336
|
+
### Tool replay and freezing in "Run from here"
|
|
337
|
+
|
|
338
|
+
When you use the dashboard's "Run from here" feature to replay a workflow from a checkpoint:
|
|
339
|
+
|
|
340
|
+
1. **Tools called before the checkpoint** are **frozen** — their recorded results are returned instantly without re-executing the actual function.
|
|
341
|
+
2. **Tools called after the checkpoint** execute normally with live dependencies.
|
|
342
|
+
|
|
343
|
+
This means:
|
|
344
|
+
- If a tool made 3 API calls before the checkpoint, those calls won't happen again on replay.
|
|
345
|
+
- If the tool is slow (waiting for external services), frozen calls are instant.
|
|
346
|
+
- The tool's frozen status is visible in the dashboard with a frozen tag/styling, matching AI step behavior.
|
|
347
|
+
- Side-effect replay is type-checked (`Date.now` vs `Math.random`) and malformed observation timestamps are auto-sanitized.
|
|
348
|
+
|
|
349
|
+
Example:
|
|
350
|
+
```
|
|
351
|
+
Task 1: fetchOrderStatus() ↓ frozen (checkpoint before this)
|
|
352
|
+
│─ API call to orders service ↓ frozen
|
|
353
|
+
└─ Result: { status: shipped } ↓ frozen
|
|
354
|
+
|
|
355
|
+
→ You modify input for Task 2 and re-run
|
|
356
|
+
The fetchOrderStatus API call is skipped; the original result is reused instantly.
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Recording tool calls manually
|
|
360
|
+
|
|
361
|
+
If you need to record tools outside the normal import flow or for providers not in `ed_tools.ts`, use:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
ctx.trace.recordToolCall({
|
|
365
|
+
name: 'customTool',
|
|
366
|
+
args: { param: 'value' },
|
|
367
|
+
result: { success: true }
|
|
368
|
+
})
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Manual recording is best-effort trace logging. For full replay/freeze semantics in "Run from here" (including frozen tool tags and deterministic replay), run tools through the replay-aware wrapper path (auto-wrapped worker tools or `wrapTool(...)`).
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
import { wrapTool } from 'elasticdash-test'
|
|
375
|
+
|
|
376
|
+
export const customTool = wrapTool('customTool', async (input: { param: string }) => {
|
|
377
|
+
return await doWork(input)
|
|
378
|
+
})
|
|
379
|
+
```
|
|
380
|
+
|
|
288
381
|
### Recording flow steps without passing `ctx.trace` (AsyncLocalStorage)
|
|
289
382
|
|
|
290
383
|
The runner now sets a per-test `currentTrace` using Node’s `AsyncLocalStorage`, so your app code can record steps without threading `ctx.trace` through every function. This remains safe under parallel execution.
|
|
@@ -319,6 +412,12 @@ Notes:
|
|
|
319
412
|
- Works per async context; if you spawn detached work (child processes/independent workers), pass `trace` explicitly there.
|
|
320
413
|
- Still compatible with manual DI: you can continue passing `ctx.trace` explicitly if you prefer.
|
|
321
414
|
|
|
415
|
+
### Optional local LLM capture proxy (for Supabase Edge / Deno)
|
|
416
|
+
- Opt in by setting `ELASTICDASH_LLM_PROXY=1` (optional: `ELASTICDASH_LLM_PROXY_PORT`, default `8787`). The runner will start a local proxy and generate a per-test `ELASTICDASH_TRACE_ID`.
|
|
417
|
+
- Point your LLM client at the proxy via base URL envs (e.g., `OPENAI_BASE_URL=http://localhost:8787/v1`, `ANTHROPIC_API_URL=http://localhost:8787`). No code changes in your workflow; only env overrides when running tests.
|
|
418
|
+
- Forward the trace ID to your Edge/Deno code (e.g., add `x-trace-id: process.env.ELASTICDASH_TRACE_ID` on the request to your Supabase Edge function). The proxy records model/prompt/completion (or `(streamed)`) and the runner folds captured steps back into `ctx.trace` after each test.
|
|
419
|
+
- When `ELASTICDASH_LLM_PROXY` is unset, behavior is unchanged and the existing Node fetch interceptor remains the default.
|
|
420
|
+
|
|
322
421
|
---
|
|
323
422
|
|
|
324
423
|
## Configuration
|
|
@@ -361,6 +460,207 @@ This gives you typed `aiTest`, `beforeAll`, `afterAll` globals and typed custom
|
|
|
361
460
|
|
|
362
461
|
---
|
|
363
462
|
|
|
463
|
+
## Workflows Dashboard
|
|
464
|
+
|
|
465
|
+
Browse and search all available workflow functions in your project:
|
|
466
|
+
|
|
467
|
+
```bash
|
|
468
|
+
elasticdash dashboard # open dashboard at http://localhost:4573
|
|
469
|
+
elasticdash dashboard --port 4572 # use custom port
|
|
470
|
+
elasticdash dashboard --no-open # skip auto-opening browser
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
The dashboard scans your workflow/tool files and displays:
|
|
474
|
+
- If both `.ts` and `.js` versions of a file exist (e.g., `ed_workflows.ts` and `ed_workflows.js`), the dashboard will always use the `.ts` file.
|
|
475
|
+
- If only `.ts` exists, it will be automatically transpiled to `.js` before scanning/importing—no manual build step required.
|
|
476
|
+
- If only `.js` exists, it will be used directly.
|
|
477
|
+
|
|
478
|
+
This means you can write your workflows and tools in TypeScript, and the dashboard will handle transpilation automatically. You do not need to run `tsc` or build manually for dashboard usage.
|
|
479
|
+
|
|
480
|
+
**Example file selection logic:**
|
|
481
|
+
| Scenario | File Used |
|
|
482
|
+
|-------------------------|------------------|
|
|
483
|
+
| Only `ed_workflows.ts` | Transpiled `.ts` |
|
|
484
|
+
| Only `ed_workflows.js` | `.js` |
|
|
485
|
+
| Both exist | `.ts` (preferred)|
|
|
486
|
+
|
|
487
|
+
The dashboard displays:
|
|
488
|
+
- **Function names** — all exported functions in the module
|
|
489
|
+
- **Signatures** — function parameters and return types
|
|
490
|
+
- **Async indicator** — marks async vs sync functions
|
|
491
|
+
- **Source module** — where the function is imported from (if re-exported)
|
|
492
|
+
- **File path** — location of the workflow file
|
|
493
|
+
|
|
494
|
+
Use the search field to filter workflows by:
|
|
495
|
+
- **Name** — find workflow by function name (e.g., `checkoutFlow`)
|
|
496
|
+
- **Source module** — find all workflows from a specific module (e.g., `app.workflows`)
|
|
497
|
+
- **File path** — filter by location in your codebase
|
|
498
|
+
|
|
499
|
+
This is useful for discovering available workflows, understanding their signatures, and identifying where functions are defined before calling them in tests.
|
|
500
|
+
|
|
501
|
+
### `ed_workflows.ts`, `ed_tools.ts`, `ed_agents.ts`
|
|
502
|
+
|
|
503
|
+
These optional files bundle and re-export existing functions from your codebase for use in tests.
|
|
504
|
+
|
|
505
|
+
#### `ed_workflows.ts`
|
|
506
|
+
|
|
507
|
+
Re-export workflow functions from your application:
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
// ed_workflows.ts
|
|
511
|
+
export { orderWorkflow, refundWorkflow } from './src/workflows'
|
|
512
|
+
export { userLookupFlow } from './src/user-flows'
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Access in tests:
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
import { orderWorkflow } from './ed_workflows'
|
|
519
|
+
|
|
520
|
+
aiTest('full order workflow', async (ctx) => {
|
|
521
|
+
const result = await orderWorkflow('order-123', 'cust-456')
|
|
522
|
+
expect(ctx.trace).toCallTool('chargeCard')
|
|
523
|
+
})
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
#### `ed_tools.ts`
|
|
527
|
+
|
|
528
|
+
Re-export tool functions that agents or workflows can invoke:
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
// ed_tools.ts
|
|
532
|
+
import { wrapTool } from 'elasticdash-test'
|
|
533
|
+
import { chargeCard as chargeCardRaw, fetchOrderStatus as fetchOrderStatusRaw, sendNotification as sendNotificationRaw } from './src/tools'
|
|
534
|
+
|
|
535
|
+
export const chargeCard = wrapTool('chargeCard', chargeCardRaw)
|
|
536
|
+
export const fetchOrderStatus = wrapTool('fetchOrderStatus', fetchOrderStatusRaw)
|
|
537
|
+
export const sendNotification = wrapTool('sendNotification', sendNotificationRaw)
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
#### `ed_agents.ts`
|
|
541
|
+
|
|
542
|
+
Re-export agent functions or create a config object:
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
// ed_agents.ts
|
|
546
|
+
export { checkoutAgent, paymentAgent } from './src/agents'
|
|
547
|
+
|
|
548
|
+
// Or as a config object:
|
|
549
|
+
export const agents = {
|
|
550
|
+
checkout: checkoutAgent,
|
|
551
|
+
payment: paymentAgent,
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
The dashboard command will scan these files and display all exported functions with their signatures, making it easy to explore your workflow API.
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## Agent Mid-Trace Replay
|
|
560
|
+
|
|
561
|
+
ElasticDash supports resuming a long-running agent from any task in its plan — without re-executing already-completed steps. This is useful for:
|
|
562
|
+
|
|
563
|
+
- **Resuming after failures**: If task 3 of 5 fails, fix the issue and re-run from task 3 only.
|
|
564
|
+
- **Pausing for approval**: Capture state after task 2, get human sign-off, then continue.
|
|
565
|
+
- **Debugging in isolation**: Re-run a single task with modified input to diagnose a problem.
|
|
566
|
+
|
|
567
|
+
### How it works
|
|
568
|
+
|
|
569
|
+
Agents are structured as an **AgentPlan** — an ordered list of **AgentTask** objects, each with a tool name, input, and output. When serialized, the plan plus all captured trace events form an **AgentState** that can be saved to disk/database and replayed later.
|
|
570
|
+
|
|
571
|
+
### Quick start
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
import { plannerAgent, executorAgent, resumeAgentFromTrace } from './ed_agents'
|
|
575
|
+
import { serializeAgentState, deserializeAgentState } from 'elasticdash-test'
|
|
576
|
+
import fs from 'node:fs'
|
|
577
|
+
|
|
578
|
+
// 1. Generate a plan
|
|
579
|
+
const plan = await plannerAgent('Show me sales for Q1', { userToken: 'tok-abc' })
|
|
580
|
+
|
|
581
|
+
// 2. Execute the plan (runs all tasks sequentially)
|
|
582
|
+
const completedPlan = await executorAgent(plan)
|
|
583
|
+
|
|
584
|
+
// 3. Serialize and save state (e.g., after partial execution)
|
|
585
|
+
const state = serializeAgentState(completedPlan, [] /* pass recorder.events in worker context */)
|
|
586
|
+
fs.writeFileSync('agent-state.json', JSON.stringify(state, null, 2))
|
|
587
|
+
|
|
588
|
+
// 4. Later: load saved state and resume from task 2 (0-based index 1)
|
|
589
|
+
const savedState = JSON.parse(fs.readFileSync('agent-state.json', 'utf8'))
|
|
590
|
+
const stateToResume = deserializeAgentState({ ...savedState, resumeFromTaskIndex: 1 })
|
|
591
|
+
const resumedPlan = await resumeAgentFromTrace(stateToResume)
|
|
592
|
+
|
|
593
|
+
console.log('Resumed plan status:', resumedPlan.status)
|
|
594
|
+
console.log('Task outputs:', resumedPlan.tasks.map((t) => ({ id: t.id, status: t.status })))
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### AgentState structure
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
interface AgentState {
|
|
601
|
+
plan: AgentPlan // Full plan with all tasks (completed and pending)
|
|
602
|
+
trace: WorkflowEvent[] // Captured trace events from previous execution
|
|
603
|
+
resumeFromTaskIndex: number // Zero-based index — tasks before this are loaded from cache
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
interface AgentPlan {
|
|
607
|
+
id: string
|
|
608
|
+
tasks: AgentTask[]
|
|
609
|
+
status: 'planning' | 'executing' | 'completed' | 'failed' | 'paused'
|
|
610
|
+
currentTaskIndex: number
|
|
611
|
+
context: Record<string, unknown>
|
|
612
|
+
metadata: Record<string, unknown>
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
interface AgentTask {
|
|
616
|
+
id: string
|
|
617
|
+
status: 'pending' | 'in-progress' | 'completed' | 'failed'
|
|
618
|
+
description: string
|
|
619
|
+
tool: string // Name of the tool function to invoke
|
|
620
|
+
input: unknown // May contain { $ref: "task-N.output.fieldName" } placeholders
|
|
621
|
+
output?: unknown // Populated after execution
|
|
622
|
+
error?: string
|
|
623
|
+
startedAt?: number
|
|
624
|
+
completedAt?: number
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### Task input placeholders
|
|
629
|
+
|
|
630
|
+
Task inputs can reference previous task outputs using `{ $ref: "taskId.output.fieldPath" }`:
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
// task-2 uses the embedding produced by task-1
|
|
634
|
+
{
|
|
635
|
+
id: 'task-2',
|
|
636
|
+
tool: 'taskSelectorService',
|
|
637
|
+
input: {
|
|
638
|
+
queryEmbedding: { $ref: 'task-1.output.embedding' },
|
|
639
|
+
topK: 3,
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Placeholders are resolved at execution time by `resolveTaskInput()`.
|
|
645
|
+
|
|
646
|
+
### Dashboard integration
|
|
647
|
+
|
|
648
|
+
When running an agent workflow through the dashboard:
|
|
649
|
+
|
|
650
|
+
1. Agent task observations are **visually highlighted** with a purple background and left border, making them easy to identify.
|
|
651
|
+
2. Each agent observation shows a **T1 / T2 / T3** badge indicating which task it belongs to.
|
|
652
|
+
3. In the observation detail panel, a **"Resume from Task N"** button appears (agent steps only — non-agent steps have no button).
|
|
653
|
+
4. Clicking it calls `/api/resume-agent-from-task` with the serialized `AgentState` and the chosen `taskIndex`, then adds the resumed run as a new trace in the comparison table.
|
|
654
|
+
|
|
655
|
+
### Best practices for resumable agents
|
|
656
|
+
|
|
657
|
+
- **Keep tasks idempotent** where possible — if a task must be re-run, ensure it produces the same result.
|
|
658
|
+
- **Store minimal outputs** — only record what downstream tasks need, not full API responses.
|
|
659
|
+
- **Version your state schema** — if tool interfaces change, old states may need migration.
|
|
660
|
+
- **Use sequential tasks** — the current implementation runs tasks one-by-one; parallel task support is a planned future enhancement.
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
364
664
|
## Project Structure
|
|
365
665
|
|
|
366
666
|
```
|
|
@@ -398,6 +698,34 @@ uninstallAIInterceptor() // restore original fetch when done
|
|
|
398
698
|
|
|
399
699
|
---
|
|
400
700
|
|
|
701
|
+
## Recording Tool Calls Explicitly
|
|
702
|
+
|
|
703
|
+
If you want to ensure tool calls are always recorded in the workflow trace—regardless of how your tools are imported or used—you can use the `recordToolCall` utility provided by this SDK.
|
|
704
|
+
|
|
705
|
+
### How to Use
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
1. Import the function in your tool implementation:
|
|
709
|
+
|
|
710
|
+
```ts
|
|
711
|
+
import { recordToolCall } from 'elasticdash-test'
|
|
712
|
+
|
|
713
|
+
export async function myTool(input: string) {
|
|
714
|
+
// ...tool logic...
|
|
715
|
+
const result = `Hello, ${input}!`
|
|
716
|
+
recordToolCall('myTool', { input }, result)
|
|
717
|
+
return result
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
2. When running under ElasticDash, all calls to `recordToolCall` will be captured in the workflow trace. When running locally or outside the runner, this function is a no-op and will not affect your code.
|
|
722
|
+
|
|
723
|
+
**This approach is robust and works with both imported and global tools.**
|
|
724
|
+
|
|
725
|
+
**Summary:**
|
|
726
|
+
- Use tools as globals (no import) for full traceability and automatic tool call capture.
|
|
727
|
+
- This approach keeps your workflow code clean and enables powerful debugging and analytics in ElasticDash.
|
|
728
|
+
|
|
401
729
|
## Non-Goals
|
|
402
730
|
|
|
403
731
|
This runner intentionally does not support:
|
package/dist/browser-ui.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
import os from 'node:os';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
3
|
import { spawn } from 'node:child_process';
|
|
6
4
|
const defaultHtml = `<!doctype html>
|
|
7
5
|
<html lang="en">
|
|
@@ -149,8 +147,6 @@ const defaultHtml = `<!doctype html>
|
|
|
149
147
|
export async function startBrowserUiServer(opts = {}) {
|
|
150
148
|
const autoOpen = opts.autoOpen !== false;
|
|
151
149
|
let port = opts.port ?? 4571;
|
|
152
|
-
// Ensure base dir for potential static assets (none now)
|
|
153
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
154
150
|
const clients = [];
|
|
155
151
|
const eventBuffer = [];
|
|
156
152
|
const handler = (req, res) => {
|
package/dist/browser-ui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-ui.js","sourceRoot":"","sources":["../src/browser-ui.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"browser-ui.js","sourceRoot":"","sources":["../src/browser-ui.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type WorkflowEventType = 'ai' | 'tool' | 'http' | 'db' | 'side_effect';
|
|
2
|
+
export interface WorkflowEvent {
|
|
3
|
+
id: number;
|
|
4
|
+
type: WorkflowEventType;
|
|
5
|
+
name: string;
|
|
6
|
+
input: unknown;
|
|
7
|
+
output: unknown;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
durationMs: number;
|
|
10
|
+
/** Optional: ID of the agent task that produced this event */
|
|
11
|
+
agentTaskId?: string;
|
|
12
|
+
/** Optional: Zero-based index of the agent task that produced this event */
|
|
13
|
+
agentTaskIndex?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface WorkflowTrace {
|
|
16
|
+
traceId: string;
|
|
17
|
+
events: WorkflowEvent[];
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=event.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../src/capture/event.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,aAAa,CAAA;AAE7E,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,iBAAiB,CAAA;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,MAAM,EAAE,OAAO,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,aAAa,EAAE,CAAA;CACxB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event.js","sourceRoot":"","sources":["../../src/capture/event.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/capture/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/capture/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { WorkflowEvent, WorkflowTrace } from './event.js';
|
|
2
|
+
import type { ReplayController } from './replay.js';
|
|
3
|
+
export declare class TraceRecorder {
|
|
4
|
+
events: WorkflowEvent[];
|
|
5
|
+
private _counter;
|
|
6
|
+
private _sideEffectCounter;
|
|
7
|
+
record(event: WorkflowEvent): void;
|
|
8
|
+
nextId(): number;
|
|
9
|
+
/** Separate counter for Date.now / Math.random — never shares IDs with main events. */
|
|
10
|
+
nextSideEffectId(): number;
|
|
11
|
+
toTrace(traceId?: string): WorkflowTrace;
|
|
12
|
+
}
|
|
13
|
+
export interface CaptureContext {
|
|
14
|
+
recorder: TraceRecorder;
|
|
15
|
+
replay: ReplayController;
|
|
16
|
+
}
|
|
17
|
+
export declare function setCaptureContext(ctx: CaptureContext | undefined): void;
|
|
18
|
+
export declare function getCaptureContext(): CaptureContext | undefined;
|
|
19
|
+
//# sourceMappingURL=recorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../src/capture/recorder.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD,qBAAa,aAAa;IACxB,MAAM,EAAE,aAAa,EAAE,CAAK;IAC5B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,kBAAkB,CAAI;IAE9B,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAIlC,MAAM,IAAI,MAAM;IAIhB,uFAAuF;IACvF,gBAAgB,IAAI,MAAM;IAI1B,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,aAAa;CAMzC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,aAAa,CAAA;IACvB,MAAM,EAAE,gBAAgB,CAAA;CACzB;AASD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,CAEvE;AAED,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,SAAS,CAE9D"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
export class TraceRecorder {
|
|
4
|
+
events = [];
|
|
5
|
+
_counter = 0;
|
|
6
|
+
_sideEffectCounter = 0;
|
|
7
|
+
record(event) {
|
|
8
|
+
this.events.push(event);
|
|
9
|
+
}
|
|
10
|
+
nextId() {
|
|
11
|
+
return ++this._counter;
|
|
12
|
+
}
|
|
13
|
+
/** Separate counter for Date.now / Math.random — never shares IDs with main events. */
|
|
14
|
+
nextSideEffectId() {
|
|
15
|
+
return ++this._sideEffectCounter;
|
|
16
|
+
}
|
|
17
|
+
toTrace(traceId) {
|
|
18
|
+
return {
|
|
19
|
+
traceId: traceId ?? randomUUID(),
|
|
20
|
+
events: [...this.events],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const g = globalThis;
|
|
25
|
+
const CAPTURE_ALS_KEY = '__elasticdash_capture_als__';
|
|
26
|
+
const captureAls = g[CAPTURE_ALS_KEY] ??
|
|
27
|
+
new AsyncLocalStorage();
|
|
28
|
+
if (!g[CAPTURE_ALS_KEY])
|
|
29
|
+
g[CAPTURE_ALS_KEY] = captureAls;
|
|
30
|
+
export function setCaptureContext(ctx) {
|
|
31
|
+
captureAls.enterWith(ctx);
|
|
32
|
+
}
|
|
33
|
+
export function getCaptureContext() {
|
|
34
|
+
return captureAls.getStore();
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.js","sourceRoot":"","sources":["../../src/capture/recorder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAIxC,MAAM,OAAO,aAAa;IACxB,MAAM,GAAoB,EAAE,CAAA;IACpB,QAAQ,GAAG,CAAC,CAAA;IACZ,kBAAkB,GAAG,CAAC,CAAA;IAE9B,MAAM,CAAC,KAAoB;QACzB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACzB,CAAC;IAED,MAAM;QACJ,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAA;IACxB,CAAC;IAED,uFAAuF;IACvF,gBAAgB;QACd,OAAO,EAAE,IAAI,CAAC,kBAAkB,CAAA;IAClC,CAAC;IAED,OAAO,CAAC,OAAgB;QACtB,OAAO;YACL,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE;YAChC,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;SACzB,CAAA;IACH,CAAC;CACF;AAOD,MAAM,CAAC,GAAG,UAAqC,CAAA;AAC/C,MAAM,eAAe,GAAG,6BAA6B,CAAA;AACrD,MAAM,UAAU,GACb,CAAC,CAAC,eAAe,CAAmD;IACrE,IAAI,iBAAiB,EAA8B,CAAA;AACrD,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC;IAAE,CAAC,CAAC,eAAe,CAAC,GAAG,UAAU,CAAA;AAExD,MAAM,UAAU,iBAAiB,CAAC,GAA+B;IAC/D,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;AAC3B,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,UAAU,CAAC,QAAQ,EAAE,CAAA;AAC9B,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WorkflowEvent } from './event.js';
|
|
2
|
+
export declare class ReplayController {
|
|
3
|
+
replayMode: boolean;
|
|
4
|
+
checkpoint: number;
|
|
5
|
+
history: WorkflowEvent[];
|
|
6
|
+
private historyMap;
|
|
7
|
+
/** Side effects keyed by their assigned sideEffectId, independent of main event IDs */
|
|
8
|
+
private sideEffectMap;
|
|
9
|
+
constructor(replayMode: boolean, checkpoint: number, history: WorkflowEvent[]);
|
|
10
|
+
shouldReplay(eventId: number): boolean;
|
|
11
|
+
getRecordedEvent(eventId: number): WorkflowEvent | undefined;
|
|
12
|
+
getRecordedResult(eventId: number): unknown;
|
|
13
|
+
/** Returns true if the side effect with this sideEffectId has a recorded value to replay */
|
|
14
|
+
shouldReplaySideEffect(n: number): boolean;
|
|
15
|
+
getSideEffectResult(n: number): unknown;
|
|
16
|
+
getRecordedSideEffectEvent(n: number): WorkflowEvent | undefined;
|
|
17
|
+
shouldReplaySideEffectOfType(n: number, expectedName: string): boolean;
|
|
18
|
+
getSideEffectResultOfType(n: number, expectedName: string): unknown;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=replay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../../src/capture/replay.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,qBAAa,gBAAgB;IAMlB,UAAU,EAAE,OAAO;IACnB,UAAU,EAAE,MAAM;IAClB,OAAO,EAAE,aAAa,EAAE;IAPjC,OAAO,CAAC,UAAU,CAA4B;IAC9C,uFAAuF;IACvF,OAAO,CAAC,aAAa,CAA4B;gBAGxC,UAAU,EAAE,OAAO,EACnB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,aAAa,EAAE;IAQjC,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAItC,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAI5D,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAI3C,4FAA4F;IAC5F,sBAAsB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO;IAI1C,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO;IAIvC,0BAA0B,CAAC,CAAC,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAIhE,4BAA4B,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO;IAMtE,yBAAyB,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO;CAKpE"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export class ReplayController {
|
|
2
|
+
replayMode;
|
|
3
|
+
checkpoint;
|
|
4
|
+
history;
|
|
5
|
+
historyMap;
|
|
6
|
+
/** Side effects keyed by their assigned sideEffectId, independent of main event IDs */
|
|
7
|
+
sideEffectMap;
|
|
8
|
+
constructor(replayMode, checkpoint, history) {
|
|
9
|
+
this.replayMode = replayMode;
|
|
10
|
+
this.checkpoint = checkpoint;
|
|
11
|
+
this.history = history;
|
|
12
|
+
this.historyMap = new Map(history.map(e => [e.id, e]));
|
|
13
|
+
this.sideEffectMap = new Map(history.filter(e => e.type === 'side_effect').map(e => [e.id, e]));
|
|
14
|
+
}
|
|
15
|
+
shouldReplay(eventId) {
|
|
16
|
+
return this.replayMode && eventId <= this.checkpoint;
|
|
17
|
+
}
|
|
18
|
+
getRecordedEvent(eventId) {
|
|
19
|
+
return this.historyMap.get(eventId);
|
|
20
|
+
}
|
|
21
|
+
getRecordedResult(eventId) {
|
|
22
|
+
return this.historyMap.get(eventId)?.output;
|
|
23
|
+
}
|
|
24
|
+
/** Returns true if the side effect with this sideEffectId has a recorded value to replay */
|
|
25
|
+
shouldReplaySideEffect(n) {
|
|
26
|
+
return this.replayMode && this.sideEffectMap.has(n);
|
|
27
|
+
}
|
|
28
|
+
getSideEffectResult(n) {
|
|
29
|
+
return this.sideEffectMap.get(n)?.output;
|
|
30
|
+
}
|
|
31
|
+
getRecordedSideEffectEvent(n) {
|
|
32
|
+
return this.sideEffectMap.get(n);
|
|
33
|
+
}
|
|
34
|
+
shouldReplaySideEffectOfType(n, expectedName) {
|
|
35
|
+
if (!this.replayMode)
|
|
36
|
+
return false;
|
|
37
|
+
const event = this.sideEffectMap.get(n);
|
|
38
|
+
return !!event && event.type === 'side_effect' && event.name === expectedName;
|
|
39
|
+
}
|
|
40
|
+
getSideEffectResultOfType(n, expectedName) {
|
|
41
|
+
const event = this.sideEffectMap.get(n);
|
|
42
|
+
if (!event || event.type !== 'side_effect' || event.name !== expectedName)
|
|
43
|
+
return undefined;
|
|
44
|
+
return event.output;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../../src/capture/replay.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,gBAAgB;IAMlB;IACA;IACA;IAPD,UAAU,CAA4B;IAC9C,uFAAuF;IAC/E,aAAa,CAA4B;IAEjD,YACS,UAAmB,EACnB,UAAkB,EAClB,OAAwB;QAFxB,eAAU,GAAV,UAAU,CAAS;QACnB,eAAU,GAAV,UAAU,CAAQ;QAClB,YAAO,GAAP,OAAO,CAAiB;QAE/B,IAAI,CAAC,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;QACtD,IAAI,CAAC,aAAa,GAAG,IAAI,GAAG,CAC1B,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAClE,CAAA;IACH,CAAC;IAED,YAAY,CAAC,OAAe;QAC1B,OAAO,IAAI,CAAC,UAAU,IAAI,OAAO,IAAI,IAAI,CAAC,UAAU,CAAA;IACtD,CAAC;IAED,gBAAgB,CAAC,OAAe;QAC9B,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IACrC,CAAC;IAED,iBAAiB,CAAC,OAAe;QAC/B,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IAC7C,CAAC;IAED,4FAA4F;IAC5F,sBAAsB,CAAC,CAAS;QAC9B,OAAO,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IACrD,CAAC;IAED,mBAAmB,CAAC,CAAS;QAC3B,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,CAAA;IAC1C,CAAC;IAED,0BAA0B,CAAC,CAAS;QAClC,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IAClC,CAAC;IAED,4BAA4B,CAAC,CAAS,EAAE,YAAoB;QAC1D,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO,KAAK,CAAA;QAClC,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QACvC,OAAO,CAAC,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,CAAA;IAC/E,CAAC;IAED,yBAAyB,CAAC,CAAS,EAAE,YAAoB;QACvD,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QACvC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;YAAE,OAAO,SAAS,CAAA;QAC3F,OAAO,KAAK,CAAC,MAAM,CAAA;IACrB,CAAC;CACF"}
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { installAIInterceptor } from './interceptors/ai-interceptor.js';
|
|
|
10
10
|
import { runFiles } from './runner.js';
|
|
11
11
|
import { reportResults } from './reporter.js';
|
|
12
12
|
import { startBrowserUiServer } from './browser-ui.js';
|
|
13
|
+
import { startDashboardServer } from './dashboard-server.js';
|
|
13
14
|
function stripAnsi(input) {
|
|
14
15
|
if (!input)
|
|
15
16
|
return input;
|
|
@@ -20,8 +21,14 @@ async function loadConfig(cwd) {
|
|
|
20
21
|
const configPathJs = path.join(cwd, 'elasticdash.config.js');
|
|
21
22
|
for (const p of [configPath, configPathJs]) {
|
|
22
23
|
if (existsSync(p)) {
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
try {
|
|
25
|
+
const mod = await import(pathToFileURL(p).href);
|
|
26
|
+
return (mod.default ?? {});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
// Skip this config file if it can't be imported (e.g., .ts when running from built dist)
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
25
32
|
}
|
|
26
33
|
}
|
|
27
34
|
return {};
|
|
@@ -201,6 +208,45 @@ async function bootstrap() {
|
|
|
201
208
|
}
|
|
202
209
|
process.exit(anyFailed ? 1 : 0);
|
|
203
210
|
});
|
|
211
|
+
// elasticdash dashboard
|
|
212
|
+
program
|
|
213
|
+
.command('dashboard')
|
|
214
|
+
.description('Browse and search workflow functions')
|
|
215
|
+
.option('--port <port>', 'Dashboard server port', (v) => Number(v), 4573)
|
|
216
|
+
.option('--no-open', 'Skip auto-opening browser')
|
|
217
|
+
.action(async (options) => {
|
|
218
|
+
console.log('[elasticdash] Starting dashboard server...');
|
|
219
|
+
const server = await startDashboardServer(cwd, {
|
|
220
|
+
port: options.port,
|
|
221
|
+
autoOpen: options.open,
|
|
222
|
+
});
|
|
223
|
+
console.log(`[elasticdash] Dashboard running at ${server.url}`);
|
|
224
|
+
console.log('[elasticdash] Press Ctrl+C to stop');
|
|
225
|
+
// Keep the process running with proper cleanup
|
|
226
|
+
let isShuttingDown = false;
|
|
227
|
+
const cleanup = async () => {
|
|
228
|
+
if (isShuttingDown) {
|
|
229
|
+
// Force exit on second Ctrl+C
|
|
230
|
+
console.log('\n[elasticdash] Force exiting...');
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
isShuttingDown = true;
|
|
234
|
+
console.log('\n[elasticdash] Shutting down dashboard server...');
|
|
235
|
+
try {
|
|
236
|
+
await Promise.race([
|
|
237
|
+
server.close(),
|
|
238
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000))
|
|
239
|
+
]);
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
console.error('[elasticdash] Error during shutdown:', error);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
process.once('SIGINT', cleanup);
|
|
248
|
+
process.once('SIGTERM', cleanup);
|
|
249
|
+
});
|
|
204
250
|
await program.parseAsync(process.argv);
|
|
205
251
|
}
|
|
206
252
|
bootstrap().catch((err) => {
|