@opensearch-project/agent-health 0.0.1 → 0.1.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.
@@ -0,0 +1,4293 @@
1
+ #!/usr/bin/env node
2
+
3
+ // cli/index.ts
4
+ import { Command as Command8 } from "commander";
5
+ import chalk8 from "chalk";
6
+ import { fileURLToPath as fileURLToPath3 } from "url";
7
+ import { dirname as dirname3, join as join4, resolve as resolve4 } from "path";
8
+ import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
9
+ import { config as loadDotenv } from "dotenv";
10
+ import open from "open";
11
+ import ora4 from "ora";
12
+
13
+ // cli/utils/startServer.ts
14
+ import { fileURLToPath } from "url";
15
+ import { dirname, join } from "path";
16
+ import { existsSync } from "fs";
17
+ var __filename = fileURLToPath(import.meta.url);
18
+ var __dirname = dirname(__filename);
19
+ function findPackageRoot() {
20
+ let dir = __dirname;
21
+ for (let i = 0; i < 5; i++) {
22
+ if (existsSync(join(dir, "package.json"))) {
23
+ return dir;
24
+ }
25
+ dir = dirname(dir);
26
+ }
27
+ return join(__dirname, "..");
28
+ }
29
+ async function startServer(options) {
30
+ process.env.VITE_BACKEND_PORT = String(options.port);
31
+ const packageRoot = findPackageRoot();
32
+ const serverPath = join(packageRoot, "server", "dist", "app.js");
33
+ const { createApp } = await import(serverPath);
34
+ const app = await createApp();
35
+ return new Promise((resolve5) => {
36
+ app.listen(options.port, "0.0.0.0", () => {
37
+ resolve5();
38
+ });
39
+ });
40
+ }
41
+
42
+ // cli/commands/list.ts
43
+ import { Command } from "commander";
44
+ import chalk from "chalk";
45
+ import Table from "cli-table3";
46
+
47
+ // lib/config/loader.ts
48
+ import { existsSync as existsSync2 } from "fs";
49
+ import { resolve } from "path";
50
+ import { pathToFileURL } from "url";
51
+
52
+ // lib/debug.ts
53
+ var isBrowser = typeof window !== "undefined";
54
+ var serverDebugEnabled = !isBrowser && typeof process !== "undefined" && process.env?.DEBUG === "true";
55
+ function isDebugEnabled() {
56
+ if (isBrowser) {
57
+ try {
58
+ return localStorage.getItem("agenteval_debug") === "true";
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+ return serverDebugEnabled;
64
+ }
65
+ function debug(module, ...args) {
66
+ if (isDebugEnabled()) {
67
+ console.debug(`[${module}]`, ...args);
68
+ }
69
+ }
70
+
71
+ // lib/config.ts
72
+ var isServerSide = typeof window === "undefined";
73
+ var SERVER_PORT = isServerSide ? process.env?.VITE_BACKEND_PORT || process.env?.PORT || "4001" : "4001";
74
+ var BACKEND_URL = isServerSide ? `http://localhost:${SERVER_PORT}` : "";
75
+ var browserEnv = {};
76
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
77
+ try {
78
+ browserEnv = import.meta?.env || {};
79
+ } catch {
80
+ }
81
+ }
82
+ var getEnvVar = (key, defaultValue) => {
83
+ if (isServerSide) {
84
+ return process.env?.[key] || defaultValue || "";
85
+ }
86
+ return browserEnv[key] || defaultValue || "";
87
+ };
88
+ var ENV_CONFIG = {
89
+ // Backend server - empty string means relative URLs
90
+ backendUrl: BACKEND_URL,
91
+ // API endpoints (derived from backend URL)
92
+ judgeApiUrl: `${BACKEND_URL}/api/judge`,
93
+ storageApiUrl: `${BACKEND_URL}/api/storage`,
94
+ agentProxyUrl: `${BACKEND_URL}/api/agent`,
95
+ openSearchProxyUrl: `${BACKEND_URL}/api/opensearch/logs`,
96
+ // AWS/Bedrock
97
+ awsRegion: getEnvVar("AWS_REGION", "us-east-1"),
98
+ awsProfile: getEnvVar("AWS_PROFILE", "default"),
99
+ bedrockModelId: getEnvVar("BEDROCK_MODEL_ID", "anthropic.claude-3-5-sonnet-20241022-v2:0"),
100
+ // OpenSearch Logs (for fetching agent observability data)
101
+ openSearchLogsEndpoint: getEnvVar("OPENSEARCH_LOGS_ENDPOINT", ""),
102
+ openSearchLogsUsername: getEnvVar("OPENSEARCH_LOGS_USERNAME", ""),
103
+ openSearchLogsPassword: getEnvVar("OPENSEARCH_LOGS_PASSWORD", ""),
104
+ openSearchLogsTracesIndex: getEnvVar("OPENSEARCH_LOGS_TRACES_INDEX", "otel-v1-apm-span-*"),
105
+ openSearchLogsIndex: getEnvVar("OPENSEARCH_LOGS_INDEX", "ml-commons-logs-*"),
106
+ // Per-agent endpoints
107
+ langgraphEndpoint: getEnvVar("LANGGRAPH_ENDPOINT", "http://localhost:3000"),
108
+ mlcommonsEndpoint: getEnvVar("MLCOMMONS_ENDPOINT", "http://localhost:9200/_plugins/_ml/agents/{agent_id}/_execute/stream"),
109
+ holmesGptEndpoint: getEnvVar("HOLMESGPT_ENDPOINT", "http://localhost:5050/api/agui/chat"),
110
+ // ML-Commons agent headers
111
+ mlcommonsHeaderOpenSearchUrl: getEnvVar("MLCOMMONS_HEADER_OPENSEARCH_URL", ""),
112
+ mlcommonsHeaderAuthorization: getEnvVar("MLCOMMONS_HEADER_AUTHORIZATION", ""),
113
+ mlcommonsHeaderAwsRegion: getEnvVar("MLCOMMONS_HEADER_AWS_REGION", ""),
114
+ mlcommonsHeaderAwsServiceName: getEnvVar("MLCOMMONS_HEADER_AWS_SERVICE_NAME", "es"),
115
+ mlcommonsHeaderAwsAccessKeyId: getEnvVar("MLCOMMONS_HEADER_AWS_ACCESS_KEY_ID", ""),
116
+ mlcommonsHeaderAwsSecretAccessKey: getEnvVar("MLCOMMONS_HEADER_AWS_SECRET_ACCESS_KEY", ""),
117
+ mlcommonsHeaderAwsSessionToken: getEnvVar("MLCOMMONS_HEADER_AWS_SESSION_TOKEN", ""),
118
+ // Claude Code Telemetry (optional - for OTEL traces from Claude Code)
119
+ claudeCodeTelemetryEnabled: getEnvVar("CLAUDE_CODE_TELEMETRY_ENABLED", "false") === "true",
120
+ otelExporterEndpoint: getEnvVar("OTEL_EXPORTER_OTLP_ENDPOINT", ""),
121
+ otelServiceName: getEnvVar("OTEL_SERVICE_NAME", "claude-code-agent"),
122
+ otelExporterProtocol: getEnvVar("OTEL_EXPORTER_OTLP_PROTOCOL", ""),
123
+ otelExporterHeaders: getEnvVar("OTEL_EXPORTER_OTLP_HEADERS", "")
124
+ };
125
+ function buildMLCommonsHeaders() {
126
+ debug("Config", "Building ML-Commons headers");
127
+ const headers = {};
128
+ if (ENV_CONFIG.mlcommonsHeaderOpenSearchUrl) {
129
+ headers["opensearch-url"] = ENV_CONFIG.mlcommonsHeaderOpenSearchUrl;
130
+ }
131
+ if (ENV_CONFIG.mlcommonsHeaderAwsRegion) {
132
+ headers["aws-region"] = ENV_CONFIG.mlcommonsHeaderAwsRegion;
133
+ }
134
+ if (ENV_CONFIG.mlcommonsHeaderAuthorization) {
135
+ headers["Authorization"] = ENV_CONFIG.mlcommonsHeaderAuthorization;
136
+ } else {
137
+ if (ENV_CONFIG.mlcommonsHeaderAwsServiceName) {
138
+ headers["aws-service-name"] = ENV_CONFIG.mlcommonsHeaderAwsServiceName;
139
+ }
140
+ if (ENV_CONFIG.mlcommonsHeaderAwsAccessKeyId) {
141
+ headers["aws-access-key-id"] = ENV_CONFIG.mlcommonsHeaderAwsAccessKeyId;
142
+ }
143
+ if (ENV_CONFIG.mlcommonsHeaderAwsSecretAccessKey) {
144
+ headers["aws-secret-access-key"] = ENV_CONFIG.mlcommonsHeaderAwsSecretAccessKey;
145
+ }
146
+ if (ENV_CONFIG.mlcommonsHeaderAwsSessionToken) {
147
+ headers["aws-session-token"] = ENV_CONFIG.mlcommonsHeaderAwsSessionToken;
148
+ }
149
+ }
150
+ debug("Config", "ML-Commons headers built, keys:", Object.keys(headers));
151
+ return headers;
152
+ }
153
+
154
+ // lib/constants.ts
155
+ function getClaudeCodeConnectorEnv() {
156
+ const env = {
157
+ AWS_PROFILE: process.env.AWS_PROFILE || "Bedrock",
158
+ CLAUDE_CODE_USE_BEDROCK: "1",
159
+ AWS_REGION: process.env.AWS_REGION || "us-west-2",
160
+ DISABLE_PROMPT_CACHING: "1",
161
+ DISABLE_ERROR_REPORTING: "1"
162
+ };
163
+ if (ENV_CONFIG.claudeCodeTelemetryEnabled && ENV_CONFIG.otelExporterEndpoint) {
164
+ env.CLAUDE_CODE_ENABLE_TELEMETRY = "1";
165
+ env.OTEL_EXPORTER_OTLP_ENDPOINT = ENV_CONFIG.otelExporterEndpoint;
166
+ env.OTEL_SERVICE_NAME = ENV_CONFIG.otelServiceName;
167
+ if (ENV_CONFIG.otelExporterProtocol) {
168
+ env.OTEL_EXPORTER_OTLP_PROTOCOL = ENV_CONFIG.otelExporterProtocol;
169
+ }
170
+ if (ENV_CONFIG.otelExporterHeaders) {
171
+ env.OTEL_EXPORTER_OTLP_HEADERS = ENV_CONFIG.otelExporterHeaders;
172
+ }
173
+ } else {
174
+ env.DISABLE_TELEMETRY = "1";
175
+ }
176
+ return env;
177
+ }
178
+ var DEFAULT_CONFIG = {
179
+ agents: [
180
+ {
181
+ key: "demo",
182
+ name: "Demo Agent",
183
+ endpoint: "mock://demo",
184
+ description: "Mock agent for testing (simulated responses)",
185
+ connectorType: "mock",
186
+ models: ["demo-model"],
187
+ headers: {},
188
+ useTraces: false
189
+ },
190
+ {
191
+ key: "langgraph",
192
+ name: "Langgraph",
193
+ endpoint: ENV_CONFIG.langgraphEndpoint,
194
+ description: "Langgraph AG-UI agent server",
195
+ connectorType: "agui-streaming",
196
+ models: [
197
+ "claude-sonnet-4.5",
198
+ "claude-sonnet-4",
199
+ "claude-haiku-3.5"
200
+ ],
201
+ headers: {},
202
+ useTraces: true
203
+ },
204
+ {
205
+ key: "mlcommons-local",
206
+ name: "ML-Commons (Localhost)",
207
+ endpoint: ENV_CONFIG.mlcommonsEndpoint,
208
+ description: "Local OpenSearch ML-Commons conversational agent",
209
+ connectorType: "agui-streaming",
210
+ models: [
211
+ "claude-sonnet-4.5",
212
+ "claude-sonnet-4",
213
+ "claude-haiku-3.5"
214
+ ],
215
+ headers: buildMLCommonsHeaders(),
216
+ useTraces: true
217
+ },
218
+ {
219
+ key: "holmesgpt",
220
+ name: "HolmesGPT",
221
+ endpoint: ENV_CONFIG.holmesGptEndpoint,
222
+ description: "HolmesGPT AI-powered RCA agent (AG-UI)",
223
+ connectorType: "agui-streaming",
224
+ models: [
225
+ "claude-sonnet-4.5",
226
+ "claude-sonnet-4",
227
+ "claude-haiku-3.5"
228
+ ],
229
+ headers: {},
230
+ useTraces: true
231
+ },
232
+ {
233
+ key: "claude-code",
234
+ name: "Claude Code",
235
+ endpoint: "claude",
236
+ // Command name, not URL
237
+ description: "Claude Code CLI agent (requires claude command installed)",
238
+ connectorType: "claude-code",
239
+ models: ["claude-sonnet-4"],
240
+ headers: {},
241
+ get useTraces() {
242
+ return !!(ENV_CONFIG.claudeCodeTelemetryEnabled && ENV_CONFIG.otelExporterEndpoint);
243
+ },
244
+ // connectorConfig env vars are evaluated at runtime by getClaudeCodeConnectorEnv()
245
+ get connectorConfig() {
246
+ return {
247
+ env: getClaudeCodeConnectorEnv()
248
+ };
249
+ }
250
+ }
251
+ ],
252
+ models: {
253
+ "demo-model": {
254
+ model_id: "mock://demo-model",
255
+ display_name: "Demo Model",
256
+ provider: "demo",
257
+ context_window: 2e5,
258
+ max_output_tokens: 4096
259
+ },
260
+ "claude-sonnet-4.5": {
261
+ model_id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
262
+ display_name: "Claude Sonnet 4.5",
263
+ provider: "bedrock",
264
+ context_window: 2e5,
265
+ max_output_tokens: 4096
266
+ },
267
+ "claude-sonnet-4": {
268
+ model_id: "us.anthropic.claude-sonnet-4-20250514-v1:0",
269
+ display_name: "Claude Sonnet 4",
270
+ provider: "bedrock",
271
+ context_window: 2e5,
272
+ max_output_tokens: 4096
273
+ },
274
+ "claude-haiku-3.5": {
275
+ model_id: "us.anthropic.claude-3-5-haiku-20241022-v1:0",
276
+ display_name: "Claude Haiku 3.5",
277
+ provider: "bedrock",
278
+ context_window: 2e5,
279
+ max_output_tokens: 4096
280
+ }
281
+ },
282
+ defaults: {
283
+ retry_attempts: 2,
284
+ retry_delay_ms: 1e3
285
+ }
286
+ };
287
+
288
+ // lib/config/loader.ts
289
+ var DEFAULT_SERVER_CONFIG = {
290
+ port: 4001,
291
+ reuseExistingServer: !process.env.CI,
292
+ startTimeout: 3e4
293
+ };
294
+ var CONFIG_FILE_NAMES = [
295
+ "agent-health.config.ts",
296
+ "agent-health.config.js",
297
+ "agent-health.config.mjs"
298
+ ];
299
+ function findConfigFile(cwd = process.cwd()) {
300
+ for (const fileName of CONFIG_FILE_NAMES) {
301
+ const filePath = resolve(cwd, fileName);
302
+ if (existsSync2(filePath)) {
303
+ const format = fileName.endsWith(".ts") ? "typescript" : "javascript";
304
+ return { path: filePath, format, exists: true };
305
+ }
306
+ }
307
+ return null;
308
+ }
309
+ function toAgentConfig(userAgent) {
310
+ return {
311
+ key: userAgent.key,
312
+ name: userAgent.name,
313
+ endpoint: userAgent.endpoint,
314
+ description: userAgent.description,
315
+ enabled: userAgent.enabled ?? true,
316
+ models: userAgent.models,
317
+ headers: userAgent.headers ?? {},
318
+ useTraces: userAgent.useTraces ?? false,
319
+ connectorType: userAgent.connectorType,
320
+ connectorConfig: userAgent.connectorConfig,
321
+ hooks: userAgent.hooks
322
+ };
323
+ }
324
+ function toModelConfig(userModel) {
325
+ return [
326
+ userModel.key,
327
+ {
328
+ model_id: userModel.model_id,
329
+ display_name: userModel.display_name,
330
+ provider: userModel.provider ?? "bedrock",
331
+ context_window: userModel.context_window ?? 2e5,
332
+ max_output_tokens: userModel.max_output_tokens ?? 4096
333
+ }
334
+ ];
335
+ }
336
+ function mergeConfigs(userConfig, defaultConfig) {
337
+ const shouldExtend = userConfig.extends !== false;
338
+ let agents;
339
+ if (shouldExtend) {
340
+ const agentMap = /* @__PURE__ */ new Map();
341
+ for (const agent of defaultConfig.agents) {
342
+ agentMap.set(agent.key, agent);
343
+ }
344
+ for (const userAgent of userConfig.agents ?? []) {
345
+ agentMap.set(userAgent.key, toAgentConfig(userAgent));
346
+ }
347
+ agents = Array.from(agentMap.values());
348
+ } else {
349
+ agents = (userConfig.agents ?? []).map(toAgentConfig);
350
+ }
351
+ let models;
352
+ if (shouldExtend) {
353
+ models = { ...defaultConfig.models };
354
+ for (const userModel of userConfig.models ?? []) {
355
+ const [key, config] = toModelConfig(userModel);
356
+ models[key] = config;
357
+ }
358
+ } else {
359
+ models = {};
360
+ for (const userModel of userConfig.models ?? []) {
361
+ const [key, config] = toModelConfig(userModel);
362
+ models[key] = config;
363
+ }
364
+ }
365
+ const connectors = userConfig.connectors ?? [];
366
+ const testCases = userConfig.testCases ? Array.isArray(userConfig.testCases) ? userConfig.testCases : [userConfig.testCases] : [];
367
+ const reporters = userConfig.reporters ?? [["console"]];
368
+ const judge = userConfig.judge ?? {
369
+ provider: "bedrock",
370
+ model: "claude-sonnet-4"
371
+ };
372
+ const server = {
373
+ ...DEFAULT_SERVER_CONFIG,
374
+ ...userConfig.server
375
+ };
376
+ return {
377
+ server,
378
+ agents,
379
+ models,
380
+ connectors,
381
+ testCases,
382
+ reporters,
383
+ judge
384
+ };
385
+ }
386
+ async function loadUserConfig(configPath) {
387
+ try {
388
+ const fileUrl = pathToFileURL(configPath).href;
389
+ const module = await import(fileUrl);
390
+ return module.default ?? module;
391
+ } catch (error) {
392
+ const message = error instanceof Error ? error.message : String(error);
393
+ throw new Error(`Failed to load config file ${configPath}: ${message}`);
394
+ }
395
+ }
396
+ var cachedConfig = null;
397
+ var cachedConfigPath = null;
398
+ async function loadConfig(cwd = process.cwd(), force = false) {
399
+ const configFile = findConfigFile(cwd);
400
+ if (!force && cachedConfig && cachedConfigPath === configFile?.path) {
401
+ return cachedConfig;
402
+ }
403
+ let userConfig = {};
404
+ if (configFile) {
405
+ console.log(`[Config] Loading ${configFile.path}`);
406
+ userConfig = await loadUserConfig(configFile.path);
407
+ } else {
408
+ console.log("[Config] No config file found, using defaults + environment variables");
409
+ }
410
+ const resolved = mergeConfigs(userConfig, DEFAULT_CONFIG);
411
+ cachedConfig = resolved;
412
+ cachedConfigPath = configFile?.path ?? null;
413
+ console.log(`[Config] Loaded ${resolved.agents.length} agents, ${Object.keys(resolved.models).length} models`);
414
+ return resolved;
415
+ }
416
+ function getConfigFileInfo(cwd = process.cwd()) {
417
+ return findConfigFile(cwd);
418
+ }
419
+
420
+ // services/connectors/registry.ts
421
+ var DEFAULT_CONNECTOR_TYPE = "agui-streaming";
422
+ var ConnectorRegistryImpl = class {
423
+ constructor() {
424
+ this.connectors = /* @__PURE__ */ new Map();
425
+ }
426
+ /**
427
+ * Register a connector implementation
428
+ * @throws Error if connector with same type is already registered
429
+ */
430
+ register(connector) {
431
+ if (this.connectors.has(connector.type)) {
432
+ console.warn(
433
+ `[ConnectorRegistry] Overwriting existing connector for type: ${connector.type}`
434
+ );
435
+ }
436
+ this.connectors.set(connector.type, connector);
437
+ }
438
+ /**
439
+ * Get a connector by protocol type
440
+ */
441
+ get(type) {
442
+ return this.connectors.get(type);
443
+ }
444
+ /**
445
+ * Get all registered connectors
446
+ */
447
+ getAll() {
448
+ return Array.from(this.connectors.values());
449
+ }
450
+ /**
451
+ * Check if a connector is registered
452
+ */
453
+ has(type) {
454
+ return this.connectors.has(type);
455
+ }
456
+ /**
457
+ * Get connector for an agent config
458
+ * Handles backwards compatibility with legacy configs
459
+ *
460
+ * Resolution order:
461
+ * 1. If endpoint starts with 'mock://', use mock connector
462
+ * 2. If connectorType is specified, use that
463
+ * 3. Default to 'agui-streaming'
464
+ */
465
+ getForAgent(agent) {
466
+ if (agent.endpoint.startsWith("mock://")) {
467
+ const mockConnector2 = this.get("mock");
468
+ if (mockConnector2) {
469
+ return mockConnector2;
470
+ }
471
+ console.warn("[ConnectorRegistry] Mock connector not registered, falling back to default");
472
+ }
473
+ const connectorType = agent.connectorType ?? DEFAULT_CONNECTOR_TYPE;
474
+ const connector = this.get(connectorType);
475
+ if (!connector) {
476
+ console.error(
477
+ `[ConnectorRegistry] Connector not found for type: ${connectorType}, falling back to ${DEFAULT_CONNECTOR_TYPE}`
478
+ );
479
+ const defaultConnector = this.get(DEFAULT_CONNECTOR_TYPE);
480
+ if (!defaultConnector) {
481
+ throw new Error(
482
+ `No connector registered for type '${connectorType}' and no default connector available`
483
+ );
484
+ }
485
+ return defaultConnector;
486
+ }
487
+ return connector;
488
+ }
489
+ /**
490
+ * Clear all registered connectors (useful for testing)
491
+ */
492
+ clear() {
493
+ this.connectors.clear();
494
+ }
495
+ /**
496
+ * Get list of registered connector types
497
+ */
498
+ getRegisteredTypes() {
499
+ return Array.from(this.connectors.keys());
500
+ }
501
+ };
502
+ var connectorRegistry = new ConnectorRegistryImpl();
503
+
504
+ // services/connectors/base/BaseConnector.ts
505
+ var BaseConnector = class {
506
+ /**
507
+ * Build HTTP headers from auth configuration
508
+ * @param auth Authentication configuration
509
+ * @returns Headers object ready for fetch/axios
510
+ */
511
+ buildAuthHeaders(auth) {
512
+ const headers = {};
513
+ switch (auth.type) {
514
+ case "basic":
515
+ if (auth.username && auth.password) {
516
+ const credentials = Buffer.from(`${auth.username}:${auth.password}`).toString("base64");
517
+ headers["Authorization"] = `Basic ${credentials}`;
518
+ }
519
+ break;
520
+ case "bearer":
521
+ if (auth.token) {
522
+ headers["Authorization"] = `Bearer ${auth.token}`;
523
+ }
524
+ break;
525
+ case "api-key":
526
+ if (auth.token) {
527
+ headers["X-API-Key"] = auth.token;
528
+ headers["x-api-key"] = auth.token;
529
+ }
530
+ break;
531
+ case "aws-sigv4":
532
+ console.warn("[BaseConnector] AWS SigV4 auth requires runtime signing");
533
+ break;
534
+ case "none":
535
+ default:
536
+ break;
537
+ }
538
+ if (auth.headers) {
539
+ Object.assign(headers, auth.headers);
540
+ }
541
+ return headers;
542
+ }
543
+ /**
544
+ * Build environment variables from auth configuration
545
+ * Used by subprocess connectors
546
+ */
547
+ buildAuthEnv(auth) {
548
+ const env = {};
549
+ if (auth.type === "aws-sigv4") {
550
+ if (auth.awsRegion) env["AWS_REGION"] = auth.awsRegion;
551
+ if (auth.awsAccessKeyId) env["AWS_ACCESS_KEY_ID"] = auth.awsAccessKeyId;
552
+ if (auth.awsSecretAccessKey) env["AWS_SECRET_ACCESS_KEY"] = auth.awsSecretAccessKey;
553
+ if (auth.awsSessionToken) env["AWS_SESSION_TOKEN"] = auth.awsSessionToken;
554
+ }
555
+ return env;
556
+ }
557
+ /**
558
+ * Generate a unique ID for trajectory steps
559
+ */
560
+ generateId() {
561
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
562
+ }
563
+ /**
564
+ * Create a trajectory step with common fields
565
+ */
566
+ createStep(type, content, extra) {
567
+ return {
568
+ id: this.generateId(),
569
+ timestamp: Date.now(),
570
+ type,
571
+ content,
572
+ ...extra
573
+ };
574
+ }
575
+ /**
576
+ * Default health check implementation
577
+ * Subclasses can override for protocol-specific checks
578
+ */
579
+ async healthCheck(endpoint, auth) {
580
+ try {
581
+ const headers = this.buildAuthHeaders(auth);
582
+ const response = await fetch(endpoint, {
583
+ method: "HEAD",
584
+ headers
585
+ });
586
+ return response.ok;
587
+ } catch (error) {
588
+ console.error(`[${this.type}] Health check failed:`, error);
589
+ return false;
590
+ }
591
+ }
592
+ /**
593
+ * Log debug message with connector type prefix
594
+ */
595
+ debug(message, ...args) {
596
+ debug(this.type, message, ...args);
597
+ }
598
+ /**
599
+ * Log error message with connector type prefix
600
+ */
601
+ error(message, ...args) {
602
+ console.error(`[${this.type}] ${message}`, ...args);
603
+ }
604
+ };
605
+
606
+ // types/agui.ts
607
+ import { EventType } from "@ag-ui/core";
608
+ var AGUIEventType = EventType;
609
+
610
+ // services/agent/sseStream.ts
611
+ var SSEClient = class {
612
+ constructor() {
613
+ this.abortController = null;
614
+ }
615
+ /**
616
+ * Start consuming SSE stream from the agent endpoint
617
+ */
618
+ async consume(options) {
619
+ const {
620
+ url,
621
+ method = "POST",
622
+ headers = {},
623
+ body,
624
+ onEvent,
625
+ onError,
626
+ onComplete,
627
+ completeOnRunEnd = false,
628
+ idleTimeoutMs = 12e4
629
+ // 2 minute idle timeout by default (LLM agents can be slow)
630
+ } = options;
631
+ this.abortController = new AbortController();
632
+ debug("SSE", "Connecting to", url);
633
+ debug("SSE", "Method:", method);
634
+ debug("SSE", "Payload:", JSON.stringify(body, null, 2).substring(0, 500));
635
+ try {
636
+ const response = await fetch(url, {
637
+ method,
638
+ headers: {
639
+ "Content-Type": "application/json",
640
+ Accept: "text/event-stream",
641
+ ...headers
642
+ },
643
+ body: body ? JSON.stringify(body) : void 0,
644
+ signal: this.abortController.signal
645
+ });
646
+ if (!response.ok) {
647
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
648
+ }
649
+ if (!response.body) {
650
+ throw new Error("Response body is null");
651
+ }
652
+ debug("SSE", "Connected, streaming events...");
653
+ debug("SSE", "Response status:", response.status);
654
+ debug("SSE", "Content-Type:", response.headers.get("content-type"));
655
+ const completionReason = await this.processStream(response.body, onEvent, completeOnRunEnd, idleTimeoutMs);
656
+ debug("SSE", `Stream completed: ${completionReason}`);
657
+ onComplete?.();
658
+ } catch (error) {
659
+ if (error instanceof Error) {
660
+ if (error.name === "AbortError") {
661
+ debug("SSE", "Stream aborted (expected after run completion)");
662
+ onComplete?.();
663
+ } else {
664
+ console.error("[SSE] Stream error:", error.message);
665
+ onError?.(error);
666
+ }
667
+ } else {
668
+ onError?.(new Error("Unknown error occurred"));
669
+ }
670
+ }
671
+ }
672
+ /**
673
+ * Process the ReadableStream and parse SSE events
674
+ * @returns Reason for stream completion
675
+ */
676
+ async processStream(stream, onEvent, completeOnRunEnd = false, idleTimeoutMs = 12e4) {
677
+ const reader = stream.getReader();
678
+ const decoder = new TextDecoder();
679
+ let buffer = "";
680
+ let lastEventTime = Date.now();
681
+ let eventCount = 0;
682
+ let idleCheckInterval = null;
683
+ const idleTimeoutPromise = new Promise((resolve5) => {
684
+ idleCheckInterval = setInterval(() => {
685
+ const idleTime = Date.now() - lastEventTime;
686
+ if (eventCount > 0 && idleTime > idleTimeoutMs) {
687
+ debug("SSE", `Idle timeout: no events for ${idleTime}ms (threshold: ${idleTimeoutMs}ms)`);
688
+ this.abort();
689
+ resolve5("idle_timeout");
690
+ }
691
+ }, 1e3);
692
+ });
693
+ try {
694
+ const streamPromise = (async () => {
695
+ while (true) {
696
+ const { done, value } = await reader.read();
697
+ if (done) {
698
+ return "connection_closed";
699
+ }
700
+ lastEventTime = Date.now();
701
+ buffer += decoder.decode(value, { stream: true });
702
+ const lines = buffer.split("\n");
703
+ buffer = lines.pop() || "";
704
+ for (const line of lines) {
705
+ if (line.startsWith("data: ")) {
706
+ const data = line.slice(6);
707
+ if (data.trim()) {
708
+ debug("SSE", "Raw event:", data.substring(0, 200) + (data.length > 200 ? "..." : ""));
709
+ try {
710
+ const event = JSON.parse(data);
711
+ debug("SSE", "Parsed:", event.type);
712
+ eventCount++;
713
+ onEvent(event);
714
+ if (completeOnRunEnd && (event.type === AGUIEventType.RUN_FINISHED || event.type === AGUIEventType.RUN_ERROR)) {
715
+ debug("SSE", `Received ${event.type}, completing stream`);
716
+ this.abort();
717
+ return `event:${event.type}`;
718
+ }
719
+ } catch (parseError) {
720
+ console.error("[SSE] Parse error:", parseError);
721
+ debug("SSE", "Failed data:", data);
722
+ }
723
+ }
724
+ }
725
+ }
726
+ }
727
+ })();
728
+ const reason = await Promise.race([streamPromise, idleTimeoutPromise]);
729
+ return reason;
730
+ } finally {
731
+ if (idleCheckInterval) {
732
+ clearInterval(idleCheckInterval);
733
+ }
734
+ reader.releaseLock();
735
+ }
736
+ }
737
+ /**
738
+ * Abort the current stream connection
739
+ */
740
+ abort() {
741
+ this.abortController?.abort();
742
+ }
743
+ };
744
+ async function consumeSSEStream(url, payload, onEvent, headers, options) {
745
+ const client = new SSEClient();
746
+ return new Promise((resolve5, reject) => {
747
+ client.consume({
748
+ url,
749
+ method: "POST",
750
+ headers,
751
+ body: payload,
752
+ onEvent,
753
+ onError: (error) => reject(error),
754
+ onComplete: () => resolve5(),
755
+ // Enable auto-completion on RUN_FINISHED/RUN_ERROR events
756
+ // This prevents hanging when the agent doesn't close the connection
757
+ completeOnRunEnd: true,
758
+ idleTimeoutMs: options?.idleTimeoutMs
759
+ });
760
+ });
761
+ }
762
+
763
+ // services/agent/payloadBuilder.ts
764
+ var DEFAULT_PPL_TOOL = {
765
+ name: "execute_ppl_query",
766
+ description: "Update the query bar with a PPL query and optionally execute it",
767
+ parameters: {
768
+ type: "object",
769
+ properties: {
770
+ query: {
771
+ type: "string",
772
+ description: "The PPL query to set in the query bar"
773
+ },
774
+ autoExecute: {
775
+ type: "boolean",
776
+ description: "Whether to automatically execute the query (default: true)"
777
+ },
778
+ description: {
779
+ type: "string",
780
+ description: "Optional description of what the query does"
781
+ }
782
+ },
783
+ required: ["query"]
784
+ }
785
+ };
786
+ function generateId(prefix) {
787
+ const timestamp = Date.now();
788
+ const random = Math.random().toString(36).substring(2, 11);
789
+ return `${prefix}-${timestamp}-${random}`;
790
+ }
791
+ function buildAgentPayload(testCase, modelId, threadId, runId) {
792
+ const tools = testCase.tools || [DEFAULT_PPL_TOOL];
793
+ return {
794
+ threadId: threadId || generateId("thread"),
795
+ runId: runId || generateId("run"),
796
+ messages: [
797
+ {
798
+ id: generateId("msg"),
799
+ role: "user",
800
+ content: testCase.initialPrompt
801
+ }
802
+ ],
803
+ tools,
804
+ context: testCase.context || [],
805
+ state: {},
806
+ forwardedProps: {}
807
+ };
808
+ }
809
+
810
+ // services/agent/aguiConverter.ts
811
+ import { v4 as uuidv4 } from "uuid";
812
+ var AGUIToTrajectoryConverter = class {
813
+ constructor() {
814
+ this.currentTextMessage = null;
815
+ this.activeTools = /* @__PURE__ */ new Map();
816
+ this.hasEmittedAction = false;
817
+ this.runFinished = false;
818
+ this.pendingTextIsResponse = false;
819
+ this.runId = null;
820
+ this.threadId = null;
821
+ // Thinking state tracking
822
+ this.isThinking = false;
823
+ this.currentThinkingMessage = null;
824
+ }
825
+ /**
826
+ * Process a single AG UI event and convert it to TrajectoryStep(s)
827
+ * Returns an array of steps (usually 0 or 1, sometimes more)
828
+ */
829
+ processEvent(event) {
830
+ debug("Converter", `Event: ${event.type}`, JSON.stringify(event).substring(0, 300));
831
+ let steps = [];
832
+ switch (event.type) {
833
+ case AGUIEventType.RUN_STARTED:
834
+ steps = this.handleRunStarted(event);
835
+ break;
836
+ case AGUIEventType.RUN_FINISHED:
837
+ steps = this.handleRunFinished(event);
838
+ break;
839
+ case AGUIEventType.RUN_ERROR:
840
+ steps = this.handleRunError(event);
841
+ break;
842
+ case AGUIEventType.TEXT_MESSAGE_START:
843
+ steps = this.handleTextMessageStart(event);
844
+ break;
845
+ case AGUIEventType.TEXT_MESSAGE_CONTENT:
846
+ steps = this.handleTextMessageContent(event);
847
+ break;
848
+ case AGUIEventType.TEXT_MESSAGE_END:
849
+ steps = this.handleTextMessageEnd(event);
850
+ break;
851
+ case AGUIEventType.ACTIVITY_SNAPSHOT:
852
+ steps = this.handleActivitySnapshot(event);
853
+ break;
854
+ case AGUIEventType.ACTIVITY_DELTA:
855
+ steps = this.handleActivityDelta(event);
856
+ break;
857
+ case AGUIEventType.TOOL_CALL_START:
858
+ steps = this.handleToolCallStart(event);
859
+ break;
860
+ case AGUIEventType.TOOL_CALL_ARGS:
861
+ steps = this.handleToolCallArgs(event);
862
+ break;
863
+ case AGUIEventType.TOOL_CALL_END:
864
+ steps = this.handleToolCallEnd(event);
865
+ break;
866
+ case AGUIEventType.TOOL_CALL_RESULT:
867
+ steps = this.handleToolCallResult(event);
868
+ break;
869
+ // Thinking events - extended reasoning from the model
870
+ case AGUIEventType.THINKING_START:
871
+ steps = this.handleThinkingStart(event);
872
+ break;
873
+ case AGUIEventType.THINKING_END:
874
+ steps = this.handleThinkingEnd(event);
875
+ break;
876
+ case AGUIEventType.THINKING_TEXT_MESSAGE_START:
877
+ steps = this.handleThinkingTextMessageStart(event);
878
+ break;
879
+ case AGUIEventType.THINKING_TEXT_MESSAGE_CONTENT:
880
+ steps = this.handleThinkingTextMessageContent(event);
881
+ break;
882
+ case AGUIEventType.THINKING_TEXT_MESSAGE_END:
883
+ steps = this.handleThinkingTextMessageEnd(event);
884
+ break;
885
+ default:
886
+ debug("Converter", `Skipped unhandled event: ${event.type}`);
887
+ break;
888
+ }
889
+ if (steps.length > 0) {
890
+ debug("Converter", `Generated ${steps.length} step(s):`, steps.map((s) => `${s.type}${s.toolName ? `:${s.toolName}` : ""}`).join(", "));
891
+ }
892
+ return steps;
893
+ }
894
+ handleRunStarted(event) {
895
+ this.runId = event.runId;
896
+ this.threadId = event.threadId;
897
+ debug("Converter", `Run started - runId: ${this.runId}, threadId: ${this.threadId}`);
898
+ this.currentTextMessage = null;
899
+ this.activeTools.clear();
900
+ this.hasEmittedAction = false;
901
+ this.runFinished = false;
902
+ this.pendingTextIsResponse = false;
903
+ this.isThinking = false;
904
+ this.currentThinkingMessage = null;
905
+ return [];
906
+ }
907
+ getRunId() {
908
+ return this.runId;
909
+ }
910
+ getThreadId() {
911
+ return this.threadId;
912
+ }
913
+ handleRunFinished(event) {
914
+ this.runFinished = true;
915
+ if (this.currentTextMessage) {
916
+ this.pendingTextIsResponse = true;
917
+ }
918
+ return [];
919
+ }
920
+ handleRunError(event) {
921
+ console.error("[Converter] Run error:", event.message);
922
+ return [{
923
+ id: uuidv4(),
924
+ timestamp: event.timestamp,
925
+ type: "tool_result",
926
+ content: `Error: ${event.message}`,
927
+ status: "FAILURE" /* FAILURE */
928
+ }];
929
+ }
930
+ handleTextMessageStart(event) {
931
+ debug("Converter", `Text message start: ${event.messageId}`);
932
+ this.currentTextMessage = {
933
+ messageId: event.messageId,
934
+ startTime: event.timestamp,
935
+ content: ""
936
+ };
937
+ return [];
938
+ }
939
+ handleTextMessageContent(event) {
940
+ if (this.currentTextMessage && this.currentTextMessage.messageId === event.messageId) {
941
+ this.currentTextMessage.content += event.delta;
942
+ }
943
+ return [];
944
+ }
945
+ handleTextMessageEnd(event) {
946
+ if (!this.currentTextMessage || this.currentTextMessage.messageId !== event.messageId) {
947
+ debug("Converter", `Text end for unknown message: ${event.messageId}`);
948
+ return [];
949
+ }
950
+ const latencyMs = event.timestamp - this.currentTextMessage.startTime;
951
+ const content = this.currentTextMessage.content.trim();
952
+ let stepType;
953
+ if (this.pendingTextIsResponse || this.runFinished) {
954
+ stepType = "response";
955
+ } else {
956
+ stepType = "assistant";
957
+ }
958
+ debug("Converter", `Classification: ${stepType} (runFinished=${this.runFinished}, pendingResponse=${this.pendingTextIsResponse}, hasAction=${this.hasEmittedAction})`);
959
+ if (stepType === "assistant" && content.length === 0) {
960
+ debug("Converter", "Skipping empty assistant message");
961
+ this.currentTextMessage = null;
962
+ return [];
963
+ }
964
+ const step = {
965
+ id: uuidv4(),
966
+ timestamp: event.timestamp,
967
+ type: stepType,
968
+ content,
969
+ latencyMs
970
+ };
971
+ this.currentTextMessage = null;
972
+ return [step];
973
+ }
974
+ handleActivitySnapshot(event) {
975
+ const toolName = this.extractToolName(event.content.title);
976
+ const toolArgs = this.parseToolArgs(event.content.description);
977
+ const actionStepId = uuidv4();
978
+ this.activeTools.set(event.messageId, {
979
+ messageId: event.messageId,
980
+ toolName,
981
+ toolArgs,
982
+ argsAccumulator: "",
983
+ startTime: event.timestamp,
984
+ actionStepId
985
+ });
986
+ this.hasEmittedAction = true;
987
+ debug("Converter", `Tool action: ${toolName}`, toolArgs);
988
+ return [{
989
+ id: actionStepId,
990
+ timestamp: event.timestamp,
991
+ type: "action",
992
+ content: `Calling ${toolName}...`,
993
+ toolName,
994
+ toolArgs
995
+ }];
996
+ }
997
+ handleActivityDelta(event) {
998
+ const toolState = this.activeTools.get(event.messageId);
999
+ if (!toolState) return [];
1000
+ const isCompletion = event.patch.some(
1001
+ (op) => op.path === "/icon" && (op.value === "CheckCircle" || op.value === "Check")
1002
+ );
1003
+ if (!isCompletion) return [];
1004
+ const descriptionPatch = event.patch.find((op) => op.path === "/description");
1005
+ const resultContent = descriptionPatch?.value || "Tool execution completed";
1006
+ const latencyMs = event.timestamp - toolState.startTime;
1007
+ this.activeTools.delete(event.messageId);
1008
+ return [{
1009
+ id: uuidv4(),
1010
+ timestamp: event.timestamp,
1011
+ type: "tool_result",
1012
+ content: resultContent,
1013
+ status: "SUCCESS" /* SUCCESS */,
1014
+ latencyMs
1015
+ }];
1016
+ }
1017
+ handleToolCallStart(event) {
1018
+ const actionStepId = uuidv4();
1019
+ this.activeTools.set(event.toolCallId, {
1020
+ messageId: event.toolCallId,
1021
+ toolName: event.toolCallName,
1022
+ toolArgs: {},
1023
+ argsAccumulator: "",
1024
+ // Will accumulate delta strings
1025
+ startTime: event.timestamp,
1026
+ actionStepId
1027
+ });
1028
+ this.hasEmittedAction = true;
1029
+ debug("Converter", `Tool call started: ${event.toolCallName} (${event.toolCallId})`);
1030
+ return [];
1031
+ }
1032
+ handleToolCallArgs(event) {
1033
+ const toolState = this.activeTools.get(event.toolCallId);
1034
+ if (toolState) {
1035
+ toolState.argsAccumulator += event.delta;
1036
+ debug("Converter", `Tool args delta accumulated (${toolState.argsAccumulator.length} chars total)`);
1037
+ }
1038
+ return [];
1039
+ }
1040
+ /**
1041
+ * Handle TOOL_CALL_END - emits the action step with complete args
1042
+ * This is called when the agent is done sending tool call arguments
1043
+ * and expects the client to execute the tool
1044
+ */
1045
+ handleToolCallEnd(event) {
1046
+ const toolState = this.activeTools.get(event.toolCallId);
1047
+ if (!toolState) {
1048
+ debug("Converter", `Tool call end for unknown tool: ${event.toolCallId}`);
1049
+ return [];
1050
+ }
1051
+ let parsedArgs = {};
1052
+ if (toolState.argsAccumulator) {
1053
+ try {
1054
+ parsedArgs = JSON.parse(toolState.argsAccumulator);
1055
+ debug("Converter", `Tool args parsed: ${JSON.stringify(parsedArgs).substring(0, 200)}`);
1056
+ } catch {
1057
+ parsedArgs = { _raw: toolState.argsAccumulator };
1058
+ }
1059
+ }
1060
+ toolState.toolArgs = parsedArgs;
1061
+ const latencyMs = event.timestamp - toolState.startTime;
1062
+ const actionStep = {
1063
+ id: toolState.actionStepId,
1064
+ timestamp: toolState.startTime,
1065
+ type: "action",
1066
+ content: `Calling ${toolState.toolName}...`,
1067
+ toolName: toolState.toolName,
1068
+ toolArgs: parsedArgs,
1069
+ latencyMs
1070
+ };
1071
+ debug("Converter", `Tool call end: ${toolState.toolName} - action step emitted with args`);
1072
+ toolState.actionStepId = "";
1073
+ return [actionStep];
1074
+ }
1075
+ handleToolCallResult(event) {
1076
+ const toolState = this.activeTools.get(event.toolCallId);
1077
+ if (!toolState) return [];
1078
+ const latencyMs = event.timestamp - toolState.startTime;
1079
+ const steps = [];
1080
+ const actionAlreadyEmitted = !toolState.actionStepId;
1081
+ if (!actionAlreadyEmitted) {
1082
+ let parsedArgs = {};
1083
+ if (toolState.argsAccumulator) {
1084
+ try {
1085
+ parsedArgs = JSON.parse(toolState.argsAccumulator);
1086
+ debug("Converter", `Tool args parsed: ${JSON.stringify(parsedArgs).substring(0, 200)}`);
1087
+ } catch {
1088
+ parsedArgs = { _raw: toolState.argsAccumulator };
1089
+ }
1090
+ }
1091
+ toolState.toolArgs = parsedArgs;
1092
+ steps.push({
1093
+ id: toolState.actionStepId,
1094
+ timestamp: toolState.startTime,
1095
+ type: "action",
1096
+ content: `Calling ${toolState.toolName}...`,
1097
+ toolName: toolState.toolName,
1098
+ toolArgs: parsedArgs
1099
+ });
1100
+ }
1101
+ let resultContent;
1102
+ try {
1103
+ const parsed = JSON.parse(event.content);
1104
+ resultContent = typeof parsed === "string" ? parsed : JSON.stringify(parsed, null, 2);
1105
+ } catch (e) {
1106
+ resultContent = event.content;
1107
+ }
1108
+ steps.push({
1109
+ id: uuidv4(),
1110
+ timestamp: event.timestamp,
1111
+ type: "tool_result",
1112
+ content: resultContent,
1113
+ status: "SUCCESS" /* SUCCESS */,
1114
+ latencyMs
1115
+ });
1116
+ this.activeTools.delete(event.toolCallId);
1117
+ debug("Converter", `Tool call result: ${toolState.toolName} -> ${steps.length} steps emitted (action already emitted: ${actionAlreadyEmitted})`);
1118
+ return steps;
1119
+ }
1120
+ // ============ THINKING Event Handlers ============
1121
+ handleThinkingStart(event) {
1122
+ this.isThinking = true;
1123
+ debug("Converter", "Thinking started");
1124
+ return [];
1125
+ }
1126
+ handleThinkingEnd(event) {
1127
+ this.isThinking = false;
1128
+ debug("Converter", "Thinking ended");
1129
+ return [];
1130
+ }
1131
+ handleThinkingTextMessageStart(event) {
1132
+ this.currentThinkingMessage = {
1133
+ startTime: event.timestamp || Date.now(),
1134
+ content: ""
1135
+ };
1136
+ debug("Converter", "Thinking text message started");
1137
+ return [];
1138
+ }
1139
+ handleThinkingTextMessageContent(event) {
1140
+ if (this.currentThinkingMessage) {
1141
+ this.currentThinkingMessage.content += event.delta;
1142
+ }
1143
+ return [];
1144
+ }
1145
+ handleThinkingTextMessageEnd(event) {
1146
+ if (!this.currentThinkingMessage) {
1147
+ debug("Converter", "Thinking text end with no active thinking message");
1148
+ return [];
1149
+ }
1150
+ const content = this.currentThinkingMessage.content.trim();
1151
+ if (content.length === 0) {
1152
+ debug("Converter", "Skipping empty thinking message");
1153
+ this.currentThinkingMessage = null;
1154
+ return [];
1155
+ }
1156
+ const latencyMs = (event.timestamp || Date.now()) - this.currentThinkingMessage.startTime;
1157
+ const step = {
1158
+ id: uuidv4(),
1159
+ timestamp: this.currentThinkingMessage.startTime,
1160
+ type: "thinking",
1161
+ content,
1162
+ latencyMs
1163
+ };
1164
+ debug("Converter", `Thinking message completed: ${content.length} chars`);
1165
+ this.currentThinkingMessage = null;
1166
+ return [step];
1167
+ }
1168
+ extractToolName(title) {
1169
+ const runningMatch = title.match(/^Running\s+(.+)$/);
1170
+ if (runningMatch) return runningMatch[1];
1171
+ const completedMatch = title.match(/^(.+)\s+completed$/i);
1172
+ if (completedMatch) return completedMatch[1];
1173
+ return title;
1174
+ }
1175
+ parseToolArgs(description) {
1176
+ try {
1177
+ const args = {};
1178
+ const pairs = description.match(/(\w+):\s*("(?:[^"]|\\")*"|\w+)/g);
1179
+ if (pairs) {
1180
+ pairs.forEach((pair) => {
1181
+ const [key, rawValue] = pair.split(":").map((s) => s.trim());
1182
+ let value = rawValue;
1183
+ if (rawValue.startsWith('"') && rawValue.endsWith('"')) {
1184
+ value = rawValue.slice(1, -1);
1185
+ } else if (rawValue === "true") {
1186
+ value = true;
1187
+ } else if (rawValue === "false") {
1188
+ value = false;
1189
+ } else if (!isNaN(Number(rawValue))) {
1190
+ value = Number(rawValue);
1191
+ }
1192
+ args[key] = value;
1193
+ });
1194
+ return args;
1195
+ }
1196
+ return { description };
1197
+ } catch (e) {
1198
+ return { description };
1199
+ }
1200
+ }
1201
+ };
1202
+ function computeTrajectoryFromRawEvents(rawEvents) {
1203
+ const converter = new AGUIToTrajectoryConverter();
1204
+ const trajectory = [];
1205
+ for (const event of rawEvents) {
1206
+ const steps = converter.processEvent(event);
1207
+ trajectory.push(...steps);
1208
+ }
1209
+ trajectory.sort((a, b) => a.timestamp - b.timestamp);
1210
+ return trajectory;
1211
+ }
1212
+
1213
+ // services/connectors/agui/AGUIStreamingConnector.ts
1214
+ var AGUIStreamingConnector = class extends BaseConnector {
1215
+ constructor() {
1216
+ super(...arguments);
1217
+ this.type = "agui-streaming";
1218
+ this.name = "AG-UI Streaming";
1219
+ this.supportsStreaming = true;
1220
+ }
1221
+ /**
1222
+ * Build AG-UI payload from standard request
1223
+ */
1224
+ buildPayload(request) {
1225
+ return buildAgentPayload(
1226
+ request.testCase,
1227
+ request.modelId,
1228
+ request.threadId,
1229
+ request.runId
1230
+ );
1231
+ }
1232
+ /**
1233
+ * Execute the request using SSE streaming
1234
+ */
1235
+ async execute(endpoint, request, auth, onProgress, onRawEvent) {
1236
+ const hasPrebuiltPayload = !!request.payload;
1237
+ const payload = request.payload || this.buildPayload(request);
1238
+ const headers = this.buildAuthHeaders(auth);
1239
+ const trajectory = [];
1240
+ const rawEvents = [];
1241
+ const converter = new AGUIToTrajectoryConverter();
1242
+ this.debug("Executing AG-UI streaming request");
1243
+ await consumeSSEStream(
1244
+ endpoint,
1245
+ payload,
1246
+ (event) => {
1247
+ rawEvents.push(event);
1248
+ onRawEvent?.(event);
1249
+ const steps = converter.processEvent(event);
1250
+ steps.forEach((step) => {
1251
+ trajectory.push(step);
1252
+ onProgress?.(step);
1253
+ });
1254
+ },
1255
+ headers
1256
+ );
1257
+ const runId = converter.getRunId();
1258
+ this.debug("Stream completed. RunId:", runId, "Steps:", trajectory.length);
1259
+ return {
1260
+ trajectory,
1261
+ runId,
1262
+ rawEvents,
1263
+ metadata: {
1264
+ threadId: converter.getThreadId()
1265
+ }
1266
+ };
1267
+ }
1268
+ /**
1269
+ * Parse raw AG-UI events into trajectory steps
1270
+ * Used for re-processing stored raw events
1271
+ */
1272
+ parseResponse(rawEvents) {
1273
+ return computeTrajectoryFromRawEvents(rawEvents);
1274
+ }
1275
+ /**
1276
+ * Health check for AG-UI endpoint
1277
+ * Tries to connect without sending a full request
1278
+ */
1279
+ async healthCheck(endpoint, auth) {
1280
+ try {
1281
+ const headers = this.buildAuthHeaders(auth);
1282
+ const response = await fetch(endpoint, {
1283
+ method: "OPTIONS",
1284
+ headers
1285
+ });
1286
+ return true;
1287
+ } catch (error) {
1288
+ this.error("Health check failed:", error);
1289
+ return false;
1290
+ }
1291
+ }
1292
+ };
1293
+ var aguiStreamingConnector = new AGUIStreamingConnector();
1294
+
1295
+ // services/connectors/mock/MockConnector.ts
1296
+ var MockConnector = class extends BaseConnector {
1297
+ constructor() {
1298
+ super(...arguments);
1299
+ this.type = "mock";
1300
+ this.name = "Demo Agent (Mock)";
1301
+ this.supportsStreaming = true;
1302
+ }
1303
+ /**
1304
+ * Build payload - not used for mock but required by interface
1305
+ */
1306
+ buildPayload(request) {
1307
+ return {
1308
+ question: request.testCase.initialPrompt,
1309
+ context: request.testCase.context
1310
+ };
1311
+ }
1312
+ /**
1313
+ * Execute mock request - generates realistic trajectory with delays
1314
+ */
1315
+ async execute(endpoint, request, auth, onProgress, onRawEvent) {
1316
+ const trajectory = [];
1317
+ const rawEvents = [];
1318
+ const runId = `mock-run-${Date.now()}`;
1319
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1320
+ this.debug("Generating mock trajectory for:", request.testCase.name);
1321
+ const emitStep = (step) => {
1322
+ trajectory.push(step);
1323
+ onProgress?.(step);
1324
+ rawEvents.push({ type: "MOCK_STEP", step });
1325
+ onRawEvent?.({ type: "MOCK_STEP", step });
1326
+ };
1327
+ await sleep(100);
1328
+ emitStep(this.createStep(
1329
+ "assistant",
1330
+ "I need to investigate this issue. Let me start by checking the cluster health and then drill down into specific metrics."
1331
+ ));
1332
+ await sleep(300);
1333
+ emitStep(this.createStep(
1334
+ "action",
1335
+ "Calling opensearch_cluster_health...",
1336
+ {
1337
+ toolName: "opensearch_cluster_health",
1338
+ toolArgs: { local: true }
1339
+ }
1340
+ ));
1341
+ await sleep(500);
1342
+ emitStep(this.createStep(
1343
+ "tool_result",
1344
+ JSON.stringify({ status: "yellow", number_of_nodes: 3, unassigned_shards: 0 }, null, 2),
1345
+ {
1346
+ status: "SUCCESS",
1347
+ latencyMs: 500
1348
+ }
1349
+ ));
1350
+ await sleep(300);
1351
+ emitStep(this.createStep(
1352
+ "assistant",
1353
+ "The cluster is in yellow state. Let me check the node stats to identify which node might be causing issues."
1354
+ ));
1355
+ await sleep(300);
1356
+ emitStep(this.createStep(
1357
+ "action",
1358
+ "Calling opensearch_nodes_stats...",
1359
+ {
1360
+ toolName: "opensearch_nodes_stats",
1361
+ toolArgs: { metric: "jvm,os" }
1362
+ }
1363
+ ));
1364
+ await sleep(600);
1365
+ emitStep(this.createStep(
1366
+ "tool_result",
1367
+ "Node-1: CPU 12%, JVM Heap 45%\nNode-2: CPU 15%, JVM Heap 52%\nNode-3: CPU 98%, JVM Heap 89% (Data Node)",
1368
+ {
1369
+ status: "SUCCESS",
1370
+ latencyMs: 600
1371
+ }
1372
+ ));
1373
+ await sleep(400);
1374
+ emitStep(this.createStep(
1375
+ "response",
1376
+ `## Root Cause Analysis Complete
1377
+
1378
+ **Finding:** High CPU utilization detected on Node-3 (98% CPU, 89% JVM Heap)
1379
+
1380
+ **Root Cause:** Node-3 is experiencing resource exhaustion, likely due to:
1381
+ 1. Heavy indexing or search operations
1382
+ 2. Garbage collection pressure from high heap usage
1383
+ 3. Possible hot spot in shard distribution
1384
+
1385
+ **Recommendations:**
1386
+ 1. Check hot threads on Node-3 using \`_nodes/Node-3/hot_threads\`
1387
+ 2. Review shard distribution and consider rebalancing
1388
+ 3. Monitor GC logs for long pauses
1389
+ 4. Consider scaling horizontally if load persists`
1390
+ ));
1391
+ this.debug("Mock trajectory completed. Steps:", trajectory.length);
1392
+ return {
1393
+ trajectory,
1394
+ runId,
1395
+ rawEvents,
1396
+ metadata: {
1397
+ mock: true,
1398
+ testCaseId: request.testCase.id,
1399
+ testCaseName: request.testCase.name
1400
+ }
1401
+ };
1402
+ }
1403
+ /**
1404
+ * Parse raw events - for mock, just extract steps from our custom format
1405
+ */
1406
+ parseResponse(rawEvents) {
1407
+ return rawEvents.filter((e) => e.type === "MOCK_STEP" && e.step).map((e) => e.step);
1408
+ }
1409
+ /**
1410
+ * Health check - mock is always "healthy"
1411
+ */
1412
+ async healthCheck(endpoint, auth) {
1413
+ return true;
1414
+ }
1415
+ };
1416
+ var mockConnector = new MockConnector();
1417
+
1418
+ // services/connectors/rest/RESTConnector.ts
1419
+ var RESTConnector = class extends BaseConnector {
1420
+ constructor() {
1421
+ super(...arguments);
1422
+ this.type = "rest";
1423
+ this.name = "REST API";
1424
+ this.supportsStreaming = false;
1425
+ }
1426
+ /**
1427
+ * Build generic REST payload
1428
+ * Can be customized via connectorConfig
1429
+ */
1430
+ buildPayload(request) {
1431
+ return {
1432
+ prompt: request.testCase.initialPrompt,
1433
+ context: request.testCase.context,
1434
+ model: request.modelId,
1435
+ tools: request.testCase.tools
1436
+ };
1437
+ }
1438
+ /**
1439
+ * Execute REST request
1440
+ */
1441
+ async execute(endpoint, request, auth, onProgress, onRawEvent) {
1442
+ const payload = request.payload || this.buildPayload(request);
1443
+ const headers = this.buildAuthHeaders(auth);
1444
+ this.debug("Executing REST request");
1445
+ this.debug("Endpoint:", endpoint);
1446
+ this.debug("Payload:", JSON.stringify(payload).substring(0, 500));
1447
+ const response = await fetch(endpoint, {
1448
+ method: "POST",
1449
+ headers: {
1450
+ "Content-Type": "application/json",
1451
+ ...headers
1452
+ },
1453
+ body: JSON.stringify(payload)
1454
+ });
1455
+ if (!response.ok) {
1456
+ const errorText = await response.text();
1457
+ throw new Error(`REST request failed: ${response.status} - ${errorText}`);
1458
+ }
1459
+ const data = await response.json();
1460
+ onRawEvent?.(data);
1461
+ const trajectory = this.parseResponse(data);
1462
+ trajectory.forEach((step) => onProgress?.(step));
1463
+ return {
1464
+ trajectory,
1465
+ runId: data.runId || data.id || null,
1466
+ rawEvents: [data],
1467
+ metadata: {
1468
+ status: response.status,
1469
+ responseHeaders: Object.fromEntries(response.headers.entries())
1470
+ }
1471
+ };
1472
+ }
1473
+ /**
1474
+ * Parse REST response into trajectory steps
1475
+ * This is a generic implementation - subclass for specific APIs
1476
+ */
1477
+ parseResponse(data) {
1478
+ const steps = [];
1479
+ if (data.thinking) {
1480
+ steps.push(this.createStep("thinking", data.thinking));
1481
+ }
1482
+ if (data.toolCalls && Array.isArray(data.toolCalls)) {
1483
+ for (const call of data.toolCalls) {
1484
+ steps.push(this.createStep("action", `Calling ${call.name}...`, {
1485
+ toolName: call.name,
1486
+ toolArgs: call.args || call.input
1487
+ }));
1488
+ if (call.result !== void 0) {
1489
+ steps.push(this.createStep(
1490
+ "tool_result",
1491
+ typeof call.result === "string" ? call.result : JSON.stringify(call.result),
1492
+ { status: "SUCCESS" }
1493
+ ));
1494
+ }
1495
+ }
1496
+ }
1497
+ const responseContent = data.response || data.content || data.answer || data.text || data.message;
1498
+ if (responseContent) {
1499
+ steps.push(this.createStep(
1500
+ "response",
1501
+ typeof responseContent === "string" ? responseContent : JSON.stringify(responseContent)
1502
+ ));
1503
+ }
1504
+ if (data.inference_results) {
1505
+ const outputs = data.inference_results[0]?.output || [];
1506
+ for (const output of outputs) {
1507
+ if (output.name === "response") {
1508
+ const content = output.dataAsMap?.response || output.result;
1509
+ if (content) {
1510
+ steps.push(this.createStep("response", content));
1511
+ }
1512
+ }
1513
+ }
1514
+ }
1515
+ if (steps.length === 0 && data) {
1516
+ steps.push(this.createStep("response", JSON.stringify(data, null, 2)));
1517
+ }
1518
+ return steps;
1519
+ }
1520
+ };
1521
+ var restConnector = new RESTConnector();
1522
+
1523
+ // services/connectors/index.ts
1524
+ connectorRegistry.register(aguiStreamingConnector);
1525
+ connectorRegistry.register(mockConnector);
1526
+ connectorRegistry.register(restConnector);
1527
+ console.log("[Connectors] Browser-safe connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
1528
+
1529
+ // services/connectors/subprocess/SubprocessConnector.ts
1530
+ import { spawn } from "child_process";
1531
+ var DEFAULT_SUBPROCESS_CONFIG = {
1532
+ command: "",
1533
+ args: [],
1534
+ env: {},
1535
+ inputMode: "stdin",
1536
+ outputParser: "text",
1537
+ timeout: 3e5
1538
+ // 5 minutes
1539
+ };
1540
+ var SubprocessConnector = class extends BaseConnector {
1541
+ constructor(config) {
1542
+ super();
1543
+ this.type = "subprocess";
1544
+ this.name = "Subprocess (CLI)";
1545
+ this.supportsStreaming = true;
1546
+ this.config = { ...DEFAULT_SUBPROCESS_CONFIG, ...config };
1547
+ }
1548
+ /**
1549
+ * Build input for the subprocess
1550
+ */
1551
+ buildPayload(request) {
1552
+ let prompt = request.testCase.initialPrompt;
1553
+ if (request.testCase.context && request.testCase.context.length > 0) {
1554
+ const contextStr = request.testCase.context.map((c) => `${c.description}: ${c.value}`).join("\n");
1555
+ prompt = `Context:
1556
+ ${contextStr}
1557
+
1558
+ Question: ${prompt}`;
1559
+ }
1560
+ return prompt;
1561
+ }
1562
+ /**
1563
+ * Execute subprocess and capture output
1564
+ */
1565
+ async execute(endpoint, request, auth, onProgress, onRawEvent) {
1566
+ this.debug("========== execute() STARTED ==========");
1567
+ const command = endpoint || this.config.command;
1568
+ const args = this.config.args || [];
1569
+ const input = request.payload || this.buildPayload(request);
1570
+ this.debug("Command:", command);
1571
+ this.debug("Args:", args);
1572
+ this.debug("Input mode:", this.config.inputMode);
1573
+ this.debug("Output parser:", this.config.outputParser);
1574
+ this.debug("Timeout:", this.config.timeout);
1575
+ this.debug("Input (first 500 chars):", input.substring(0, 500));
1576
+ this.debug("Working dir:", this.config.workingDir || process.cwd());
1577
+ const env = {
1578
+ ...process.env,
1579
+ ...this.buildAuthEnv(auth),
1580
+ ...this.config.env
1581
+ };
1582
+ return new Promise((resolve5, reject) => {
1583
+ const trajectory = [];
1584
+ const rawOutput = [];
1585
+ let stdout = "";
1586
+ let stderr = "";
1587
+ let settled = false;
1588
+ const finalArgs = this.config.inputMode === "arg" ? [...args, input] : args;
1589
+ this.debug("Spawning process...");
1590
+ this.debug("Full command:", command, finalArgs.join(" "));
1591
+ const proc = spawn(command, finalArgs, {
1592
+ env,
1593
+ cwd: this.config.workingDir,
1594
+ shell: true
1595
+ });
1596
+ this.debug("Process spawned, PID:", proc.pid);
1597
+ const timeoutId = setTimeout(() => {
1598
+ if (settled) return;
1599
+ settled = true;
1600
+ this.debug("TIMEOUT reached, killing process");
1601
+ proc.kill("SIGTERM");
1602
+ reject(new Error(`Subprocess timed out after ${this.config.timeout}ms`));
1603
+ }, this.config.timeout);
1604
+ if (this.config.inputMode === "stdin") {
1605
+ this.debug("Writing input to stdin...");
1606
+ proc.stdin.write(input);
1607
+ proc.stdin.end();
1608
+ this.debug("stdin closed");
1609
+ }
1610
+ proc.stdout.on("data", (data) => {
1611
+ const chunk = data.toString();
1612
+ this.debug("stdout received:", chunk.length, "bytes");
1613
+ this.debug("stdout preview:", chunk.substring(0, 200));
1614
+ stdout += chunk;
1615
+ rawOutput.push({ type: "stdout", data: chunk, timestamp: Date.now() });
1616
+ onRawEvent?.({ type: "stdout", data: chunk });
1617
+ if (this.config.outputParser === "streaming") {
1618
+ this.parseStreamingOutput(chunk, trajectory, onProgress);
1619
+ }
1620
+ });
1621
+ proc.stderr.on("data", (data) => {
1622
+ const chunk = data.toString();
1623
+ this.debug("stderr received:", chunk.length, "bytes");
1624
+ this.debug("stderr:", chunk);
1625
+ stderr += chunk;
1626
+ onRawEvent?.({ type: "stderr", data: chunk });
1627
+ this.debug("stderr:", chunk);
1628
+ });
1629
+ proc.on("close", (code, signal) => {
1630
+ this.debug("Process closed with code:", code, "signal:", signal);
1631
+ clearTimeout(timeoutId);
1632
+ if (settled) return;
1633
+ settled = true;
1634
+ if (code !== 0) {
1635
+ this.debug("Non-zero exit code:", code);
1636
+ this.error(`Process exited with code ${code}`);
1637
+ this.error("stderr:", stderr);
1638
+ }
1639
+ const finalTrajectory = this.config.outputParser === "streaming" ? trajectory : this.parseResponse({ stdout, stderr, exitCode: code });
1640
+ if (this.config.outputParser !== "streaming") {
1641
+ finalTrajectory.forEach((step) => onProgress?.(step));
1642
+ }
1643
+ this.debug("Resolving with trajectory of", finalTrajectory.length, "steps");
1644
+ resolve5({
1645
+ trajectory: finalTrajectory,
1646
+ runId: `subprocess-${Date.now()}`,
1647
+ rawEvents: rawOutput,
1648
+ metadata: {
1649
+ command,
1650
+ args: finalArgs,
1651
+ exitCode: code,
1652
+ stderr: stderr || void 0
1653
+ }
1654
+ });
1655
+ });
1656
+ proc.on("error", (error) => {
1657
+ this.debug("ERROR event:", error.message);
1658
+ clearTimeout(timeoutId);
1659
+ if (settled) return;
1660
+ settled = true;
1661
+ let errorMsg = `Failed to spawn subprocess: ${error.message}`;
1662
+ if (error.message.includes("ENOENT")) {
1663
+ errorMsg = `Command '${command}' not found. Is it installed and in PATH?`;
1664
+ console.error(`[Subprocess] ENOENT error - command '${command}' not found in PATH`);
1665
+ } else if (error.message.includes("EACCES")) {
1666
+ errorMsg = `Permission denied executing '${command}'. Check file permissions.`;
1667
+ console.error(`[Subprocess] EACCES error - permission denied for '${command}'`);
1668
+ } else if (error.message.includes("EPERM")) {
1669
+ errorMsg = `Operation not permitted for '${command}'. May require elevated privileges.`;
1670
+ console.error(`[Subprocess] EPERM error - operation not permitted`);
1671
+ }
1672
+ reject(new Error(errorMsg));
1673
+ });
1674
+ });
1675
+ this.debug("========== execute() COMPLETED ==========");
1676
+ }
1677
+ /**
1678
+ * Parse streaming output and emit steps in real-time
1679
+ */
1680
+ parseStreamingOutput(chunk, trajectory, onProgress) {
1681
+ const lines = chunk.split("\n").filter((line) => line.trim());
1682
+ for (const line of lines) {
1683
+ const step = this.createStep("assistant", line);
1684
+ trajectory.push(step);
1685
+ onProgress?.(step);
1686
+ }
1687
+ }
1688
+ /**
1689
+ * Parse final subprocess output
1690
+ */
1691
+ parseResponse(data) {
1692
+ const steps = [];
1693
+ if (this.config.outputParser === "json") {
1694
+ try {
1695
+ const parsed = JSON.parse(data.stdout);
1696
+ return this.parseJsonOutput(parsed);
1697
+ } catch {
1698
+ this.debug("Failed to parse JSON output, falling back to text");
1699
+ }
1700
+ }
1701
+ if (data.stdout.trim()) {
1702
+ steps.push(this.createStep("response", data.stdout.trim()));
1703
+ }
1704
+ if (data.exitCode !== 0 && data.stderr.trim()) {
1705
+ steps.push(this.createStep("tool_result", `Error: ${data.stderr.trim()}`, {
1706
+ status: "FAILURE"
1707
+ }));
1708
+ }
1709
+ return steps;
1710
+ }
1711
+ /**
1712
+ * Parse JSON output into trajectory steps
1713
+ */
1714
+ parseJsonOutput(data) {
1715
+ const steps = [];
1716
+ if (data.thinking) {
1717
+ steps.push(this.createStep("thinking", data.thinking));
1718
+ }
1719
+ if (data.steps && Array.isArray(data.steps)) {
1720
+ for (const step of data.steps) {
1721
+ steps.push(this.createStep(step.type || "assistant", step.content, {
1722
+ toolName: step.toolName,
1723
+ toolArgs: step.toolArgs
1724
+ }));
1725
+ }
1726
+ }
1727
+ if (data.response || data.answer || data.content) {
1728
+ steps.push(this.createStep("response", data.response || data.answer || data.content));
1729
+ }
1730
+ return steps;
1731
+ }
1732
+ /**
1733
+ * Health check - verify command exists
1734
+ */
1735
+ async healthCheck(endpoint, auth) {
1736
+ const command = endpoint || this.config.command;
1737
+ if (!command) return false;
1738
+ return new Promise((resolve5) => {
1739
+ const proc = spawn("which", [command], { shell: true });
1740
+ proc.on("close", (code) => resolve5(code === 0));
1741
+ proc.on("error", () => resolve5(false));
1742
+ });
1743
+ }
1744
+ };
1745
+ var subprocessConnector = new SubprocessConnector();
1746
+
1747
+ // services/connectors/claude-code/ClaudeCodeConnector.ts
1748
+ var CLAUDE_CODE_DEFAULT_CONFIG = {
1749
+ command: "claude",
1750
+ args: ["--print", "--verbose", "--output-format", "stream-json"],
1751
+ // Structured JSON output (--verbose required with stream-json)
1752
+ env: {
1753
+ // These can be overridden by agent config or environment
1754
+ DISABLE_PROMPT_CACHING: "1",
1755
+ DISABLE_ERROR_REPORTING: "1"
1756
+ // Note: DISABLE_TELEMETRY removed - telemetry enabled by default
1757
+ // Configure OTEL_EXPORTER_OTLP_ENDPOINT in .env to send traces
1758
+ },
1759
+ inputMode: "stdin",
1760
+ outputParser: "streaming",
1761
+ timeout: 6e5
1762
+ // 10 minutes for Claude Code
1763
+ };
1764
+ var ClaudeCodeConnector = class extends SubprocessConnector {
1765
+ constructor(config) {
1766
+ super({ ...CLAUDE_CODE_DEFAULT_CONFIG, ...config });
1767
+ this.type = "claude-code";
1768
+ this.name = "Claude Code CLI";
1769
+ this.outputBuffer = "";
1770
+ this.thinkingBuffer = "";
1771
+ this.isInThinking = false;
1772
+ }
1773
+ /**
1774
+ * Build prompt for Claude Code
1775
+ * Structures the input to get the best RCA results
1776
+ */
1777
+ buildPayload(request) {
1778
+ const parts = [];
1779
+ if (request.testCase.context && request.testCase.context.length > 0) {
1780
+ parts.push("## Context");
1781
+ for (const ctx of request.testCase.context) {
1782
+ parts.push(`**${ctx.description}:**`);
1783
+ parts.push(ctx.value);
1784
+ parts.push("");
1785
+ }
1786
+ }
1787
+ parts.push("## Task");
1788
+ parts.push(request.testCase.initialPrompt);
1789
+ return parts.join("\n");
1790
+ }
1791
+ /**
1792
+ * Parse Claude Code streaming output (stream-json format)
1793
+ * Each line is a JSON object with type and content
1794
+ */
1795
+ parseStreamingOutput(chunk, trajectory, onProgress) {
1796
+ this.outputBuffer += chunk;
1797
+ const lines = this.outputBuffer.split("\n");
1798
+ this.outputBuffer = lines.pop() || "";
1799
+ for (const line of lines) {
1800
+ const trimmed = line.trim();
1801
+ if (!trimmed) continue;
1802
+ try {
1803
+ const event = JSON.parse(trimmed);
1804
+ const steps = this.parseJsonEvent(event);
1805
+ for (const step of steps) {
1806
+ trajectory.push(step);
1807
+ onProgress?.(step);
1808
+ }
1809
+ } catch {
1810
+ if (trimmed) {
1811
+ const step = this.createStep("assistant", trimmed);
1812
+ trajectory.push(step);
1813
+ onProgress?.(step);
1814
+ }
1815
+ }
1816
+ }
1817
+ }
1818
+ /**
1819
+ * Parse a single JSON event from stream-json output
1820
+ */
1821
+ parseJsonEvent(event) {
1822
+ const steps = [];
1823
+ if (event.type === "assistant" && event.message?.content) {
1824
+ for (const block of event.message.content) {
1825
+ if (block.type === "thinking" && block.thinking) {
1826
+ steps.push(this.createStep("thinking", block.thinking));
1827
+ } else if (block.type === "text" && block.text) {
1828
+ steps.push(this.createStep("assistant", block.text));
1829
+ } else if (block.type === "tool_use") {
1830
+ steps.push(this.createStep("action", JSON.stringify(block.input || {}), {
1831
+ toolName: block.name,
1832
+ toolArgs: block.input
1833
+ }));
1834
+ }
1835
+ }
1836
+ } else if (event.type === "content_block_delta") {
1837
+ if (event.delta?.type === "thinking_delta" && event.delta.thinking) {
1838
+ this.thinkingBuffer += event.delta.thinking;
1839
+ } else if (event.delta?.type === "text_delta" && event.delta.text) {
1840
+ steps.push(this.createStep("assistant", event.delta.text));
1841
+ }
1842
+ } else if (event.type === "content_block_stop" && this.thinkingBuffer) {
1843
+ steps.push(this.createStep("thinking", this.thinkingBuffer));
1844
+ this.thinkingBuffer = "";
1845
+ } else if (event.type === "result" && event.result) {
1846
+ steps.push(this.createStep(
1847
+ "response",
1848
+ typeof event.result === "string" ? event.result : JSON.stringify(event.result)
1849
+ ));
1850
+ } else if (event.type === "tool_result") {
1851
+ steps.push(this.createStep(
1852
+ "tool_result",
1853
+ typeof event.content === "string" ? event.content : JSON.stringify(event.content),
1854
+ { status: event.is_error ? "FAILURE" /* FAILURE */ : "SUCCESS" /* SUCCESS */ }
1855
+ ));
1856
+ }
1857
+ return steps;
1858
+ }
1859
+ /**
1860
+ * Parse final output for Claude Code
1861
+ */
1862
+ parseResponse(data) {
1863
+ const steps = [];
1864
+ let content = data.stdout;
1865
+ const thinkingMatches = content.matchAll(/<thinking>([\s\S]*?)<\/thinking>/g);
1866
+ for (const match of thinkingMatches) {
1867
+ const thinking = match[1].trim();
1868
+ if (thinking) {
1869
+ steps.push(this.createStep("thinking", thinking));
1870
+ }
1871
+ content = content.replace(match[0], "");
1872
+ }
1873
+ const response = content.trim();
1874
+ if (response) {
1875
+ steps.push(this.createStep("response", response));
1876
+ }
1877
+ if (data.exitCode !== 0 && data.stderr.trim()) {
1878
+ steps.push(this.createStep("tool_result", `Error: ${data.stderr.trim()}`, {
1879
+ status: "FAILURE" /* FAILURE */
1880
+ }));
1881
+ }
1882
+ return steps;
1883
+ }
1884
+ /**
1885
+ * Reset state for new execution
1886
+ */
1887
+ resetState() {
1888
+ this.outputBuffer = "";
1889
+ this.thinkingBuffer = "";
1890
+ this.isInThinking = false;
1891
+ }
1892
+ /**
1893
+ * Override execute to reset state
1894
+ */
1895
+ async execute(endpoint, request, auth, onProgress, onRawEvent) {
1896
+ this.debug("========== execute() STARTED ==========");
1897
+ this.debug("Endpoint:", endpoint);
1898
+ this.debug("Test case:", request.testCase.name);
1899
+ this.debug("Config:", this["config"]);
1900
+ this.resetState();
1901
+ this.debug("State reset, calling super.execute()...");
1902
+ const result = await super.execute(endpoint, request, auth, onProgress, onRawEvent);
1903
+ this.debug("super.execute() returned with", result.trajectory.length, "steps");
1904
+ this.debug("========== execute() COMPLETED ==========");
1905
+ return result;
1906
+ }
1907
+ /**
1908
+ * Health check - verify claude command exists
1909
+ */
1910
+ async healthCheck(endpoint, auth) {
1911
+ return super.healthCheck(endpoint || "claude", auth);
1912
+ }
1913
+ };
1914
+ var claudeCodeConnector = new ClaudeCodeConnector();
1915
+
1916
+ // services/connectors/server.ts
1917
+ connectorRegistry.register(subprocessConnector);
1918
+ connectorRegistry.register(claudeCodeConnector);
1919
+ console.log("[Connectors] Server connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
1920
+
1921
+ // cli/utils/serverLifecycle.ts
1922
+ import { spawn as spawn2, execSync } from "child_process";
1923
+ import net from "net";
1924
+ import { readFileSync } from "fs";
1925
+ import { fileURLToPath as fileURLToPath2 } from "url";
1926
+ import { dirname as dirname2, join as join3 } from "path";
1927
+ var __filename2 = fileURLToPath2(import.meta.url);
1928
+ var __dirname2 = dirname2(__filename2);
1929
+ var packageJsonPath = join3(__dirname2, "..", "..", "package.json");
1930
+ var cachedVersion = null;
1931
+ function getCliVersion() {
1932
+ if (cachedVersion !== null) {
1933
+ return cachedVersion;
1934
+ }
1935
+ try {
1936
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
1937
+ cachedVersion = packageJson.version || "unknown";
1938
+ } catch {
1939
+ try {
1940
+ const altPath = join3(__dirname2, "..", "..", "..", "package.json");
1941
+ const packageJson = JSON.parse(readFileSync(altPath, "utf-8"));
1942
+ cachedVersion = packageJson.version || "unknown";
1943
+ } catch {
1944
+ cachedVersion = "unknown";
1945
+ }
1946
+ }
1947
+ return cachedVersion;
1948
+ }
1949
+ async function isServerRunning(port) {
1950
+ const controller = new AbortController();
1951
+ const timeout = setTimeout(() => controller.abort(), 2e3);
1952
+ try {
1953
+ const response = await fetch(`http://localhost:${port}/health`, {
1954
+ signal: controller.signal
1955
+ });
1956
+ if (response.ok) {
1957
+ return true;
1958
+ }
1959
+ } catch {
1960
+ } finally {
1961
+ clearTimeout(timeout);
1962
+ }
1963
+ return new Promise((resolve5) => {
1964
+ const socket = new net.Socket();
1965
+ socket.setTimeout(1e3);
1966
+ socket.on("connect", () => {
1967
+ socket.destroy();
1968
+ resolve5(true);
1969
+ });
1970
+ socket.on("timeout", () => {
1971
+ socket.destroy();
1972
+ resolve5(false);
1973
+ });
1974
+ socket.on("error", () => {
1975
+ resolve5(false);
1976
+ });
1977
+ socket.connect(port, "localhost");
1978
+ });
1979
+ }
1980
+ async function checkServerStatus(port) {
1981
+ const controller = new AbortController();
1982
+ const timeout = setTimeout(() => controller.abort(), 2e3);
1983
+ try {
1984
+ const response = await fetch(`http://localhost:${port}/health`, {
1985
+ signal: controller.signal
1986
+ });
1987
+ if (response.ok) {
1988
+ const data = await response.json();
1989
+ return {
1990
+ running: true,
1991
+ version: data.version
1992
+ };
1993
+ }
1994
+ } catch {
1995
+ } finally {
1996
+ clearTimeout(timeout);
1997
+ }
1998
+ return { running: false };
1999
+ }
2000
+ async function killServerOnPort(port) {
2001
+ try {
2002
+ if (process.platform !== "win32") {
2003
+ try {
2004
+ execSync(`lsof -t -i:${port} -sTCP:LISTEN | xargs kill -9 2>/dev/null || true`, { stdio: "ignore" });
2005
+ } catch {
2006
+ }
2007
+ } else {
2008
+ try {
2009
+ const result = execSync(`netstat -ano | findstr :${port}`, { encoding: "utf-8" });
2010
+ const lines = result.trim().split("\n");
2011
+ for (const line of lines) {
2012
+ const parts = line.trim().split(/\s+/);
2013
+ const pid = parts[parts.length - 1];
2014
+ if (pid && !isNaN(parseInt(pid))) {
2015
+ try {
2016
+ execSync(`taskkill /PID ${pid} /F`, { stdio: "ignore" });
2017
+ } catch {
2018
+ }
2019
+ }
2020
+ }
2021
+ } catch {
2022
+ }
2023
+ }
2024
+ const maxRetries = 10;
2025
+ const retryDelay = 500;
2026
+ for (let i = 0; i < maxRetries; i++) {
2027
+ await new Promise((r) => setTimeout(r, retryDelay));
2028
+ const stillRunning = await isServerRunning(port);
2029
+ if (!stillRunning) {
2030
+ return;
2031
+ }
2032
+ }
2033
+ console.warn(`[ServerLifecycle] Port ${port} may still be in use after ${maxRetries} retries`);
2034
+ } catch {
2035
+ }
2036
+ }
2037
+ async function waitForServer(port, timeout) {
2038
+ const startTime = Date.now();
2039
+ const pollInterval = 500;
2040
+ while (Date.now() - startTime < timeout) {
2041
+ if (await isServerRunning(port)) {
2042
+ return true;
2043
+ }
2044
+ await new Promise((r) => setTimeout(r, pollInterval));
2045
+ }
2046
+ return false;
2047
+ }
2048
+ async function startServer2(port, timeout) {
2049
+ const packageRoot = join3(__dirname2, "..", "..");
2050
+ const cliPath = join3(packageRoot, "bin", "cli.js");
2051
+ const child = spawn2("node", [cliPath, "serve", "-p", String(port), "--no-browser"], {
2052
+ detached: true,
2053
+ stdio: ["ignore", "pipe", "pipe"],
2054
+ env: {
2055
+ ...process.env
2056
+ }
2057
+ });
2058
+ let stderrOutput = "";
2059
+ let stdoutOutput = "";
2060
+ child.stderr?.on("data", (data) => {
2061
+ stderrOutput += data.toString();
2062
+ });
2063
+ child.stdout?.on("data", (data) => {
2064
+ stdoutOutput += data.toString();
2065
+ });
2066
+ let earlyExit = false;
2067
+ let exitCode = null;
2068
+ child.on("exit", (code) => {
2069
+ earlyExit = true;
2070
+ exitCode = code;
2071
+ });
2072
+ child.unref();
2073
+ const ready = await waitForServer(port, timeout);
2074
+ if (!ready) {
2075
+ try {
2076
+ child.kill();
2077
+ } catch {
2078
+ }
2079
+ if (earlyExit) {
2080
+ console.error(`[ServerLifecycle] Server process exited with code ${exitCode} before becoming ready`);
2081
+ } else {
2082
+ console.error(`[ServerLifecycle] Server process did not respond to health checks within ${timeout}ms`);
2083
+ }
2084
+ if (stderrOutput) {
2085
+ console.error(`[ServerLifecycle] Server stderr:
2086
+ ${stderrOutput}`);
2087
+ }
2088
+ if (stdoutOutput) {
2089
+ console.error(`[ServerLifecycle] Server stdout:
2090
+ ${stdoutOutput}`);
2091
+ }
2092
+ if (!stderrOutput && !stdoutOutput) {
2093
+ console.error(`[ServerLifecycle] No output captured from server process`);
2094
+ console.error(`[ServerLifecycle] CLI path: ${cliPath}`);
2095
+ console.error(`[ServerLifecycle] Package root: ${packageRoot}`);
2096
+ }
2097
+ throw new Error(`Server failed to start within ${timeout}ms on port ${port}`);
2098
+ }
2099
+ return child;
2100
+ }
2101
+ function stopServer(process2) {
2102
+ try {
2103
+ if (process2.pid) {
2104
+ try {
2105
+ process2.kill("SIGTERM");
2106
+ } catch {
2107
+ }
2108
+ }
2109
+ } catch {
2110
+ }
2111
+ }
2112
+ async function ensureServer(config) {
2113
+ const { port, reuseExistingServer, startTimeout } = config;
2114
+ const baseUrl = `http://localhost:${port}`;
2115
+ const serverStatus = await checkServerStatus(port);
2116
+ const cliVersion = getCliVersion();
2117
+ if (serverStatus.running) {
2118
+ const versionMatches = serverStatus.version === cliVersion || serverStatus.version === "unknown" || cliVersion === "unknown";
2119
+ if (!versionMatches) {
2120
+ console.log(`[ServerLifecycle] Version mismatch detected!`);
2121
+ console.log(`[ServerLifecycle] Server version: ${serverStatus.version}`);
2122
+ console.log(`[ServerLifecycle] CLI version: ${cliVersion}`);
2123
+ if (reuseExistingServer) {
2124
+ console.log(`[ServerLifecycle] Stopping old server and starting v${cliVersion}...`);
2125
+ await killServerOnPort(port);
2126
+ } else {
2127
+ throw new Error(
2128
+ `Server version mismatch: server=${serverStatus.version}, CLI=${cliVersion}. Stop the existing server or upgrade to matching version.`
2129
+ );
2130
+ }
2131
+ } else if (reuseExistingServer) {
2132
+ console.log(`[ServerLifecycle] Reusing existing server (version ${serverStatus.version})`);
2133
+ return {
2134
+ wasStarted: false,
2135
+ baseUrl
2136
+ };
2137
+ } else {
2138
+ throw new Error(
2139
+ `Server already running on port ${port}. In CI mode (reuseExistingServer=false), this is an error. Stop the existing server or set reuseExistingServer: true.`
2140
+ );
2141
+ }
2142
+ }
2143
+ const serverProcess = await startServer2(port, startTimeout);
2144
+ return {
2145
+ wasStarted: true,
2146
+ baseUrl,
2147
+ process: serverProcess
2148
+ };
2149
+ }
2150
+ function createServerCleanup(result, isCI) {
2151
+ return () => {
2152
+ if (result.wasStarted && isCI && result.process) {
2153
+ stopServer(result.process);
2154
+ }
2155
+ };
2156
+ }
2157
+
2158
+ // cli/utils/apiClient.ts
2159
+ var ApiClient = class {
2160
+ constructor(baseUrl) {
2161
+ this.baseUrl = baseUrl;
2162
+ }
2163
+ /**
2164
+ * Check if server is healthy, with optional retries and exponential backoff.
2165
+ */
2166
+ async checkHealth(retries = 2, delayMs = 500) {
2167
+ let lastError;
2168
+ for (let attempt = 0; attempt <= retries; attempt++) {
2169
+ try {
2170
+ const res = await fetch(`${this.baseUrl}/health`);
2171
+ if (!res.ok) {
2172
+ throw new Error(`Server health check failed: ${res.status} ${res.statusText}`);
2173
+ }
2174
+ return res.json();
2175
+ } catch (err) {
2176
+ lastError = err instanceof Error ? err : new Error(String(err));
2177
+ if (attempt < retries) {
2178
+ await new Promise((resolve5) => setTimeout(resolve5, delayMs * Math.pow(2, attempt)));
2179
+ }
2180
+ }
2181
+ }
2182
+ throw lastError;
2183
+ }
2184
+ /**
2185
+ * List all benchmarks
2186
+ */
2187
+ async listBenchmarks() {
2188
+ const res = await fetch(`${this.baseUrl}/api/storage/benchmarks`);
2189
+ if (!res.ok) {
2190
+ throw new Error(`Failed to list benchmarks: ${res.status} ${res.statusText}`);
2191
+ }
2192
+ const data = await res.json();
2193
+ return data.benchmarks || [];
2194
+ }
2195
+ /**
2196
+ * Get benchmark by ID
2197
+ */
2198
+ async getBenchmark(id) {
2199
+ const res = await fetch(`${this.baseUrl}/api/storage/benchmarks/${encodeURIComponent(id)}`);
2200
+ if (res.status === 404) {
2201
+ return null;
2202
+ }
2203
+ if (!res.ok) {
2204
+ throw new Error(`Failed to get benchmark: ${res.status} ${res.statusText}`);
2205
+ }
2206
+ return res.json();
2207
+ }
2208
+ /**
2209
+ * Find benchmark by name or ID
2210
+ *
2211
+ * Prioritizes:
2212
+ * 1. Exact ID match
2213
+ * 2. Exact name match (case-sensitive)
2214
+ */
2215
+ async findBenchmark(identifier) {
2216
+ const byId = await this.getBenchmark(identifier);
2217
+ if (byId) {
2218
+ return byId;
2219
+ }
2220
+ const benchmarks = await this.listBenchmarks();
2221
+ return benchmarks.find((b) => b.name === identifier) || null;
2222
+ }
2223
+ /**
2224
+ * Execute benchmark run (SSE stream)
2225
+ *
2226
+ * Streams progress events and returns the completed run.
2227
+ * If the SSE stream disconnects, falls back to polling for status.
2228
+ */
2229
+ async executeBenchmark(benchmarkId, runConfig, onProgress) {
2230
+ const res = await fetch(
2231
+ `${this.baseUrl}/api/storage/benchmarks/${encodeURIComponent(benchmarkId)}/execute`,
2232
+ {
2233
+ method: "POST",
2234
+ headers: { "Content-Type": "application/json" },
2235
+ body: JSON.stringify(runConfig)
2236
+ }
2237
+ );
2238
+ if (!res.ok) {
2239
+ const errorBody = await res.text();
2240
+ let errorMessage;
2241
+ try {
2242
+ const parsed = JSON.parse(errorBody);
2243
+ errorMessage = parsed.error || errorBody;
2244
+ } catch {
2245
+ errorMessage = errorBody;
2246
+ }
2247
+ throw new Error(`Failed to execute benchmark: ${errorMessage}`);
2248
+ }
2249
+ if (!res.body) {
2250
+ throw new Error("Response body is missing");
2251
+ }
2252
+ const reader = res.body.getReader();
2253
+ const decoder = new TextDecoder();
2254
+ let buffer = "";
2255
+ let finalRun = null;
2256
+ let runId = null;
2257
+ try {
2258
+ while (true) {
2259
+ const { done, value } = await reader.read();
2260
+ if (done) break;
2261
+ buffer += decoder.decode(value, { stream: true });
2262
+ const lines = buffer.split("\n\n");
2263
+ buffer = lines.pop() || "";
2264
+ for (const line of lines) {
2265
+ if (line.startsWith("data: ")) {
2266
+ try {
2267
+ const event = JSON.parse(line.slice(6));
2268
+ onProgress?.(event);
2269
+ if (event.type === "started") {
2270
+ runId = event.runId;
2271
+ } else if (event.type === "completed" || event.type === "cancelled") {
2272
+ finalRun = event.run;
2273
+ } else if (event.type === "error") {
2274
+ throw new Error(event.error);
2275
+ }
2276
+ } catch (e) {
2277
+ if (e instanceof SyntaxError) continue;
2278
+ throw e;
2279
+ }
2280
+ }
2281
+ }
2282
+ }
2283
+ } catch (streamError) {
2284
+ if (runId) {
2285
+ console.warn(`[ApiClient] SSE stream disconnected: ${streamError instanceof Error ? streamError.message : streamError}`);
2286
+ console.warn(`[ApiClient] Falling back to polling for run ${runId}...`);
2287
+ await new Promise((resolve5) => setTimeout(resolve5, 2e3));
2288
+ const polledRun = await this.pollRunStatus(benchmarkId, runId, (run) => {
2289
+ const completedCount = Object.values(run.results || {}).filter(
2290
+ (r) => r.status === "completed" || r.status === "failed"
2291
+ ).length;
2292
+ const totalCount = Object.keys(run.results || {}).length;
2293
+ onProgress?.({
2294
+ type: "progress",
2295
+ currentTestCaseIndex: completedCount - 1,
2296
+ totalTestCases: totalCount,
2297
+ currentTestCase: { id: "polling", name: "Polling for status..." }
2298
+ });
2299
+ });
2300
+ if (polledRun) {
2301
+ return polledRun;
2302
+ }
2303
+ }
2304
+ throw streamError;
2305
+ } finally {
2306
+ try {
2307
+ await reader.cancel();
2308
+ } catch {
2309
+ }
2310
+ }
2311
+ if (!finalRun) {
2312
+ if (runId) {
2313
+ console.warn("[ApiClient] SSE stream ended without completion event, polling for status...");
2314
+ const polledRun = await this.pollRunStatus(benchmarkId, runId);
2315
+ if (polledRun) {
2316
+ return polledRun;
2317
+ }
2318
+ }
2319
+ throw new Error("No final run received from server");
2320
+ }
2321
+ return finalRun;
2322
+ }
2323
+ /**
2324
+ * Cancel an in-progress benchmark run
2325
+ */
2326
+ async cancelRun(benchmarkId, runId) {
2327
+ const res = await fetch(
2328
+ `${this.baseUrl}/api/storage/benchmarks/${encodeURIComponent(benchmarkId)}/cancel`,
2329
+ {
2330
+ method: "POST",
2331
+ headers: { "Content-Type": "application/json" },
2332
+ body: JSON.stringify({ runId })
2333
+ }
2334
+ );
2335
+ if (!res.ok) {
2336
+ const errorBody = await res.text();
2337
+ throw new Error(`Failed to cancel run: ${errorBody}`);
2338
+ }
2339
+ }
2340
+ /**
2341
+ * Get a specific run from a benchmark by ID.
2342
+ *
2343
+ * Fetches the benchmark and extracts the run with the matching ID.
2344
+ *
2345
+ * @param benchmarkId - The benchmark ID
2346
+ * @param runId - The run ID within the benchmark
2347
+ * @returns The run if found, null otherwise
2348
+ */
2349
+ async getRun(benchmarkId, runId) {
2350
+ const benchmark = await this.getBenchmark(benchmarkId);
2351
+ if (!benchmark) {
2352
+ return null;
2353
+ }
2354
+ return benchmark.runs?.find((r) => r.id === runId) || null;
2355
+ }
2356
+ /**
2357
+ * Poll a run until it reaches a terminal state (completed, failed, cancelled).
2358
+ *
2359
+ * Used as a fallback when SSE stream connection is lost but server continues
2360
+ * processing in the background.
2361
+ *
2362
+ * @param benchmarkId - The benchmark ID
2363
+ * @param runId - The run ID to poll
2364
+ * @param onProgress - Optional callback for progress updates during polling
2365
+ * @param timeoutMs - Maximum time to wait (default: 10 minutes)
2366
+ * @returns The final run state, or null if not found
2367
+ */
2368
+ async pollRunStatus(benchmarkId, runId, onProgress, timeoutMs = 6e5) {
2369
+ const startTime = Date.now();
2370
+ const pollInterval = 5e3;
2371
+ while (Date.now() - startTime < timeoutMs) {
2372
+ const run = await this.getRun(benchmarkId, runId);
2373
+ if (!run) return null;
2374
+ onProgress?.(run);
2375
+ if (run.status && ["completed", "failed", "cancelled"].includes(run.status)) {
2376
+ return run;
2377
+ }
2378
+ await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
2379
+ }
2380
+ return this.getRun(benchmarkId, runId);
2381
+ }
2382
+ /**
2383
+ * Get a single report (TestCaseRun) by ID.
2384
+ *
2385
+ * This is the preferred method for fetching reports - use report IDs
2386
+ * from run.results[testCaseId].reportId.
2387
+ *
2388
+ * @param reportId - The report ID to fetch
2389
+ * @returns The report if found, null otherwise
2390
+ */
2391
+ async getReportById(reportId) {
2392
+ const res = await fetch(
2393
+ `${this.baseUrl}/api/storage/runs/${encodeURIComponent(reportId)}`
2394
+ );
2395
+ if (res.status === 404) {
2396
+ return null;
2397
+ }
2398
+ if (!res.ok) {
2399
+ throw new Error(`Failed to get report: ${res.status} ${res.statusText}`);
2400
+ }
2401
+ return res.json();
2402
+ }
2403
+ /**
2404
+ * List all test cases (basic)
2405
+ */
2406
+ async listTestCases() {
2407
+ const res = await fetch(`${this.baseUrl}/api/storage/test-cases`);
2408
+ if (!res.ok) {
2409
+ throw new Error(`Failed to list test cases: ${res.status} ${res.statusText}`);
2410
+ }
2411
+ const data = await res.json();
2412
+ return data.testCases || [];
2413
+ }
2414
+ /**
2415
+ * List all test cases with full data and metadata
2416
+ */
2417
+ async listTestCasesWithMeta() {
2418
+ const res = await fetch(`${this.baseUrl}/api/storage/test-cases`);
2419
+ if (!res.ok) {
2420
+ throw new Error(`Failed to list test cases: ${res.status} ${res.statusText}`);
2421
+ }
2422
+ const data = await res.json();
2423
+ return {
2424
+ data: data.testCases || [],
2425
+ total: data.total || 0,
2426
+ meta: data.meta || {
2427
+ storageConfigured: false,
2428
+ storageReachable: false,
2429
+ realDataCount: 0,
2430
+ sampleDataCount: data.testCases?.length || 0
2431
+ }
2432
+ };
2433
+ }
2434
+ /**
2435
+ * Bulk create test cases
2436
+ */
2437
+ async bulkCreateTestCases(testCases) {
2438
+ const res = await fetch(`${this.baseUrl}/api/storage/test-cases/bulk`, {
2439
+ method: "POST",
2440
+ headers: { "Content-Type": "application/json" },
2441
+ body: JSON.stringify({ testCases })
2442
+ });
2443
+ if (!res.ok) {
2444
+ const errorBody = await res.text();
2445
+ let errorMessage;
2446
+ try {
2447
+ const parsed = JSON.parse(errorBody);
2448
+ errorMessage = parsed.error || errorBody;
2449
+ } catch {
2450
+ errorMessage = errorBody;
2451
+ }
2452
+ throw new Error(`Failed to bulk create test cases: ${errorMessage}`);
2453
+ }
2454
+ return res.json();
2455
+ }
2456
+ /**
2457
+ * List all benchmarks with metadata
2458
+ */
2459
+ async listBenchmarksWithMeta() {
2460
+ const res = await fetch(`${this.baseUrl}/api/storage/benchmarks`);
2461
+ if (!res.ok) {
2462
+ throw new Error(`Failed to list benchmarks: ${res.status} ${res.statusText}`);
2463
+ }
2464
+ const data = await res.json();
2465
+ return {
2466
+ data: data.benchmarks || [],
2467
+ total: data.total || 0,
2468
+ meta: data.meta || {
2469
+ storageConfigured: false,
2470
+ storageReachable: false,
2471
+ realDataCount: 0,
2472
+ sampleDataCount: data.benchmarks?.length || 0
2473
+ }
2474
+ };
2475
+ }
2476
+ /**
2477
+ * List all configured agents
2478
+ */
2479
+ async listAgents() {
2480
+ const res = await fetch(`${this.baseUrl}/api/agents`);
2481
+ if (!res.ok) {
2482
+ throw new Error(`Failed to list agents: ${res.status} ${res.statusText}`);
2483
+ }
2484
+ const data = await res.json();
2485
+ return data.agents || [];
2486
+ }
2487
+ /**
2488
+ * List all configured models
2489
+ */
2490
+ async listModels() {
2491
+ const res = await fetch(`${this.baseUrl}/api/models`);
2492
+ if (!res.ok) {
2493
+ throw new Error(`Failed to list models: ${res.status} ${res.statusText}`);
2494
+ }
2495
+ const data = await res.json();
2496
+ return data.models || [];
2497
+ }
2498
+ /**
2499
+ * Get a single test case by ID
2500
+ */
2501
+ async getTestCase(id) {
2502
+ const res = await fetch(`${this.baseUrl}/api/storage/test-cases/${encodeURIComponent(id)}`);
2503
+ if (res.status === 404) {
2504
+ return null;
2505
+ }
2506
+ if (!res.ok) {
2507
+ throw new Error(`Failed to get test case: ${res.status} ${res.statusText}`);
2508
+ }
2509
+ return res.json();
2510
+ }
2511
+ /**
2512
+ * Find test case by ID or name
2513
+ */
2514
+ async findTestCase(identifier) {
2515
+ const byId = await this.getTestCase(identifier);
2516
+ if (byId) {
2517
+ return byId;
2518
+ }
2519
+ const response = await this.listTestCasesWithMeta();
2520
+ return response.data.find(
2521
+ (tc) => tc.name.toLowerCase() === identifier.toLowerCase()
2522
+ ) || null;
2523
+ }
2524
+ /**
2525
+ * Evaluation progress event types
2526
+ */
2527
+ /**
2528
+ * Run a single test case evaluation via server API (SSE stream)
2529
+ */
2530
+ async runEvaluation(testCaseId, agentKey, modelId, onProgress) {
2531
+ const res = await fetch(`${this.baseUrl}/api/evaluate`, {
2532
+ method: "POST",
2533
+ headers: { "Content-Type": "application/json" },
2534
+ body: JSON.stringify({ testCaseId, agentKey, modelId })
2535
+ });
2536
+ if (!res.ok) {
2537
+ const errorBody = await res.text();
2538
+ let errorMessage;
2539
+ try {
2540
+ const parsed = JSON.parse(errorBody);
2541
+ errorMessage = parsed.error || errorBody;
2542
+ } catch {
2543
+ errorMessage = errorBody;
2544
+ }
2545
+ throw new Error(`Failed to run evaluation: ${errorMessage}`);
2546
+ }
2547
+ if (!res.body) {
2548
+ throw new Error("Response body is missing");
2549
+ }
2550
+ const reader = res.body.getReader();
2551
+ const decoder = new TextDecoder();
2552
+ let buffer = "";
2553
+ let result = null;
2554
+ try {
2555
+ while (true) {
2556
+ const { done, value } = await reader.read();
2557
+ if (done) break;
2558
+ buffer += decoder.decode(value, { stream: true });
2559
+ const lines = buffer.split("\n\n");
2560
+ buffer = lines.pop() || "";
2561
+ for (const line of lines) {
2562
+ if (line.startsWith("data: ")) {
2563
+ try {
2564
+ const event = JSON.parse(line.slice(6));
2565
+ onProgress?.(event);
2566
+ if (event.type === "completed") {
2567
+ result = event.report;
2568
+ } else if (event.type === "error") {
2569
+ throw new Error(event.error);
2570
+ }
2571
+ } catch (e) {
2572
+ if (e instanceof SyntaxError) continue;
2573
+ throw e;
2574
+ }
2575
+ }
2576
+ }
2577
+ }
2578
+ } finally {
2579
+ try {
2580
+ await reader.cancel();
2581
+ } catch {
2582
+ }
2583
+ }
2584
+ if (!result) {
2585
+ throw new Error("No result received from evaluation");
2586
+ }
2587
+ return result;
2588
+ }
2589
+ /**
2590
+ * Export benchmark test cases as import-compatible JSON
2591
+ */
2592
+ async exportBenchmark(benchmarkId) {
2593
+ const res = await fetch(
2594
+ `${this.baseUrl}/api/storage/benchmarks/${encodeURIComponent(benchmarkId)}/export`
2595
+ );
2596
+ if (res.status === 404) {
2597
+ throw new Error(`Benchmark not found: ${benchmarkId}`);
2598
+ }
2599
+ if (!res.ok) {
2600
+ throw new Error(`Failed to export benchmark: ${res.status} ${res.statusText}`);
2601
+ }
2602
+ return res.json();
2603
+ }
2604
+ /**
2605
+ * Create a new benchmark
2606
+ */
2607
+ async createBenchmark(input) {
2608
+ const res = await fetch(`${this.baseUrl}/api/storage/benchmarks`, {
2609
+ method: "POST",
2610
+ headers: { "Content-Type": "application/json" },
2611
+ body: JSON.stringify(input)
2612
+ });
2613
+ if (!res.ok) {
2614
+ const errorBody = await res.text();
2615
+ throw new Error(`Failed to create benchmark: ${errorBody}`);
2616
+ }
2617
+ return res.json();
2618
+ }
2619
+ };
2620
+
2621
+ // cli/commands/list.ts
2622
+ function formatJson(data) {
2623
+ return JSON.stringify(data, null, 2);
2624
+ }
2625
+ function displayStorageWarnings(meta) {
2626
+ if (!meta.storageConfigured) {
2627
+ console.log(chalk.yellow("\n \u26A0 Storage not configured"));
2628
+ console.log(chalk.gray(" Showing sample data only. Set OPENSEARCH_STORAGE_* env vars for persistent storage.\n"));
2629
+ } else if (!meta.storageReachable) {
2630
+ console.log(chalk.yellow("\n \u26A0 Storage unreachable"));
2631
+ meta.warnings?.forEach((w) => console.log(chalk.gray(` - ${w}`)));
2632
+ console.log();
2633
+ }
2634
+ }
2635
+ async function listAgents(format, config) {
2636
+ const serverResult = await ensureServer(config.server);
2637
+ const cleanup = createServerCleanup(serverResult, config.server.reuseExistingServer === false);
2638
+ try {
2639
+ const client = new ApiClient(serverResult.baseUrl);
2640
+ const agents = await client.listAgents();
2641
+ if (format === "json") {
2642
+ console.log(formatJson(agents));
2643
+ return;
2644
+ }
2645
+ const table = new Table({
2646
+ head: [
2647
+ chalk.cyan("Key"),
2648
+ chalk.cyan("Name"),
2649
+ chalk.cyan("Connector"),
2650
+ chalk.cyan("Models"),
2651
+ chalk.cyan("Endpoint")
2652
+ ],
2653
+ colWidths: [15, 20, 15, 25, 40],
2654
+ wordWrap: true
2655
+ });
2656
+ for (const agent of agents) {
2657
+ table.push([
2658
+ agent.key,
2659
+ agent.name,
2660
+ agent.connectorType || "agui-streaming",
2661
+ agent.models.slice(0, 3).join(", ") + (agent.models.length > 3 ? "..." : ""),
2662
+ agent.endpoint.substring(0, 37) + (agent.endpoint.length > 37 ? "..." : "")
2663
+ ]);
2664
+ }
2665
+ console.log(chalk.bold("\nAvailable Agents:\n"));
2666
+ console.log(table.toString());
2667
+ console.log(chalk.gray(`
2668
+ Total: ${agents.length} agents
2669
+ `));
2670
+ } catch (error) {
2671
+ console.error(chalk.red(`
2672
+ Error: ${error.message}`));
2673
+ console.log(chalk.gray(" Is the server running? Start with: npm run dev:server\n"));
2674
+ process.exit(1);
2675
+ } finally {
2676
+ cleanup();
2677
+ }
2678
+ }
2679
+ async function listTestCases(format, config) {
2680
+ const serverResult = await ensureServer(config.server);
2681
+ const cleanup = createServerCleanup(serverResult, config.server.reuseExistingServer === false);
2682
+ try {
2683
+ const client = new ApiClient(serverResult.baseUrl);
2684
+ const response = await client.listTestCasesWithMeta();
2685
+ displayStorageWarnings(response.meta);
2686
+ if (format === "json") {
2687
+ console.log(formatJson(response));
2688
+ return;
2689
+ }
2690
+ const table = new Table({
2691
+ head: [
2692
+ chalk.cyan("ID"),
2693
+ chalk.cyan("Name"),
2694
+ chalk.cyan("Labels"),
2695
+ chalk.cyan("Version"),
2696
+ chalk.cyan("Source")
2697
+ ],
2698
+ colWidths: [25, 28, 28, 10, 10],
2699
+ wordWrap: true
2700
+ });
2701
+ for (const tc of response.data) {
2702
+ const isDemo = tc.id.startsWith("demo-");
2703
+ table.push([
2704
+ tc.id,
2705
+ tc.name,
2706
+ tc.labels?.slice(0, 3).join(", ") || "",
2707
+ `v${tc.currentVersion || 1}`,
2708
+ isDemo ? chalk.gray("Sample") : chalk.green("Stored")
2709
+ ]);
2710
+ }
2711
+ console.log(chalk.bold("\nAvailable Test Cases:\n"));
2712
+ console.log(table.toString());
2713
+ const { meta } = response;
2714
+ if (meta.realDataCount > 0 || meta.sampleDataCount > 0) {
2715
+ console.log(chalk.gray(`
2716
+ Total: ${response.total} test cases (${meta.realDataCount} stored, ${meta.sampleDataCount} sample)
2717
+ `));
2718
+ } else {
2719
+ console.log(chalk.gray(`
2720
+ Total: ${response.total} test cases
2721
+ `));
2722
+ }
2723
+ } catch (error) {
2724
+ console.error(chalk.red(`
2725
+ Error: ${error.message}`));
2726
+ console.log(chalk.gray(" Is the server running? Start with: npm run dev:server\n"));
2727
+ process.exit(1);
2728
+ } finally {
2729
+ cleanup();
2730
+ }
2731
+ }
2732
+ async function listBenchmarks(format, config) {
2733
+ const serverResult = await ensureServer(config.server);
2734
+ const cleanup = createServerCleanup(serverResult, config.server.reuseExistingServer === false);
2735
+ try {
2736
+ const client = new ApiClient(serverResult.baseUrl);
2737
+ const response = await client.listBenchmarksWithMeta();
2738
+ displayStorageWarnings(response.meta);
2739
+ if (format === "json") {
2740
+ console.log(formatJson(response));
2741
+ return;
2742
+ }
2743
+ const table = new Table({
2744
+ head: [
2745
+ chalk.cyan("ID"),
2746
+ chalk.cyan("Name"),
2747
+ chalk.cyan("Test Cases"),
2748
+ chalk.cyan("Created"),
2749
+ chalk.cyan("Source")
2750
+ ],
2751
+ colWidths: [28, 28, 12, 22, 10],
2752
+ wordWrap: true
2753
+ });
2754
+ for (const b of response.data) {
2755
+ const isDemo = b.id.startsWith("demo-");
2756
+ table.push([
2757
+ b.id,
2758
+ b.name,
2759
+ b.testCaseIds.length.toString(),
2760
+ new Date(b.createdAt).toLocaleDateString(),
2761
+ isDemo ? chalk.gray("Sample") : chalk.green("Stored")
2762
+ ]);
2763
+ }
2764
+ console.log(chalk.bold("\nAvailable Benchmarks:\n"));
2765
+ console.log(table.toString());
2766
+ const { meta } = response;
2767
+ if (meta.realDataCount > 0 || meta.sampleDataCount > 0) {
2768
+ console.log(chalk.gray(`
2769
+ Total: ${response.total} benchmarks (${meta.realDataCount} stored, ${meta.sampleDataCount} sample)
2770
+ `));
2771
+ } else {
2772
+ console.log(chalk.gray(`
2773
+ Total: ${response.total} benchmarks
2774
+ `));
2775
+ }
2776
+ } catch (error) {
2777
+ console.error(chalk.red(`
2778
+ Error: ${error.message}`));
2779
+ console.log(chalk.gray(" Is the server running? Start with: npm run dev:server\n"));
2780
+ process.exit(1);
2781
+ } finally {
2782
+ cleanup();
2783
+ }
2784
+ }
2785
+ function listConnectors(format) {
2786
+ const types = connectorRegistry.getRegisteredTypes();
2787
+ const connectors = types.map((type) => {
2788
+ const connector = connectorRegistry.get(type);
2789
+ return {
2790
+ type,
2791
+ name: connector?.name || "Unknown",
2792
+ streaming: connector?.supportsStreaming || false
2793
+ };
2794
+ });
2795
+ if (format === "json") {
2796
+ console.log(formatJson(connectors));
2797
+ return;
2798
+ }
2799
+ const table = new Table({
2800
+ head: [
2801
+ chalk.cyan("Type"),
2802
+ chalk.cyan("Name"),
2803
+ chalk.cyan("Streaming")
2804
+ ],
2805
+ colWidths: [20, 25, 12]
2806
+ });
2807
+ for (const c of connectors) {
2808
+ table.push([
2809
+ c.type,
2810
+ c.name,
2811
+ c.streaming ? chalk.green("Yes") : chalk.gray("No")
2812
+ ]);
2813
+ }
2814
+ console.log(chalk.bold("\nRegistered Connectors:\n"));
2815
+ console.log(table.toString());
2816
+ console.log(chalk.gray(`
2817
+ Total: ${connectors.length} connectors
2818
+ `));
2819
+ }
2820
+ async function listModels(format, config) {
2821
+ const serverResult = await ensureServer(config.server);
2822
+ const cleanup = createServerCleanup(serverResult, config.server.reuseExistingServer === false);
2823
+ try {
2824
+ const client = new ApiClient(serverResult.baseUrl);
2825
+ const models = await client.listModels();
2826
+ if (format === "json") {
2827
+ console.log(formatJson(models));
2828
+ return;
2829
+ }
2830
+ const table = new Table({
2831
+ head: [
2832
+ chalk.cyan("Key"),
2833
+ chalk.cyan("Display Name"),
2834
+ chalk.cyan("Provider"),
2835
+ chalk.cyan("Context")
2836
+ ],
2837
+ colWidths: [25, 30, 12, 12],
2838
+ wordWrap: true
2839
+ });
2840
+ for (const m of models) {
2841
+ table.push([
2842
+ m.key,
2843
+ m.display_name || m.key,
2844
+ m.provider || "bedrock",
2845
+ m.context_window ? `${Math.round(m.context_window / 1e3)}k` : "-"
2846
+ ]);
2847
+ }
2848
+ console.log(chalk.bold("\nAvailable Models:\n"));
2849
+ console.log(table.toString());
2850
+ console.log(chalk.gray(`
2851
+ Total: ${models.length} models
2852
+ `));
2853
+ } catch (error) {
2854
+ console.error(chalk.red(`
2855
+ Error: ${error.message}`));
2856
+ console.log(chalk.gray(" Is the server running? Start with: npm run dev:server\n"));
2857
+ process.exit(1);
2858
+ } finally {
2859
+ cleanup();
2860
+ }
2861
+ }
2862
+ function createListCommand() {
2863
+ const command = new Command("list").description("List available resources").argument("<resource>", "Resource type: agents, test-cases, benchmarks, connectors, models").option("-o, --output <format>", "Output format: table, json", "table").action(async (resource, options) => {
2864
+ const format = options.output;
2865
+ const config = await loadConfig();
2866
+ for (const connector of config.connectors) {
2867
+ connectorRegistry.register(connector);
2868
+ }
2869
+ switch (resource.toLowerCase()) {
2870
+ case "agents":
2871
+ await listAgents(format, config);
2872
+ break;
2873
+ case "test-cases":
2874
+ case "testcases":
2875
+ case "tc":
2876
+ await listTestCases(format, config);
2877
+ break;
2878
+ case "benchmarks":
2879
+ case "bench":
2880
+ await listBenchmarks(format, config);
2881
+ break;
2882
+ case "connectors":
2883
+ listConnectors(format);
2884
+ break;
2885
+ case "models":
2886
+ await listModels(format, config);
2887
+ break;
2888
+ default:
2889
+ console.error(chalk.red(`
2890
+ Unknown resource type: ${resource}`));
2891
+ console.log(chalk.gray(" Available: agents, test-cases, benchmarks, connectors, models\n"));
2892
+ process.exit(1);
2893
+ }
2894
+ });
2895
+ return command;
2896
+ }
2897
+
2898
+ // cli/commands/run.ts
2899
+ import { Command as Command2 } from "commander";
2900
+ import chalk2 from "chalk";
2901
+ import ora from "ora";
2902
+ import Table2 from "cli-table3";
2903
+ function findAgent(identifier, config) {
2904
+ return config.agents.find(
2905
+ (a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
2906
+ );
2907
+ }
2908
+ function getDefaultModel(agent) {
2909
+ return agent.models[0] || "claude-sonnet";
2910
+ }
2911
+ async function commandExists(command) {
2912
+ const { execSync: execSync2 } = await import("child_process");
2913
+ const checkCommand = process.platform === "win32" ? `where ${command}` : `which ${command}`;
2914
+ try {
2915
+ execSync2(checkCommand, { stdio: "ignore" });
2916
+ return true;
2917
+ } catch {
2918
+ return false;
2919
+ }
2920
+ }
2921
+ async function validateAgentRequirements(agent) {
2922
+ if (agent.connectorType === "claude-code") {
2923
+ if (!await commandExists("claude")) {
2924
+ return `Claude CLI not found in PATH. Install Claude Code CLI: https://claude.ai/code`;
2925
+ }
2926
+ }
2927
+ if (agent.connectorType === "subprocess" && agent.endpoint) {
2928
+ const command = agent.endpoint.split(" ")[0];
2929
+ if (!await commandExists(command)) {
2930
+ return `Command '${command}' not found in PATH`;
2931
+ }
2932
+ }
2933
+ return void 0;
2934
+ }
2935
+ async function runForAgent(client, testCaseId, agent, modelId, verbose) {
2936
+ const spinner = ora(`Running ${agent.name}...`).start();
2937
+ try {
2938
+ const report = await client.runEvaluation(
2939
+ testCaseId,
2940
+ agent.key,
2941
+ modelId,
2942
+ (event) => {
2943
+ if (event.type === "step" && verbose) {
2944
+ spinner.text = `${agent.name}: Step ${event.stepIndex + 1} (${event.step.type})`;
2945
+ } else if (event.type === "started") {
2946
+ spinner.text = `${agent.name}: Started evaluation...`;
2947
+ }
2948
+ }
2949
+ );
2950
+ if (report.status === "completed" && report.passFailStatus === "passed") {
2951
+ spinner.succeed(`${agent.name}: ${chalk2.green("PASSED")}`);
2952
+ } else if (report.status === "completed") {
2953
+ spinner.succeed(`${agent.name}: ${chalk2.red("FAILED")}`);
2954
+ } else {
2955
+ spinner.fail(`${agent.name}: ${chalk2.yellow(report.status)}`);
2956
+ }
2957
+ return report;
2958
+ } catch (error) {
2959
+ spinner.fail(`${agent.name}: ${chalk2.red("ERROR")}`);
2960
+ throw error;
2961
+ }
2962
+ }
2963
+ function displayTableResults(results) {
2964
+ const table = new Table2({
2965
+ head: [
2966
+ chalk2.cyan("Agent"),
2967
+ chalk2.cyan("Status"),
2968
+ chalk2.cyan("Accuracy"),
2969
+ chalk2.cyan("Steps"),
2970
+ chalk2.cyan("Report ID")
2971
+ ],
2972
+ colWidths: [20, 12, 12, 10, 30]
2973
+ });
2974
+ for (const r of results) {
2975
+ if (!r.report) {
2976
+ table.push([
2977
+ r.agent.name,
2978
+ chalk2.red("ERROR"),
2979
+ "-",
2980
+ "-",
2981
+ "-"
2982
+ ]);
2983
+ continue;
2984
+ }
2985
+ const statusStr = r.report.passFailStatus === "passed" ? chalk2.green("PASSED") : r.report.passFailStatus === "failed" ? chalk2.red("FAILED") : chalk2.yellow(r.report.status);
2986
+ table.push([
2987
+ r.agent.name,
2988
+ statusStr,
2989
+ r.report.metrics?.accuracy ? `${Math.round(r.report.metrics.accuracy)}%` : "-",
2990
+ r.report.trajectorySteps.toString(),
2991
+ r.report.id?.substring(0, 27) + "..." || "-"
2992
+ ]);
2993
+ }
2994
+ console.log("\n");
2995
+ console.log(table.toString());
2996
+ }
2997
+ function createRunCommand() {
2998
+ const command = new Command2("run").description("Run a test case against agents").requiredOption("-t, --test-case <id>", "Test case ID or name").option("-a, --agent <key>", "Agent key (can be specified multiple times)", (val, arr) => [...arr, val], []).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>", "Output format: table, json", "table").option("-v, --verbose", "Show detailed trajectory output").action(async (options) => {
2999
+ console.log(chalk2.bold("\nAgent Health - Test Case Runner\n"));
3000
+ const config = await loadConfig();
3001
+ for (const connector of config.connectors) {
3002
+ connectorRegistry.register(connector);
3003
+ }
3004
+ const serverResult = await ensureServer(config.server);
3005
+ const cleanup = createServerCleanup(serverResult, config.server.reuseExistingServer === false);
3006
+ try {
3007
+ const client = new ApiClient(serverResult.baseUrl);
3008
+ await client.checkHealth();
3009
+ const testCase = await client.findTestCase(options.testCase);
3010
+ if (!testCase) {
3011
+ console.error(chalk2.red(` Error: Test case not found: ${options.testCase}`));
3012
+ console.log(chalk2.gray(" Use `agent-health list test-cases` to see available test cases\n"));
3013
+ process.exit(1);
3014
+ }
3015
+ console.log(chalk2.gray(` Test Case: ${testCase.name} (${testCase.id})`));
3016
+ console.log(chalk2.gray(` Server: ${serverResult.baseUrl}`));
3017
+ let agents = [];
3018
+ if (options.agent.length === 0) {
3019
+ agents = [config.agents[0]];
3020
+ console.log(chalk2.gray(` Agent: ${agents[0].name} (default)`));
3021
+ } else {
3022
+ for (const agentId of options.agent) {
3023
+ const agent = findAgent(agentId, config);
3024
+ if (!agent) {
3025
+ console.error(chalk2.red(` Error: Agent not found: ${agentId}`));
3026
+ console.log(chalk2.gray(" Use `agent-health list agents` to see available agents\n"));
3027
+ process.exit(1);
3028
+ }
3029
+ agents.push(agent);
3030
+ }
3031
+ console.log(chalk2.gray(` Agents: ${agents.map((a) => a.name).join(", ")}`));
3032
+ }
3033
+ console.log("");
3034
+ const results = [];
3035
+ for (const agent of agents) {
3036
+ const modelId = options.model || getDefaultModel(agent);
3037
+ const validationError = await validateAgentRequirements(agent);
3038
+ if (validationError) {
3039
+ console.error(chalk2.red(` Error: ${validationError}`));
3040
+ results.push({ agent, report: null });
3041
+ continue;
3042
+ }
3043
+ try {
3044
+ const report = await runForAgent(client, testCase.id, agent, modelId, options.verbose || false);
3045
+ results.push({ agent, report });
3046
+ } catch (error) {
3047
+ console.error(chalk2.red(` Error running ${agent.name}: ${error instanceof Error ? error.message : error}`));
3048
+ results.push({ agent, report: null });
3049
+ }
3050
+ }
3051
+ if (options.output === "json") {
3052
+ console.log(JSON.stringify(results.map((r) => ({
3053
+ agent: { key: r.agent.key, name: r.agent.name },
3054
+ report: r.report
3055
+ })), null, 2));
3056
+ } else {
3057
+ displayTableResults(results);
3058
+ }
3059
+ } catch (error) {
3060
+ console.error(chalk2.red(`
3061
+ Error: ${error.message}`));
3062
+ console.log(chalk2.gray(" Is the server running? Start with: npm run dev:server\n"));
3063
+ process.exit(1);
3064
+ } finally {
3065
+ cleanup();
3066
+ }
3067
+ });
3068
+ return command;
3069
+ }
3070
+
3071
+ // cli/commands/benchmark.ts
3072
+ import { Command as Command3 } from "commander";
3073
+ import chalk3 from "chalk";
3074
+ import ora2 from "ora";
3075
+ import Table3 from "cli-table3";
3076
+ import { readFileSync as readFileSync2, writeFileSync } from "fs";
3077
+
3078
+ // lib/testCaseValidation.ts
3079
+ import { z } from "zod";
3080
+ var contextItemSchema = z.object({
3081
+ description: z.string(),
3082
+ value: z.string()
3083
+ }).required();
3084
+ var difficultySchema = z.enum(["Easy", "Medium", "Hard"]);
3085
+ var testCaseSchema = z.object({
3086
+ name: z.string().min(1, "Name is required"),
3087
+ description: z.string().optional().default(""),
3088
+ category: z.string().min(1, "Category is required"),
3089
+ subcategory: z.string().optional(),
3090
+ difficulty: difficultySchema,
3091
+ initialPrompt: z.string().min(1, "Initial prompt is required"),
3092
+ context: z.array(contextItemSchema).optional().default([]),
3093
+ expectedOutcomes: z.array(z.string()).refine(
3094
+ (outcomes) => outcomes.some((o) => o.trim().length > 0),
3095
+ "At least one non-empty expected outcome is required"
3096
+ )
3097
+ });
3098
+ var testCasesArraySchema = z.array(testCaseSchema).min(1, "Array cannot be empty");
3099
+ function zodErrorToValidationErrors(error) {
3100
+ return error.errors.map((e) => ({
3101
+ path: e.path.join("."),
3102
+ message: e.message,
3103
+ type: "error"
3104
+ }));
3105
+ }
3106
+ function validateTestCaseJson(json) {
3107
+ if (Array.isArray(json)) {
3108
+ return {
3109
+ valid: false,
3110
+ errors: [{ path: "", message: "Received an array. Use Bulk Import mode for multiple test cases", type: "error" }]
3111
+ };
3112
+ }
3113
+ const result = testCaseSchema.safeParse(json);
3114
+ if (!result.success) {
3115
+ return {
3116
+ valid: false,
3117
+ errors: zodErrorToValidationErrors(result.error)
3118
+ };
3119
+ }
3120
+ return { valid: true, errors: [], data: result.data };
3121
+ }
3122
+ function validateTestCasesArrayJson(json) {
3123
+ if (json && typeof json === "object" && !Array.isArray(json)) {
3124
+ const singleResult = validateTestCaseJson(json);
3125
+ if (singleResult.valid && singleResult.data) {
3126
+ return {
3127
+ valid: true,
3128
+ errors: [],
3129
+ data: [singleResult.data]
3130
+ };
3131
+ }
3132
+ return { valid: false, errors: singleResult.errors };
3133
+ }
3134
+ const result = testCasesArraySchema.safeParse(json);
3135
+ if (!result.success) {
3136
+ return {
3137
+ valid: false,
3138
+ errors: zodErrorToValidationErrors(result.error)
3139
+ };
3140
+ }
3141
+ return { valid: true, errors: [], data: result.data };
3142
+ }
3143
+
3144
+ // lib/runStats.ts
3145
+ function calculateRunStats(run, reports) {
3146
+ let passed = 0;
3147
+ let failed = 0;
3148
+ let pending = 0;
3149
+ let total = 0;
3150
+ Object.entries(run.results || {}).forEach(([testCaseId, result]) => {
3151
+ total++;
3152
+ if (result.status === "pending" || result.status === "running") {
3153
+ pending++;
3154
+ return;
3155
+ }
3156
+ if (result.status === "failed" || result.status === "cancelled") {
3157
+ failed++;
3158
+ return;
3159
+ }
3160
+ if (result.status === "completed" && result.reportId) {
3161
+ const report = reports[result.reportId];
3162
+ if (!report) {
3163
+ pending++;
3164
+ return;
3165
+ }
3166
+ if (report.metricsStatus === "pending" || report.metricsStatus === "calculating") {
3167
+ pending++;
3168
+ return;
3169
+ }
3170
+ if (report.passFailStatus === "passed") {
3171
+ passed++;
3172
+ } else {
3173
+ failed++;
3174
+ }
3175
+ } else {
3176
+ pending++;
3177
+ }
3178
+ });
3179
+ const passRate = total > 0 ? Math.round(passed / total * 100) : 0;
3180
+ return {
3181
+ passed,
3182
+ failed,
3183
+ pending,
3184
+ total,
3185
+ passRate
3186
+ };
3187
+ }
3188
+ function getReportIdsFromRun(run) {
3189
+ const reportIds = /* @__PURE__ */ new Set();
3190
+ Object.values(run.results || {}).forEach((result) => {
3191
+ if (result.reportId) {
3192
+ reportIds.add(result.reportId);
3193
+ }
3194
+ });
3195
+ return Array.from(reportIds);
3196
+ }
3197
+
3198
+ // cli/commands/benchmark.ts
3199
+ function findAgent2(identifier, config) {
3200
+ return config.agents.find(
3201
+ (a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
3202
+ );
3203
+ }
3204
+ function getDefaultModel2(agent) {
3205
+ return agent.models[0] || "claude-sonnet";
3206
+ }
3207
+ function isFilePath(value) {
3208
+ return value.toLowerCase().endsWith(".json");
3209
+ }
3210
+ function loadAndValidateTestCasesFile(filePath) {
3211
+ let raw;
3212
+ try {
3213
+ raw = readFileSync2(filePath, "utf-8");
3214
+ } catch (err) {
3215
+ throw new Error(`Cannot read file: ${filePath} (${err instanceof Error ? err.message : err})`);
3216
+ }
3217
+ let parsed;
3218
+ try {
3219
+ parsed = JSON.parse(raw);
3220
+ } catch {
3221
+ throw new Error(`Invalid JSON in file: ${filePath}`);
3222
+ }
3223
+ const result = validateTestCasesArrayJson(parsed);
3224
+ if (!result.valid || !result.data) {
3225
+ const msgs = result.errors.map((e) => e.path ? `${e.path}: ${e.message}` : e.message).join("\n ");
3226
+ throw new Error(`Validation failed for ${filePath}:
3227
+ ${msgs}`);
3228
+ }
3229
+ return result.data;
3230
+ }
3231
+ async function fetchReportsForRun(api, run) {
3232
+ const reportIds = getReportIdsFromRun(run);
3233
+ const reportsMap = {};
3234
+ await Promise.all(
3235
+ reportIds.map(async (reportId) => {
3236
+ reportsMap[reportId] = await api.getReportById(reportId);
3237
+ })
3238
+ );
3239
+ return reportsMap;
3240
+ }
3241
+ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3242
+ const results = {
3243
+ agent,
3244
+ passed: 0,
3245
+ failed: 0
3246
+ };
3247
+ const totalTestCases = benchmark.testCaseIds.length;
3248
+ const spinner = ora2(`Running ${agent.name} (0/${totalTestCases})`).start();
3249
+ let startedRunId;
3250
+ try {
3251
+ const completedRun = await api.executeBenchmark(
3252
+ benchmark.id,
3253
+ {
3254
+ name: `CLI Run - ${agent.name}`,
3255
+ agentKey: agent.key,
3256
+ modelId
3257
+ },
3258
+ (event) => {
3259
+ if (event.type === "started") {
3260
+ startedRunId = event.runId;
3261
+ } else if (event.type === "progress") {
3262
+ const current = event.currentTestCaseIndex + 1;
3263
+ const testCaseName = event.currentTestCase?.name || `Test ${current}`;
3264
+ spinner.text = `${agent.name}: ${testCaseName} (${current}/${totalTestCases})`;
3265
+ if (verbose && event.result) {
3266
+ const status = event.result.status === "completed" ? chalk3.green("\u2713") : chalk3.red("\u2717");
3267
+ spinner.text = `${agent.name}: ${testCaseName} ${status} (${current}/${totalTestCases})`;
3268
+ }
3269
+ }
3270
+ }
3271
+ );
3272
+ results.run = completedRun;
3273
+ const reportsMap = await fetchReportsForRun(api, completedRun);
3274
+ const stats = calculateRunStats(completedRun, reportsMap);
3275
+ results.passed = stats.passed;
3276
+ results.failed = stats.failed;
3277
+ results.reports = Object.values(reportsMap).filter((r) => r !== null);
3278
+ const passRate = stats.passRate;
3279
+ if (passRate >= 80) {
3280
+ spinner.succeed(
3281
+ `${agent.name}: ${chalk3.green(`${stats.passed}/${stats.total} passed`)} (${passRate}% pass rate)`
3282
+ );
3283
+ } else if (passRate >= 50) {
3284
+ spinner.warn(
3285
+ `${agent.name}: ${chalk3.yellow(`${stats.passed}/${stats.total} passed`)} (${passRate}% pass rate)`
3286
+ );
3287
+ } else {
3288
+ spinner.fail(
3289
+ `${agent.name}: ${chalk3.red(`${stats.passed}/${stats.total} passed`)} (${passRate}% pass rate)`
3290
+ );
3291
+ }
3292
+ } catch (error) {
3293
+ const errorMessage = error instanceof Error ? error.message : String(error);
3294
+ if (startedRunId) {
3295
+ results.runId = startedRunId;
3296
+ try {
3297
+ const run = await api.getRun(benchmark.id, startedRunId);
3298
+ if (run) {
3299
+ results.run = run;
3300
+ const reportsMap = await fetchReportsForRun(api, run);
3301
+ const stats = calculateRunStats(run, reportsMap);
3302
+ results.passed = stats.passed;
3303
+ results.failed = stats.failed;
3304
+ results.reports = Object.values(reportsMap).filter((r) => r !== null);
3305
+ if (run.status === "completed" || run.status === "cancelled") {
3306
+ const passRate = stats.passRate;
3307
+ if (passRate >= 80) {
3308
+ spinner.succeed(
3309
+ `${agent.name}: ${chalk3.green(`${stats.passed}/${stats.total} passed`)} (${passRate}% pass rate)`
3310
+ );
3311
+ } else if (passRate >= 50) {
3312
+ spinner.warn(
3313
+ `${agent.name}: ${chalk3.yellow(`${stats.passed}/${stats.total} passed`)} (${passRate}% pass rate)`
3314
+ );
3315
+ } else {
3316
+ spinner.fail(
3317
+ `${agent.name}: ${chalk3.red(`${stats.passed}/${stats.total} passed`)} (${passRate}% pass rate)`
3318
+ );
3319
+ }
3320
+ return results;
3321
+ }
3322
+ }
3323
+ } catch {
3324
+ }
3325
+ }
3326
+ const isStreamError = errorMessage.includes("terminated") || errorMessage.includes("network") || errorMessage.includes("stream") || errorMessage.includes("aborted");
3327
+ if (isStreamError && startedRunId) {
3328
+ spinner.warn(`${agent.name}: ${chalk3.yellow("Stream disconnected")} - server may still be processing`);
3329
+ console.log(chalk3.gray(` Check status: Use the UI to monitor progress`));
3330
+ } else {
3331
+ spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${errorMessage}`);
3332
+ }
3333
+ }
3334
+ return results;
3335
+ }
3336
+ function displaySummaryTable(allResults, totalTestCases) {
3337
+ const table = new Table3({
3338
+ head: [
3339
+ chalk3.cyan("Agent"),
3340
+ chalk3.cyan("Passed"),
3341
+ chalk3.cyan("Failed"),
3342
+ chalk3.cyan("Pass Rate"),
3343
+ chalk3.cyan("Run ID")
3344
+ ],
3345
+ colWidths: [25, 10, 10, 12, 35]
3346
+ });
3347
+ for (const results of allResults) {
3348
+ const passRate = totalTestCases > 0 ? results.passed / totalTestCases * 100 : 0;
3349
+ const passRateColor = passRate >= 80 ? chalk3.green : passRate >= 50 ? chalk3.yellow : chalk3.red;
3350
+ table.push([
3351
+ results.agent.name,
3352
+ chalk3.green(results.passed.toString()),
3353
+ chalk3.red(results.failed.toString()),
3354
+ passRateColor(`${passRate.toFixed(0)}%`),
3355
+ results.run?.id || results.runId || chalk3.gray("N/A")
3356
+ ]);
3357
+ }
3358
+ console.log("\n");
3359
+ console.log(chalk3.bold("Benchmark Summary"));
3360
+ console.log(table.toString());
3361
+ }
3362
+ function exportResults(benchmark, allResults, exportPath) {
3363
+ const exportData = {
3364
+ benchmark: {
3365
+ id: benchmark.id,
3366
+ name: benchmark.name,
3367
+ testCaseCount: benchmark.testCaseIds.length
3368
+ },
3369
+ runs: allResults.map((r) => ({
3370
+ agent: { key: r.agent.key, name: r.agent.name },
3371
+ runId: r.run?.id || r.runId,
3372
+ status: r.run?.status,
3373
+ passed: r.passed,
3374
+ failed: r.failed,
3375
+ passRate: benchmark.testCaseIds.length > 0 ? r.passed / benchmark.testCaseIds.length * 100 : 0,
3376
+ results: r.run?.results,
3377
+ reports: r.reports
3378
+ })),
3379
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString()
3380
+ };
3381
+ writeFileSync(exportPath, JSON.stringify(exportData, null, 2));
3382
+ console.log(chalk3.green(`
3383
+ Results exported to: ${exportPath}`));
3384
+ }
3385
+ function createBenchmarkCommand() {
3386
+ const command = new Command3("benchmark").description("Run a benchmark against one or more agents").option("-n, --name <name>", "Benchmark name or ID (optional in quick mode)").option("-f, --file <path>", "JSON file of test cases to import and benchmark").option(
3387
+ "-a, --agent <key>",
3388
+ "Agent key (can be specified multiple times)",
3389
+ (val, arr) => [...arr, val],
3390
+ []
3391
+ ).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>", "Output format: table, json", "table").option("--export <path>", "Export results to JSON file").option("-v, --verbose", "Show detailed output").option("--stop-server", "Stop the server after benchmark completes (default: keep running)").action(async (options) => {
3392
+ console.log(chalk3.bold("\nAgent Health - Benchmark Runner\n"));
3393
+ const config = await loadConfig();
3394
+ const serverConfig = { ...DEFAULT_SERVER_CONFIG, ...config.server };
3395
+ const isCI = !!process.env.CI;
3396
+ const serverWasRunning = await isServerRunning(serverConfig.port);
3397
+ const filePath = options.file || (options.name && isFilePath(options.name) ? options.name : void 0);
3398
+ const fileMode = !!filePath;
3399
+ const quickMode = !options.name && !fileMode && !serverWasRunning;
3400
+ if (!options.name && !fileMode && serverWasRunning) {
3401
+ console.error(chalk3.red(" Error: Benchmark name required when server is already running."));
3402
+ console.log("");
3403
+ console.log(chalk3.cyan(" Options:"));
3404
+ console.log(chalk3.gray(' 1. Specify a benchmark: benchmark -n "Name" -a claude-code'));
3405
+ console.log(chalk3.gray(" 2. Import from file: benchmark -f ./test-cases.json -a mock"));
3406
+ console.log(chalk3.gray(" 3. Stop the server and run in quick mode"));
3407
+ console.log(chalk3.gray(" 4. List available: npx agent-health list benchmarks"));
3408
+ console.log("");
3409
+ process.exit(1);
3410
+ }
3411
+ if (fileMode) {
3412
+ console.log(chalk3.cyan(` Running in file mode (importing test cases from ${filePath})`));
3413
+ } else if (quickMode) {
3414
+ console.log(chalk3.cyan(" Running in quick mode (auto-creating benchmark from test cases)"));
3415
+ }
3416
+ const connectSpinner = ora2("Connecting to server...").start();
3417
+ let serverResult;
3418
+ let cleanup;
3419
+ const shouldStopServer = isCI || quickMode || fileMode || options.stopServer;
3420
+ try {
3421
+ serverResult = await ensureServer(serverConfig);
3422
+ cleanup = createServerCleanup(serverResult, shouldStopServer);
3423
+ if (serverResult.wasStarted) {
3424
+ connectSpinner.succeed(`Started server on port ${serverConfig.port}`);
3425
+ } else {
3426
+ connectSpinner.succeed(`Connected to existing server on port ${serverConfig.port}`);
3427
+ }
3428
+ } catch (error) {
3429
+ connectSpinner.fail(
3430
+ `Failed to connect to server: ${error instanceof Error ? error.message : error}`
3431
+ );
3432
+ process.exit(1);
3433
+ }
3434
+ const api = new ApiClient(serverResult.baseUrl);
3435
+ try {
3436
+ let benchmark = null;
3437
+ if (fileMode) {
3438
+ const importSpinner = ora2(`Loading test cases from ${filePath}...`).start();
3439
+ try {
3440
+ const validatedTestCases = loadAndValidateTestCasesFile(filePath);
3441
+ importSpinner.succeed(`Validated ${validatedTestCases.length} test cases from file`);
3442
+ const uploadSpinner = ora2("Importing test cases to server...").start();
3443
+ const bulkResult = await api.bulkCreateTestCases(validatedTestCases);
3444
+ uploadSpinner.succeed(`Imported ${bulkResult.created} test cases`);
3445
+ const benchmarkName = options.file && options.name ? options.name : `file-${Date.now()}`;
3446
+ const createSpinner = ora2("Creating benchmark...").start();
3447
+ benchmark = await api.createBenchmark({
3448
+ name: benchmarkName,
3449
+ description: `Imported from ${filePath}`,
3450
+ testCaseIds: bulkResult.testCases.map((tc) => tc.id)
3451
+ });
3452
+ createSpinner.succeed(`Created benchmark: ${benchmark.name}`);
3453
+ } catch (error) {
3454
+ importSpinner.fail(`File import failed: ${error instanceof Error ? error.message : error}`);
3455
+ process.exit(1);
3456
+ }
3457
+ } else if (quickMode) {
3458
+ const testCasesSpinner = ora2("Fetching test cases...").start();
3459
+ try {
3460
+ const testCases = await api.listTestCases();
3461
+ if (testCases.length === 0) {
3462
+ testCasesSpinner.fail("No test cases found");
3463
+ console.log(chalk3.gray(" Add test cases via the UI or provide a file with -f option."));
3464
+ process.exit(1);
3465
+ }
3466
+ testCasesSpinner.succeed(`Found ${testCases.length} test cases`);
3467
+ const createSpinner = ora2("Creating quick benchmark...").start();
3468
+ benchmark = await api.createBenchmark({
3469
+ name: `quick-${Date.now()}`,
3470
+ description: "Auto-generated benchmark for quick mode",
3471
+ testCaseIds: testCases.map((tc) => tc.id)
3472
+ });
3473
+ createSpinner.succeed(`Created benchmark: ${benchmark.name}`);
3474
+ } catch (error) {
3475
+ testCasesSpinner.fail(`Failed to create benchmark: ${error instanceof Error ? error.message : error}`);
3476
+ process.exit(1);
3477
+ }
3478
+ } else {
3479
+ benchmark = await api.findBenchmark(options.name);
3480
+ if (!benchmark) {
3481
+ console.error(chalk3.red(` Error: Benchmark not found: "${options.name}"`));
3482
+ console.log("");
3483
+ console.log(chalk3.cyan(" The -n/--name option accepts:"));
3484
+ console.log(chalk3.gray(" \u2022 Benchmark ID (e.g., demo-baseline)"));
3485
+ console.log(chalk3.gray(' \u2022 Benchmark name (case-sensitive, e.g., "Baseline")'));
3486
+ console.log("");
3487
+ console.log(chalk3.cyan(" Or import from file:"));
3488
+ console.log(chalk3.gray(" benchmark -f ./test-cases.json -a mock"));
3489
+ console.log("");
3490
+ console.log(chalk3.cyan(" Available benchmarks:"));
3491
+ console.log(chalk3.gray(" npx agent-health list benchmarks"));
3492
+ console.log("");
3493
+ process.exit(1);
3494
+ }
3495
+ if (benchmark.id.startsWith("demo-")) {
3496
+ console.error(chalk3.red(` Error: Cannot execute sample benchmarks.`));
3497
+ console.log(chalk3.gray(" Sample data is read-only with pre-completed runs."));
3498
+ console.log(chalk3.gray(" Create a real benchmark in the UI to run evaluations."));
3499
+ console.log("");
3500
+ process.exit(1);
3501
+ }
3502
+ }
3503
+ console.log(chalk3.gray(` Benchmark: ${benchmark.name} (${benchmark.id})`));
3504
+ console.log(chalk3.gray(` Test Cases: ${benchmark.testCaseIds.length}`));
3505
+ console.log(chalk3.gray(` Server: ${serverResult.baseUrl}`));
3506
+ let agents = [];
3507
+ if (options.agent.length === 0) {
3508
+ const enabledAgent = config.agents.find((a) => a.enabled !== false);
3509
+ if (!enabledAgent) {
3510
+ console.error(chalk3.red(" Error: No enabled agents found in config."));
3511
+ process.exit(1);
3512
+ }
3513
+ agents = [enabledAgent];
3514
+ console.log(chalk3.gray(` Agent: ${agents[0].name} (default)`));
3515
+ } else {
3516
+ for (const agentId of options.agent) {
3517
+ const agent = findAgent2(agentId, config);
3518
+ if (!agent) {
3519
+ console.error(chalk3.red(` Error: Agent not found: ${agentId}`));
3520
+ console.log(chalk3.gray(" Available agents:"));
3521
+ for (const a of config.agents) {
3522
+ console.log(chalk3.gray(` - ${a.name} (${a.key})`));
3523
+ }
3524
+ console.log("");
3525
+ process.exit(1);
3526
+ }
3527
+ agents.push(agent);
3528
+ }
3529
+ console.log(chalk3.gray(` Agents: ${agents.map((a) => a.name).join(", ")}`));
3530
+ }
3531
+ console.log("");
3532
+ const allResults = [];
3533
+ for (const agent of agents) {
3534
+ const modelId = options.model || getDefaultModel2(agent);
3535
+ const results = await runBenchmarkForAgent(
3536
+ api,
3537
+ agent,
3538
+ modelId,
3539
+ benchmark,
3540
+ options.verbose || false
3541
+ );
3542
+ allResults.push(results);
3543
+ }
3544
+ if (options.output === "json") {
3545
+ const jsonOutput = allResults.map((r) => ({
3546
+ agent: { key: r.agent.key, name: r.agent.name },
3547
+ runId: r.run?.id || r.runId,
3548
+ passed: r.passed,
3549
+ failed: r.failed,
3550
+ passRate: benchmark.testCaseIds.length > 0 ? r.passed / benchmark.testCaseIds.length * 100 : 0,
3551
+ results: r.run?.results
3552
+ }));
3553
+ console.log(JSON.stringify(jsonOutput, null, 2));
3554
+ } else {
3555
+ displaySummaryTable(allResults, benchmark.testCaseIds.length);
3556
+ }
3557
+ if (options.export) {
3558
+ exportResults(benchmark, allResults, options.export);
3559
+ }
3560
+ console.log("");
3561
+ console.log(chalk3.cyan("View results:"));
3562
+ for (const result of allResults) {
3563
+ const runId = result.run?.id || result.runId;
3564
+ if (runId) {
3565
+ console.log(chalk3.gray(` ${result.agent.name}: ${serverResult.baseUrl}/benchmarks/${benchmark.id}/runs/${runId}`));
3566
+ }
3567
+ }
3568
+ if (process.env.OPENSEARCH_DASHBOARDS_URL) {
3569
+ console.log(chalk3.gray(` OpenSearch Dashboards: ${process.env.OPENSEARCH_DASHBOARDS_URL}`));
3570
+ }
3571
+ if (serverResult.wasStarted && !shouldStopServer) {
3572
+ console.log("");
3573
+ console.log(chalk3.gray(`Server still running on port ${serverConfig.port}`));
3574
+ console.log(chalk3.gray(` Use --stop-server flag to stop after benchmark`));
3575
+ console.log(chalk3.gray(` Or manually: kill $(lsof -t -i:${serverConfig.port})`));
3576
+ }
3577
+ } finally {
3578
+ cleanup();
3579
+ }
3580
+ });
3581
+ return command;
3582
+ }
3583
+
3584
+ // cli/commands/export.ts
3585
+ import { Command as Command4 } from "commander";
3586
+ import chalk4 from "chalk";
3587
+ import { writeFileSync as writeFileSync2 } from "fs";
3588
+
3589
+ // lib/benchmarkExport.ts
3590
+ function generateExportFilename(benchmarkName) {
3591
+ const sanitized = (benchmarkName || "benchmark-export").trim().toLowerCase().replace(/[^a-z0-9_\-\s]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3592
+ return `${sanitized || "benchmark-export"}.json`;
3593
+ }
3594
+
3595
+ // cli/commands/export.ts
3596
+ function createExportCommand() {
3597
+ const command = new Command4("export").description("Export benchmark test cases as JSON").requiredOption("-b, --benchmark <id-or-name>", "Benchmark ID or name").option("-o, --output <file>", "Output file path (default: <benchmark-name>.json)").option("--stdout", "Write to stdout instead of file").action(async (options) => {
3598
+ const config = await loadConfig();
3599
+ const serverResult = await ensureServer(config.server);
3600
+ const cleanup = createServerCleanup(serverResult, config.server.reuseExistingServer === false);
3601
+ try {
3602
+ const client = new ApiClient(serverResult.baseUrl);
3603
+ const benchmark = await client.findBenchmark(options.benchmark);
3604
+ if (!benchmark) {
3605
+ console.error(chalk4.red(`
3606
+ Error: Benchmark not found: ${options.benchmark}
3607
+ `));
3608
+ process.exit(1);
3609
+ }
3610
+ const exportData = await client.exportBenchmark(benchmark.id);
3611
+ if (options.stdout) {
3612
+ process.stdout.write(JSON.stringify(exportData, null, 2) + "\n");
3613
+ return;
3614
+ }
3615
+ const outputFile = options.output || generateExportFilename(benchmark.name);
3616
+ writeFileSync2(outputFile, JSON.stringify(exportData, null, 2) + "\n", "utf-8");
3617
+ console.log(chalk4.green(`
3618
+ Exported ${exportData.length} test case(s) to ${chalk4.bold(outputFile)}`));
3619
+ console.log(chalk4.gray(` Benchmark: ${benchmark.name} (${benchmark.id})
3620
+ `));
3621
+ } catch (error) {
3622
+ console.error(chalk4.red(`
3623
+ Error: ${error.message}`));
3624
+ console.log(chalk4.gray(" Is the server running? Start with: npm run dev:server\n"));
3625
+ process.exit(1);
3626
+ } finally {
3627
+ cleanup();
3628
+ }
3629
+ });
3630
+ return command;
3631
+ }
3632
+
3633
+ // cli/commands/doctor.ts
3634
+ import { Command as Command5 } from "commander";
3635
+ import chalk5 from "chalk";
3636
+ import { existsSync as existsSync3 } from "fs";
3637
+ import { resolve as resolve2 } from "path";
3638
+ function checkConfigFile() {
3639
+ const configInfo = getConfigFileInfo();
3640
+ if (configInfo) {
3641
+ return {
3642
+ name: "Config File",
3643
+ status: "ok",
3644
+ message: `Found: ${configInfo.path.split("/").pop()}`,
3645
+ details: [`Format: ${configInfo.format}`]
3646
+ };
3647
+ }
3648
+ return {
3649
+ name: "Config File",
3650
+ status: "ok",
3651
+ message: "Using defaults + environment variables",
3652
+ details: [
3653
+ "Config file is optional (for custom agents)",
3654
+ "Run `agent-health init` to create one if needed"
3655
+ ]
3656
+ };
3657
+ }
3658
+ function checkEnvFile() {
3659
+ const envPath = resolve2(process.cwd(), ".env");
3660
+ if (existsSync3(envPath)) {
3661
+ return {
3662
+ name: "Environment File",
3663
+ status: "ok",
3664
+ message: "Found: .env"
3665
+ };
3666
+ }
3667
+ return {
3668
+ name: "Environment File",
3669
+ status: "ok",
3670
+ message: "Not using .env file",
3671
+ details: ["Env vars can be set in shell, CI/CD, or via --env-file"]
3672
+ };
3673
+ }
3674
+ function checkAWSCredentials() {
3675
+ const profile = process.env.AWS_PROFILE;
3676
+ const accessKey = process.env.AWS_ACCESS_KEY_ID;
3677
+ const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
3678
+ const details = [];
3679
+ if (profile) {
3680
+ details.push(`AWS_PROFILE: ${profile}`);
3681
+ }
3682
+ if (accessKey) {
3683
+ details.push(`AWS_ACCESS_KEY_ID: ${accessKey.substring(0, 8)}...`);
3684
+ }
3685
+ if (region) {
3686
+ details.push(`AWS_REGION: ${region}`);
3687
+ }
3688
+ if (profile || accessKey) {
3689
+ return {
3690
+ name: "AWS Credentials",
3691
+ status: "ok",
3692
+ message: profile ? `Profile: ${profile}` : "Using access key",
3693
+ details: details.length > 0 ? details : void 0
3694
+ };
3695
+ }
3696
+ return {
3697
+ name: "AWS Credentials",
3698
+ status: "warning",
3699
+ message: "No AWS credentials detected",
3700
+ details: [
3701
+ "Set AWS_PROFILE or AWS_ACCESS_KEY_ID for Bedrock judge",
3702
+ "Claude Code connector also requires AWS credentials"
3703
+ ]
3704
+ };
3705
+ }
3706
+ async function checkClaudeCodeCLI() {
3707
+ const { execSync: execSync2 } = await import("child_process");
3708
+ try {
3709
+ execSync2("which claude", { stdio: "pipe" });
3710
+ return {
3711
+ name: "Claude Code CLI",
3712
+ status: "ok",
3713
+ message: "claude command available"
3714
+ };
3715
+ } catch {
3716
+ return {
3717
+ name: "Claude Code CLI",
3718
+ status: "warning",
3719
+ message: "claude command not found",
3720
+ details: [
3721
+ "Install Claude Code: npm install -g @anthropic/claude-code",
3722
+ "Or skip if not using claude-code connector"
3723
+ ]
3724
+ };
3725
+ }
3726
+ }
3727
+ function checkAgents(config) {
3728
+ const agents = config.agents;
3729
+ const details = [];
3730
+ for (const agent of agents) {
3731
+ const connectorType = agent.connectorType || "agui-streaming";
3732
+ const hasConnector = connectorRegistry.get(connectorType);
3733
+ const status = hasConnector ? "\u2713" : "\u2717";
3734
+ details.push(`${status} ${agent.name} (${connectorType})`);
3735
+ }
3736
+ return {
3737
+ name: "Agents",
3738
+ status: "ok",
3739
+ message: `${agents.length} agents configured`,
3740
+ details
3741
+ };
3742
+ }
3743
+ function checkConnectors() {
3744
+ const types = connectorRegistry.getRegisteredTypes();
3745
+ return {
3746
+ name: "Connectors",
3747
+ status: "ok",
3748
+ message: `${types.length} connectors registered`,
3749
+ details: types.map((t) => `\u2713 ${t}`)
3750
+ };
3751
+ }
3752
+ function checkOpenSearchStorage() {
3753
+ const endpoint = process.env.OPENSEARCH_STORAGE_ENDPOINT;
3754
+ const user = process.env.OPENSEARCH_STORAGE_USERNAME;
3755
+ if (endpoint) {
3756
+ return {
3757
+ name: "OpenSearch Storage",
3758
+ status: "ok",
3759
+ message: `Configured: ${endpoint.substring(0, 50)}...`,
3760
+ details: user ? [`Username: ${user}`] : void 0
3761
+ };
3762
+ }
3763
+ return {
3764
+ name: "OpenSearch Storage",
3765
+ status: "warning",
3766
+ message: "Not configured (results won't persist)",
3767
+ details: [
3768
+ "Set OPENSEARCH_STORAGE_ENDPOINT, _USERNAME, _PASSWORD to save results",
3769
+ "Without storage, results are shown in terminal only"
3770
+ ]
3771
+ };
3772
+ }
3773
+ function checkOpenSearchObservability() {
3774
+ const endpoint = process.env.OPENSEARCH_LOGS_ENDPOINT;
3775
+ const user = process.env.OPENSEARCH_LOGS_USERNAME;
3776
+ if (endpoint) {
3777
+ return {
3778
+ name: "OpenSearch Observability",
3779
+ status: "ok",
3780
+ message: `Configured: ${endpoint.substring(0, 50)}...`,
3781
+ details: user ? [`Username: ${user}`] : void 0
3782
+ };
3783
+ }
3784
+ return {
3785
+ name: "OpenSearch Observability",
3786
+ status: "ok",
3787
+ message: "Not configured (optional)",
3788
+ details: [
3789
+ "Set OPENSEARCH_LOGS_ENDPOINT for agent traces",
3790
+ "Only needed for ML-Commons agent observability"
3791
+ ]
3792
+ };
3793
+ }
3794
+ function displayResults(results) {
3795
+ console.log(chalk5.bold("\n Configuration Check\n"));
3796
+ for (const result of results) {
3797
+ const icon = {
3798
+ ok: chalk5.green("\u2713"),
3799
+ warning: chalk5.yellow("\u26A0"),
3800
+ error: chalk5.red("\u2717")
3801
+ }[result.status];
3802
+ const messageColor = {
3803
+ ok: chalk5.green,
3804
+ warning: chalk5.yellow,
3805
+ error: chalk5.red
3806
+ }[result.status];
3807
+ console.log(` ${icon} ${chalk5.bold(result.name)}: ${messageColor(result.message)}`);
3808
+ if (result.details) {
3809
+ for (const detail of result.details) {
3810
+ console.log(chalk5.gray(` ${detail}`));
3811
+ }
3812
+ }
3813
+ }
3814
+ console.log("");
3815
+ const errors = results.filter((r) => r.status === "error").length;
3816
+ const warnings = results.filter((r) => r.status === "warning").length;
3817
+ if (errors > 0) {
3818
+ console.log(chalk5.red(` ${errors} error(s) found. Fix these before running evaluations.
3819
+ `));
3820
+ } else if (warnings > 0) {
3821
+ console.log(chalk5.yellow(` ${warnings} warning(s). Some features may be limited.
3822
+ `));
3823
+ } else {
3824
+ console.log(chalk5.green(" All checks passed!\n"));
3825
+ }
3826
+ }
3827
+ function createDoctorCommand() {
3828
+ const command = new Command5("doctor").description("Check configuration and system requirements").option("-o, --output <format>", "Output format: text, json", "text").action(async (options) => {
3829
+ const results = [];
3830
+ const config = await loadConfig();
3831
+ for (const connector of config.connectors) {
3832
+ connectorRegistry.register(connector);
3833
+ }
3834
+ results.push(checkConfigFile());
3835
+ results.push(checkEnvFile());
3836
+ results.push(checkAWSCredentials());
3837
+ results.push(await checkClaudeCodeCLI());
3838
+ results.push(checkAgents(config));
3839
+ results.push(checkConnectors());
3840
+ results.push(checkOpenSearchStorage());
3841
+ results.push(checkOpenSearchObservability());
3842
+ if (options.output === "json") {
3843
+ console.log(JSON.stringify(results, null, 2));
3844
+ } else {
3845
+ displayResults(results);
3846
+ }
3847
+ });
3848
+ return command;
3849
+ }
3850
+
3851
+ // cli/commands/init.ts
3852
+ import { Command as Command6 } from "commander";
3853
+ import chalk6 from "chalk";
3854
+ import { writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
3855
+ import { resolve as resolve3 } from "path";
3856
+ var TYPESCRIPT_CONFIG = `/*
3857
+ * Agent Health Configuration
3858
+ * See docs/CONFIGURATION.md for full options
3859
+ */
3860
+
3861
+ import { defineConfig, AGUIStreamingConnector, ClaudeCodeConnector } from '@opensearch-project/agent-health';
3862
+
3863
+ export default defineConfig({
3864
+ agents: [
3865
+ // ML-Commons agent (AG-UI streaming)
3866
+ {
3867
+ name: 'ml-commons',
3868
+ key: 'ml-commons',
3869
+ connector: new AGUIStreamingConnector(),
3870
+ endpoint: process.env.MLCOMMONS_ENDPOINT || 'https://localhost:9200/_plugins/_ml/agents/YOUR_AGENT_ID/_execute',
3871
+ auth: {
3872
+ type: 'basic',
3873
+ username: process.env.OPENSEARCH_USER || 'admin',
3874
+ password: process.env.OPENSEARCH_PASS || 'admin',
3875
+ },
3876
+ models: ['claude-sonnet'],
3877
+ },
3878
+
3879
+ // Claude Code CLI agent (optional)
3880
+ // Uncomment to enable Claude Code comparison
3881
+ /*
3882
+ {
3883
+ name: 'claude-code',
3884
+ key: 'claude-code',
3885
+ connector: new ClaudeCodeConnector({
3886
+ env: {
3887
+ AWS_PROFILE: process.env.AWS_PROFILE || 'Bedrock',
3888
+ CLAUDE_CODE_USE_BEDROCK: '1',
3889
+ AWS_REGION: process.env.AWS_REGION || 'us-west-2',
3890
+ },
3891
+ }),
3892
+ endpoint: 'claude', // Command name
3893
+ models: ['claude-sonnet-4'],
3894
+ },
3895
+ */
3896
+ ],
3897
+
3898
+ // Test cases can be inline or loaded from files
3899
+ testCases: './test-cases/*.yaml',
3900
+
3901
+ // Output reporters
3902
+ reporters: [
3903
+ ['console'],
3904
+ ['json', { output: 'report.json' }],
3905
+ ],
3906
+
3907
+ // Judge configuration
3908
+ judge: {
3909
+ provider: 'bedrock',
3910
+ model: 'claude-sonnet',
3911
+ region: process.env.AWS_REGION || 'us-west-2',
3912
+ },
3913
+ });
3914
+ `;
3915
+ var ENV_TEMPLATE = `# Agent Health Environment Configuration
3916
+ # Copy this to .env and fill in your values
3917
+
3918
+ # ============ AWS Configuration ============
3919
+ # For Bedrock judge and Claude Code CLI
3920
+ AWS_PROFILE=Bedrock
3921
+ AWS_REGION=us-west-2
3922
+ # Or use explicit credentials:
3923
+ # AWS_ACCESS_KEY_ID=
3924
+ # AWS_SECRET_ACCESS_KEY=
3925
+ # AWS_SESSION_TOKEN=
3926
+
3927
+ # ============ OpenSearch/ML-Commons ============
3928
+ # Agent endpoint
3929
+ MLCOMMONS_ENDPOINT=https://localhost:9200/_plugins/_ml/agents/YOUR_AGENT_ID/_execute
3930
+
3931
+ # Storage cluster (for test cases, benchmarks persistence)
3932
+ OPENSEARCH_STORAGE_URL=https://localhost:9200
3933
+ OPENSEARCH_STORAGE_USER=admin
3934
+ OPENSEARCH_STORAGE_PASS=admin
3935
+
3936
+ # Optional: Headers for ML-Commons agent data source access
3937
+ # MLCOMMONS_HEADER_OPENSEARCH_URL=
3938
+ # MLCOMMONS_HEADER_AUTHORIZATION=
3939
+
3940
+ # ============ Server Configuration ============
3941
+ # Backend port (default: 4001)
3942
+ # BACKEND_PORT=4001
3943
+ `;
3944
+ var SAMPLE_TEST_CASE = `# Sample Test Case
3945
+ # Place in test-cases/ directory
3946
+
3947
+ id: sample-rca-001
3948
+ name: Sample RCA Test Case
3949
+ version: 1
3950
+
3951
+ labels:
3952
+ - category:RCA
3953
+ - difficulty:Medium
3954
+
3955
+ initialPrompt: |
3956
+ A customer reports that their web application is experiencing
3957
+ slow response times. The issue started approximately 2 hours ago.
3958
+ Please investigate and identify the root cause.
3959
+
3960
+ context:
3961
+ - description: Application logs
3962
+ value: |
3963
+ 2024-01-15 10:00:00 ERROR: Connection timeout to database
3964
+ 2024-01-15 10:00:05 WARN: Retry attempt 1 for DB connection
3965
+ 2024-01-15 10:00:10 ERROR: Connection timeout to database
3966
+
3967
+ expectedOutcomes:
3968
+ - The agent should identify database connectivity issues
3969
+ - The agent should check database server health
3970
+ - The agent should suggest investigating network connectivity
3971
+ `;
3972
+ function createInitCommand() {
3973
+ const command = new Command6("init").description("Initialize configuration files").option("--force", "Overwrite existing files").option("--with-examples", "Include example test case").action(async (options) => {
3974
+ console.log(chalk6.bold("\n Agent Health - Initialize Configuration\n"));
3975
+ const cwd = process.cwd();
3976
+ const files = [];
3977
+ files.push({
3978
+ path: resolve3(cwd, "agent-health.config.ts"),
3979
+ content: TYPESCRIPT_CONFIG,
3980
+ name: "agent-health.config.ts"
3981
+ });
3982
+ files.push({
3983
+ path: resolve3(cwd, ".env.example"),
3984
+ content: ENV_TEMPLATE,
3985
+ name: ".env.example"
3986
+ });
3987
+ if (options.withExamples) {
3988
+ const { mkdirSync } = await import("fs");
3989
+ const testCasesDir = resolve3(cwd, "test-cases");
3990
+ if (!existsSync4(testCasesDir)) {
3991
+ mkdirSync(testCasesDir, { recursive: true });
3992
+ }
3993
+ files.push({
3994
+ path: resolve3(testCasesDir, "sample-rca.yaml"),
3995
+ content: SAMPLE_TEST_CASE,
3996
+ name: "test-cases/sample-rca.yaml"
3997
+ });
3998
+ }
3999
+ let created = 0;
4000
+ let skipped = 0;
4001
+ for (const file of files) {
4002
+ if (existsSync4(file.path) && !options.force) {
4003
+ console.log(chalk6.yellow(` \u26A0 Skipped: ${file.name} (already exists, use --force to overwrite)`));
4004
+ skipped++;
4005
+ } else {
4006
+ writeFileSync3(file.path, file.content);
4007
+ console.log(chalk6.green(` \u2713 Created: ${file.name}`));
4008
+ created++;
4009
+ }
4010
+ }
4011
+ console.log("");
4012
+ if (created > 0) {
4013
+ console.log(chalk6.gray(" Next steps:"));
4014
+ console.log(chalk6.gray(" 1. Copy .env.example to .env and fill in your values"));
4015
+ console.log(chalk6.gray(" 2. Update the config file with your agent endpoint"));
4016
+ console.log(chalk6.gray(" 3. Run `agent-health doctor` to verify configuration"));
4017
+ console.log(chalk6.gray(" 4. Run `agent-health run -t sample-rca-001` to test\n"));
4018
+ }
4019
+ if (skipped > 0) {
4020
+ console.log(chalk6.yellow(` ${skipped} file(s) skipped. Use --force to overwrite.
4021
+ `));
4022
+ }
4023
+ });
4024
+ return command;
4025
+ }
4026
+
4027
+ // cli/commands/migrate.ts
4028
+ import { Command as Command7 } from "commander";
4029
+ import chalk7 from "chalk";
4030
+ import ora3 from "ora";
4031
+ function computeStatsFromReports(run, reports) {
4032
+ const reportsMap = new Map(reports.map((r) => [r.id, r]));
4033
+ let passed = 0;
4034
+ let failed = 0;
4035
+ let pending = 0;
4036
+ const total = Object.keys(run.results || {}).length;
4037
+ Object.values(run.results || {}).forEach((result) => {
4038
+ if (result.status === "pending" || result.status === "running") {
4039
+ pending++;
4040
+ return;
4041
+ }
4042
+ if (result.status === "failed" || result.status === "cancelled") {
4043
+ failed++;
4044
+ return;
4045
+ }
4046
+ if (result.status === "completed" && result.reportId) {
4047
+ const report = reportsMap.get(result.reportId);
4048
+ if (!report) {
4049
+ pending++;
4050
+ return;
4051
+ }
4052
+ if (report.metricsStatus === "pending" || report.metricsStatus === "calculating") {
4053
+ pending++;
4054
+ return;
4055
+ }
4056
+ if (report.passFailStatus === "passed") {
4057
+ passed++;
4058
+ } else {
4059
+ failed++;
4060
+ }
4061
+ } else {
4062
+ pending++;
4063
+ }
4064
+ });
4065
+ return { passed, failed, pending, total };
4066
+ }
4067
+ function createMigrateCommand() {
4068
+ const command = new Command7("migrate").description("One-time migration to add stats to existing benchmark runs").option("--dry-run", "Show what would be migrated without making changes").option("-v, --verbose", "Show detailed progress").action(async (options) => {
4069
+ console.log(chalk7.cyan.bold("\n Benchmark Stats Migration\n"));
4070
+ const config = await loadConfig();
4071
+ const serverResult = await ensureServer(config.server);
4072
+ const cleanup = createServerCleanup(serverResult, config.server.reuseExistingServer === false);
4073
+ try {
4074
+ const client = new ApiClient(serverResult.baseUrl);
4075
+ const spinner = ora3("Fetching benchmarks...").start();
4076
+ const benchmarks = await client.listBenchmarks();
4077
+ spinner.succeed(`Found ${benchmarks.length} benchmarks`);
4078
+ const migratable = benchmarks.filter(
4079
+ (b) => !b.id.startsWith("demo-") && (b.runs?.length ?? 0) > 0
4080
+ );
4081
+ if (migratable.length === 0) {
4082
+ console.log(chalk7.yellow("\n No benchmarks to migrate.\n"));
4083
+ console.log(chalk7.gray(" Only user-created benchmarks with runs can be migrated."));
4084
+ console.log(chalk7.gray(" Sample data (demo-*) already has stats computed.\n"));
4085
+ return;
4086
+ }
4087
+ console.log(chalk7.gray(`
4088
+ Migrating ${migratable.length} benchmarks with runs...
4089
+ `));
4090
+ let totalRuns = 0;
4091
+ let migratedRuns = 0;
4092
+ let skippedRuns = 0;
4093
+ let errors = 0;
4094
+ for (const benchmark of migratable) {
4095
+ const runs = benchmark.runs || [];
4096
+ totalRuns += runs.length;
4097
+ if (options.verbose) {
4098
+ console.log(chalk7.gray(` Processing: ${benchmark.name} (${runs.length} runs)`));
4099
+ }
4100
+ for (const run of runs) {
4101
+ if (run.stats && typeof run.stats.passed === "number") {
4102
+ skippedRuns++;
4103
+ if (options.verbose) {
4104
+ console.log(chalk7.gray(` \u2713 ${run.name} - already has stats`));
4105
+ }
4106
+ continue;
4107
+ }
4108
+ try {
4109
+ const reportsRes = await fetch(
4110
+ `${serverResult.baseUrl}/api/storage/runs/by-benchmark-run/${benchmark.id}/${run.id}`
4111
+ );
4112
+ if (!reportsRes.ok) {
4113
+ throw new Error(`Failed to fetch reports: ${reportsRes.status}`);
4114
+ }
4115
+ const { runs: reports } = await reportsRes.json();
4116
+ const stats = computeStatsFromReports(run, reports || []);
4117
+ if (options.verbose) {
4118
+ console.log(chalk7.gray(
4119
+ ` \u2192 ${run.name}: passed=${stats.passed}, failed=${stats.failed}, pending=${stats.pending}`
4120
+ ));
4121
+ }
4122
+ if (!options.dryRun) {
4123
+ const updateRes = await fetch(
4124
+ `${serverResult.baseUrl}/api/storage/benchmarks/${benchmark.id}/runs/${run.id}/stats`,
4125
+ {
4126
+ method: "PATCH",
4127
+ headers: { "Content-Type": "application/json" },
4128
+ body: JSON.stringify(stats)
4129
+ }
4130
+ );
4131
+ if (!updateRes.ok) {
4132
+ const errorBody = await updateRes.text();
4133
+ throw new Error(`Failed to update stats: ${errorBody}`);
4134
+ }
4135
+ }
4136
+ migratedRuns++;
4137
+ } catch (error) {
4138
+ errors++;
4139
+ const msg = error instanceof Error ? error.message : "Unknown error";
4140
+ if (options.verbose) {
4141
+ console.log(chalk7.red(` \u2717 ${run.name} - ${msg}`));
4142
+ }
4143
+ }
4144
+ }
4145
+ console.log(
4146
+ options.dryRun ? chalk7.blue(` [DRY RUN] ${benchmark.name} - ${runs.length} runs would be processed`) : chalk7.green(` \u2713 ${benchmark.name} - ${runs.length} runs`)
4147
+ );
4148
+ }
4149
+ console.log(chalk7.bold("\n Migration Summary\n"));
4150
+ console.log(chalk7.gray(` Total runs: ${totalRuns}`));
4151
+ console.log(chalk7.green(` Migrated: ${migratedRuns}`));
4152
+ console.log(chalk7.yellow(` Already done: ${skippedRuns}`));
4153
+ if (errors > 0) {
4154
+ console.log(chalk7.red(` Errors: ${errors}`));
4155
+ }
4156
+ if (options.dryRun) {
4157
+ console.log(chalk7.blue("\n This was a dry run. No changes were made."));
4158
+ console.log(chalk7.blue(" Run without --dry-run to apply changes.\n"));
4159
+ } else {
4160
+ console.log(chalk7.green("\n Migration complete!\n"));
4161
+ }
4162
+ } catch (error) {
4163
+ const msg = error instanceof Error ? error.message : "Unknown error";
4164
+ console.error(chalk7.red(`
4165
+ Error: ${msg}
4166
+ `));
4167
+ process.exit(1);
4168
+ } finally {
4169
+ cleanup();
4170
+ }
4171
+ });
4172
+ return command;
4173
+ }
4174
+
4175
+ // cli/index.ts
4176
+ var __filename3 = fileURLToPath3(import.meta.url);
4177
+ var __dirname3 = dirname3(__filename3);
4178
+ var packageJsonPath2 = join4(__dirname3, "..", "..", "package.json");
4179
+ var version = "0.1.0";
4180
+ try {
4181
+ const packageJson = JSON.parse(readFileSync3(packageJsonPath2, "utf-8"));
4182
+ version = packageJson.version;
4183
+ } catch {
4184
+ }
4185
+ function loadEnvFile(envPath) {
4186
+ const absolutePath = resolve4(process.cwd(), envPath);
4187
+ if (!existsSync5(absolutePath)) {
4188
+ console.error(chalk8.red(`
4189
+ Error: Environment file not found: ${absolutePath}
4190
+ `));
4191
+ process.exit(1);
4192
+ }
4193
+ const result = loadDotenv({ path: absolutePath });
4194
+ if (result.error) {
4195
+ console.error(chalk8.red(`
4196
+ Error loading environment file: ${result.error.message}
4197
+ `));
4198
+ process.exit(1);
4199
+ }
4200
+ console.log(chalk8.gray(` Loaded environment from: ${envPath}`));
4201
+ }
4202
+ var defaultEnvPath = resolve4(process.cwd(), ".env");
4203
+ if (existsSync5(defaultEnvPath)) {
4204
+ loadDotenv({ path: defaultEnvPath });
4205
+ }
4206
+ var program = new Command8();
4207
+ program.name("agent-health").description("Agent Health Evaluation Framework - Evaluate and monitor AI agent performance").version(version).enablePositionalOptions().passThroughOptions();
4208
+ program.option("-p, --port <number>", "Server port", "4001").option("-e, --env-file <path>", "Load environment variables from file (e.g., .env)").option("--no-browser", "Do not open browser automatically");
4209
+ program.action(async (options) => {
4210
+ console.log(chalk8.cyan.bold(`
4211
+ Agent Health v${version} - AI Agent Evaluation Framework
4212
+ `));
4213
+ console.log(chalk8.gray(` Working directory: ${process.cwd()}`));
4214
+ console.log(chalk8.gray(` Package directory: ${__dirname3}`));
4215
+ if (options.envFile) {
4216
+ loadEnvFile(options.envFile);
4217
+ } else if (existsSync5(defaultEnvPath)) {
4218
+ console.log(chalk8.gray(" Auto-loaded .env from current directory"));
4219
+ }
4220
+ const port = parseInt(options.port, 10);
4221
+ const spinner = ora4("Starting server...").start();
4222
+ try {
4223
+ await startServer({ port });
4224
+ spinner.succeed("Server started");
4225
+ console.log(chalk8.gray("\n Configuration:"));
4226
+ console.log(chalk8.gray(` Storage: Sample data (configure OpenSearch for persistence)`));
4227
+ console.log(chalk8.gray(` Agent: Select in UI (Demo Agent for mock, real agents require endpoints)`));
4228
+ console.log(chalk8.gray(` Judge: Select in UI (Demo Judge for mock, Bedrock requires AWS creds)
4229
+ `));
4230
+ const url = `http://localhost:${port}`;
4231
+ console.log(chalk8.green(` Server running at ${chalk8.bold(url)}
4232
+ `));
4233
+ if (options.browser !== false) {
4234
+ console.log(chalk8.gray(" Opening browser..."));
4235
+ await open(url);
4236
+ }
4237
+ console.log(chalk8.gray(" Press Ctrl+C to stop\n"));
4238
+ } catch (error) {
4239
+ spinner.fail("Failed to start server");
4240
+ console.error(chalk8.red(`
4241
+ Error: ${error instanceof Error ? error.message : error}
4242
+ `));
4243
+ process.exit(1);
4244
+ }
4245
+ });
4246
+ program.addCommand(createListCommand());
4247
+ program.addCommand(createRunCommand());
4248
+ program.addCommand(createBenchmarkCommand());
4249
+ program.addCommand(createExportCommand());
4250
+ program.addCommand(createDoctorCommand());
4251
+ program.addCommand(createInitCommand());
4252
+ program.addCommand(createMigrateCommand());
4253
+ program.command("serve").description("Start the Agent Health server (same as default action)").option("-p, --port <number>", "Server port", "4001").option("--no-browser", "Do not open browser automatically").action(async (options) => {
4254
+ console.log(chalk8.cyan.bold(`
4255
+ Agent Health v${version} - AI Agent Evaluation Framework
4256
+ `));
4257
+ const port = parseInt(options.port, 10);
4258
+ const spinner = ora4("Starting server...").start();
4259
+ try {
4260
+ await startServer({ port });
4261
+ spinner.succeed("Server started");
4262
+ const url = `http://localhost:${port}`;
4263
+ console.log(chalk8.green(` Server running at ${chalk8.bold(url)}
4264
+ `));
4265
+ if (options.browser !== false) {
4266
+ console.log(chalk8.gray(" Opening browser..."));
4267
+ await open(url);
4268
+ }
4269
+ console.log(chalk8.gray(" Press Ctrl+C to stop\n"));
4270
+ } catch (error) {
4271
+ spinner.fail("Failed to start server");
4272
+ console.error(chalk8.red(`
4273
+ Error: ${error instanceof Error ? error.message : error}
4274
+ `));
4275
+ process.exit(1);
4276
+ }
4277
+ });
4278
+ program.on("command:*", (operands) => {
4279
+ const unknownCommand = operands[0];
4280
+ const availableCommands = program.commands.map((cmd) => cmd.name());
4281
+ console.error(chalk8.red(`
4282
+ Error: Unknown command '${unknownCommand}'`));
4283
+ console.log("");
4284
+ console.log(chalk8.cyan(" Available commands:"));
4285
+ for (const cmd of availableCommands) {
4286
+ console.log(chalk8.gray(` - ${cmd}`));
4287
+ }
4288
+ console.log("");
4289
+ console.log(chalk8.gray(` Run ${chalk8.cyan("agent-health --help")} for usage information.
4290
+ `));
4291
+ process.exitCode = 1;
4292
+ });
4293
+ program.parse();