@opensearch-project/agent-health 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // cli/index.ts
4
- import { Command as Command8 } from "commander";
5
- import chalk8 from "chalk";
4
+ import { Command as Command10 } from "commander";
5
+ import chalk10 from "chalk";
6
6
  import { fileURLToPath as fileURLToPath3 } from "url";
7
7
  import { dirname as dirname3, join as join4, resolve as resolve4 } from "path";
8
8
  import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
9
9
  import { config as loadDotenv } from "dotenv";
10
10
  import open from "open";
11
- import ora4 from "ora";
11
+ import ora5 from "ora";
12
12
 
13
13
  // cli/utils/startServer.ts
14
14
  import { fileURLToPath } from "url";
@@ -49,25 +49,6 @@ import { existsSync as existsSync2 } from "fs";
49
49
  import { resolve } from "path";
50
50
  import { pathToFileURL } from "url";
51
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
52
  // lib/config.ts
72
53
  var isServerSide = typeof window === "undefined";
73
54
  var SERVER_PORT = isServerSide ? process.env?.VITE_BACKEND_PORT || process.env?.PORT || "4001" : "4001";
@@ -103,10 +84,8 @@ var ENV_CONFIG = {
103
84
  openSearchLogsPassword: getEnvVar("OPENSEARCH_LOGS_PASSWORD", ""),
104
85
  openSearchLogsTracesIndex: getEnvVar("OPENSEARCH_LOGS_TRACES_INDEX", "otel-v1-apm-span-*"),
105
86
  openSearchLogsIndex: getEnvVar("OPENSEARCH_LOGS_INDEX", "ml-commons-logs-*"),
106
- // Per-agent endpoints
107
- langgraphEndpoint: getEnvVar("LANGGRAPH_ENDPOINT", "http://localhost:3000"),
87
+ // ML-Commons agent endpoint
108
88
  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
89
  // ML-Commons agent headers
111
90
  mlcommonsHeaderOpenSearchUrl: getEnvVar("MLCOMMONS_HEADER_OPENSEARCH_URL", ""),
112
91
  mlcommonsHeaderAuthorization: getEnvVar("MLCOMMONS_HEADER_AUTHORIZATION", ""),
@@ -115,6 +94,11 @@ var ENV_CONFIG = {
115
94
  mlcommonsHeaderAwsAccessKeyId: getEnvVar("MLCOMMONS_HEADER_AWS_ACCESS_KEY_ID", ""),
116
95
  mlcommonsHeaderAwsSecretAccessKey: getEnvVar("MLCOMMONS_HEADER_AWS_SECRET_ACCESS_KEY", ""),
117
96
  mlcommonsHeaderAwsSessionToken: getEnvVar("MLCOMMONS_HEADER_AWS_SESSION_TOKEN", ""),
97
+ // Travel Planner multi-agent endpoint (OTel Demo in Docker)
98
+ travelPlannerEndpoint: getEnvVar("TRAVEL_PLANNER_ENDPOINT", "http://localhost:3000"),
99
+ // OpenAI-compatible (optional - for OpenAI-compatible judge/agent endpoints)
100
+ openaiCompatibleApiKey: getEnvVar("OPENAI_COMPATIBLE_API_KEY", ""),
101
+ openaiCompatibleEndpoint: getEnvVar("OPENAI_COMPATIBLE_ENDPOINT", "http://localhost:4000/v1/chat/completions"),
118
102
  // Claude Code Telemetry (optional - for OTEL traces from Claude Code)
119
103
  claudeCodeTelemetryEnabled: getEnvVar("CLAUDE_CODE_TELEMETRY_ENABLED", "false") === "true",
120
104
  otelExporterEndpoint: getEnvVar("OTEL_EXPORTER_OTLP_ENDPOINT", ""),
@@ -122,41 +106,47 @@ var ENV_CONFIG = {
122
106
  otelExporterProtocol: getEnvVar("OTEL_EXPORTER_OTLP_PROTOCOL", ""),
123
107
  otelExporterHeaders: getEnvVar("OTEL_EXPORTER_OTLP_HEADERS", "")
124
108
  };
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
109
 
154
110
  // lib/constants.ts
111
+ var CONNECTOR_TYPE_INFO = {
112
+ "agui-streaming": {
113
+ label: "AG-UI Streaming",
114
+ description: "AG-UI protocol over SSE. Use for ML-Commons and AG-UI compatible agents.",
115
+ serverOnly: false
116
+ },
117
+ "rest": {
118
+ label: "REST",
119
+ description: "Standard HTTP POST. Agent receives JSON, returns JSON. No streaming.",
120
+ serverOnly: false
121
+ },
122
+ "openai-compatible": {
123
+ label: "OpenAI Compatible",
124
+ description: "OpenAI chat completions format (POST /v1/chat/completions). Works with LiteLLM, Ollama, vLLM.",
125
+ serverOnly: false
126
+ },
127
+ "subprocess": {
128
+ label: "Subprocess",
129
+ description: "Runs a CLI command as a child process. Server-only \u2014 use the CLI or benchmark runner.",
130
+ serverOnly: true
131
+ },
132
+ "claude-code": {
133
+ label: "Claude Code",
134
+ description: "Invokes the Claude Code CLI. Server-only \u2014 use the CLI or benchmark runner.",
135
+ serverOnly: true
136
+ },
137
+ "mock": {
138
+ label: "Mock",
139
+ description: "Built-in demo agent for testing. No real endpoint needed.",
140
+ serverOnly: false
141
+ }
142
+ };
143
+ var VALID_CONNECTOR_TYPES = Object.keys(CONNECTOR_TYPE_INFO);
144
+ var BROWSER_SAFE_CONNECTORS = Object.entries(CONNECTOR_TYPE_INFO).filter(([, info]) => !info.serverOnly).map(([type]) => type);
155
145
  function getClaudeCodeConnectorEnv() {
156
146
  const env = {
157
- AWS_PROFILE: process.env.AWS_PROFILE || "Bedrock",
147
+ AWS_PROFILE: ENV_CONFIG.awsProfile || "Bedrock",
158
148
  CLAUDE_CODE_USE_BEDROCK: "1",
159
- AWS_REGION: process.env.AWS_REGION || "us-west-2",
149
+ AWS_REGION: ENV_CONFIG.awsRegion || "us-west-2",
160
150
  DISABLE_PROMPT_CACHING: "1",
161
151
  DISABLE_ERROR_REPORTING: "1"
162
152
  };
@@ -183,70 +173,18 @@ var DEFAULT_CONFIG = {
183
173
  endpoint: "mock://demo",
184
174
  description: "Mock agent for testing (simulated responses)",
185
175
  connectorType: "mock",
186
- models: ["demo-model"],
187
176
  headers: {},
188
177
  useTraces: false
189
178
  },
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
179
  {
233
180
  key: "claude-code",
234
181
  name: "Claude Code",
235
182
  endpoint: "claude",
236
- // Command name, not URL
237
183
  description: "Claude Code CLI agent (requires claude command installed)",
238
184
  connectorType: "claude-code",
239
- models: ["claude-sonnet-4"],
240
185
  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
- }
186
+ useTraces: ENV_CONFIG.claudeCodeTelemetryEnabled && !!ENV_CONFIG.otelExporterEndpoint,
187
+ connectorConfig: { env: getClaudeCodeConnectorEnv() }
250
188
  }
251
189
  ],
252
190
  models: {
@@ -257,6 +195,48 @@ var DEFAULT_CONFIG = {
257
195
  context_window: 2e5,
258
196
  max_output_tokens: 4096
259
197
  },
198
+ "claude-opus-4.6": {
199
+ model_id: "us.anthropic.claude-opus-4-6-v1",
200
+ display_name: "Claude Opus 4.6",
201
+ provider: "bedrock",
202
+ context_window: 2e5,
203
+ max_output_tokens: 128e3
204
+ },
205
+ "claude-sonnet-4.6": {
206
+ model_id: "us.anthropic.claude-sonnet-4-6",
207
+ display_name: "Claude Sonnet 4.6",
208
+ provider: "bedrock",
209
+ context_window: 2e5,
210
+ max_output_tokens: 64e3
211
+ },
212
+ "claude-haiku-4.5": {
213
+ model_id: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
214
+ display_name: "Claude Haiku 4.5",
215
+ provider: "bedrock",
216
+ context_window: 2e5,
217
+ max_output_tokens: 64e3
218
+ },
219
+ "claude-opus-4.5": {
220
+ model_id: "us.anthropic.claude-opus-4-5-20251101-v1:0",
221
+ display_name: "Claude Opus 4.5",
222
+ provider: "bedrock",
223
+ context_window: 2e5,
224
+ max_output_tokens: 64e3
225
+ },
226
+ "claude-opus-4.1": {
227
+ model_id: "us.anthropic.claude-opus-4-1-20250805-v1:0",
228
+ display_name: "Claude Opus 4.1",
229
+ provider: "bedrock",
230
+ context_window: 2e5,
231
+ max_output_tokens: 32e3
232
+ },
233
+ "claude-opus-4": {
234
+ model_id: "us.anthropic.claude-opus-4-20250514-v1:0",
235
+ display_name: "Claude Opus 4",
236
+ provider: "bedrock",
237
+ context_window: 2e5,
238
+ max_output_tokens: 32e3
239
+ },
260
240
  "claude-sonnet-4.5": {
261
241
  model_id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
262
242
  display_name: "Claude Sonnet 4.5",
@@ -277,6 +257,27 @@ var DEFAULT_CONFIG = {
277
257
  provider: "bedrock",
278
258
  context_window: 2e5,
279
259
  max_output_tokens: 4096
260
+ },
261
+ "gpt-4o": {
262
+ model_id: "gpt-4o",
263
+ display_name: "GPT-4o",
264
+ provider: "openai-compatible",
265
+ context_window: 128e3,
266
+ max_output_tokens: 4096
267
+ },
268
+ "deepseek-r1:8b": {
269
+ model_id: "deepseek-r1:8b",
270
+ display_name: "DeepSeek R1 8B (Ollama)",
271
+ provider: "openai-compatible",
272
+ context_window: 128e3,
273
+ max_output_tokens: 8192
274
+ },
275
+ "gemma3:12b": {
276
+ model_id: "gemma3:12b",
277
+ display_name: "Gemma 3 12B (Ollama)",
278
+ provider: "openai-compatible",
279
+ context_window: 128e3,
280
+ max_output_tokens: 8192
280
281
  }
281
282
  },
282
283
  defaults: {
@@ -313,7 +314,6 @@ function toAgentConfig(userAgent) {
313
314
  endpoint: userAgent.endpoint,
314
315
  description: userAgent.description,
315
316
  enabled: userAgent.enabled ?? true,
316
- models: userAgent.models,
317
317
  headers: userAgent.headers ?? {},
318
318
  useTraces: userAgent.useTraces ?? false,
319
319
  connectorType: userAgent.connectorType,
@@ -501,6 +501,44 @@ var ConnectorRegistryImpl = class {
501
501
  };
502
502
  var connectorRegistry = new ConnectorRegistryImpl();
503
503
 
504
+ // lib/debug.ts
505
+ import fs from "fs";
506
+ import path from "path";
507
+ var isBrowser = typeof window !== "undefined";
508
+ var CONFIG_FILENAME = "agent-health.config.json";
509
+ var serverDebugEnabled = false;
510
+ if (!isBrowser) {
511
+ try {
512
+ const configPath = path.join(process.cwd(), CONFIG_FILENAME);
513
+ if (fs.existsSync(configPath)) {
514
+ const content = fs.readFileSync(configPath, "utf-8");
515
+ const config = JSON.parse(content) || {};
516
+ serverDebugEnabled = config.debug === true;
517
+ } else if (process.env?.DEBUG === "true") {
518
+ serverDebugEnabled = true;
519
+ }
520
+ } catch (err) {
521
+ if (process.env?.DEBUG === "true") {
522
+ serverDebugEnabled = true;
523
+ }
524
+ }
525
+ }
526
+ function isDebugEnabled() {
527
+ if (isBrowser) {
528
+ try {
529
+ return localStorage.getItem("agenteval_debug") === "true";
530
+ } catch {
531
+ return false;
532
+ }
533
+ }
534
+ return serverDebugEnabled;
535
+ }
536
+ function debug(module, ...args) {
537
+ if (isDebugEnabled()) {
538
+ console.debug(`[${module}]`, ...args);
539
+ }
540
+ }
541
+
504
542
  // services/connectors/base/BaseConnector.ts
505
543
  var BaseConnector = class {
506
544
  /**
@@ -631,9 +669,11 @@ var SSEClient = class {
631
669
  this.abortController = new AbortController();
632
670
  debug("SSE", "Connecting to", url);
633
671
  debug("SSE", "Method:", method);
634
- debug("SSE", "Payload:", JSON.stringify(body, null, 2).substring(0, 500));
672
+ debug("SSE", "Headers:", headers);
673
+ debug("SSE", "Payload:", body ? JSON.stringify(body, null, 2).substring(0, 500) : "none");
674
+ debug("SSE", "Timeout:", idleTimeoutMs, "ms");
635
675
  try {
636
- const response = await fetch(url, {
676
+ const requestConfig = {
637
677
  method,
638
678
  headers: {
639
679
  "Content-Type": "application/json",
@@ -642,17 +682,28 @@ var SSEClient = class {
642
682
  },
643
683
  body: body ? JSON.stringify(body) : void 0,
644
684
  signal: this.abortController.signal
645
- });
685
+ };
686
+ debug("SSE", "Request config:", JSON.stringify(requestConfig, null, 2).substring(0, 500));
687
+ const response = await fetch(url, requestConfig);
688
+ debug("SSE", "Response received:", response.status, response.statusText);
646
689
  if (!response.ok) {
647
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
690
+ let errorBody = "";
691
+ try {
692
+ errorBody = await response.text();
693
+ debug("SSE", "Error response body:", errorBody.substring(0, 500));
694
+ } catch {
695
+ debug("SSE", "Could not read error response body");
696
+ }
697
+ throw new Error(`HTTP ${response.status}: ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`);
648
698
  }
649
699
  if (!response.body) {
650
700
  throw new Error("Response body is null");
651
701
  }
652
- debug("SSE", "Connected, streaming events...");
702
+ console.info("[SSE] Connected to agent endpoint, streaming events...");
653
703
  debug("SSE", "Response status:", response.status);
654
704
  debug("SSE", "Content-Type:", response.headers.get("content-type"));
655
705
  const completionReason = await this.processStream(response.body, onEvent, completeOnRunEnd, idleTimeoutMs);
706
+ console.info(`[SSE] Stream completed: ${completionReason}`);
656
707
  debug("SSE", `Stream completed: ${completionReason}`);
657
708
  onComplete?.();
658
709
  } catch (error) {
@@ -662,9 +713,36 @@ var SSEClient = class {
662
713
  onComplete?.();
663
714
  } else {
664
715
  console.error("[SSE] Stream error:", error.message);
716
+ console.error("[SSE] Debug mode is:", isDebugEnabled() ? "ENABLED \u2705" : "DISABLED \u274C");
717
+ const errorDetails = {
718
+ name: error.name,
719
+ message: error.message,
720
+ stack: error.stack,
721
+ cause: error.cause,
722
+ url,
723
+ method,
724
+ headers
725
+ };
726
+ debug("SSE", "Error details:", errorDetails);
727
+ if (error.message.includes("fetch failed")) {
728
+ debug("SSE", '\u{1F4A1} Diagnostic: "fetch failed" typically means:');
729
+ debug("SSE", " - Connection refused (endpoint not running)");
730
+ debug("SSE", " - DNS resolution failed (invalid hostname)");
731
+ debug("SSE", " - Network unreachable (firewall/VPN issues)");
732
+ debug("SSE", " - SSL/TLS certificate issues (self-signed cert)");
733
+ debug("SSE", ` Check if ${url} is accessible`);
734
+ } else if (error.message.includes("timeout")) {
735
+ debug("SSE", "\u{1F4A1} Diagnostic: Request timed out - endpoint may be slow or unresponsive");
736
+ } else if (error.message.includes("ENOTFOUND")) {
737
+ debug("SSE", "\u{1F4A1} Diagnostic: DNS lookup failed - hostname not found");
738
+ } else if (error.message.includes("ECONNREFUSED")) {
739
+ debug("SSE", "\u{1F4A1} Diagnostic: Connection refused - service not listening on this port");
740
+ }
665
741
  onError?.(error);
666
742
  }
667
743
  } else {
744
+ console.error("[SSE] Unknown error:", error);
745
+ debug("SSE", "Unknown error details:", error);
668
746
  onError?.(new Error("Unknown error occurred"));
669
747
  }
670
748
  }
@@ -708,8 +786,11 @@ var SSEClient = class {
708
786
  debug("SSE", "Raw event:", data.substring(0, 200) + (data.length > 200 ? "..." : ""));
709
787
  try {
710
788
  const event = JSON.parse(data);
711
- debug("SSE", "Parsed:", event.type);
789
+ debug("SSE", "Parsed event:", event.type);
712
790
  eventCount++;
791
+ if (eventCount % 10 === 0) {
792
+ console.info(`[SSE] Processed ${eventCount} events...`);
793
+ }
713
794
  onEvent(event);
714
795
  if (completeOnRunEnd && (event.type === AGUIEventType.RUN_FINISHED || event.type === AGUIEventType.RUN_ERROR)) {
715
796
  debug("SSE", `Received ${event.type}, completing stream`);
@@ -1520,10 +1601,123 @@ var RESTConnector = class extends BaseConnector {
1520
1601
  };
1521
1602
  var restConnector = new RESTConnector();
1522
1603
 
1604
+ // services/connectors/openai-compatible/OpenAICompatibleConnector.ts
1605
+ var OpenAICompatibleConnector = class extends BaseConnector {
1606
+ constructor() {
1607
+ super(...arguments);
1608
+ this.type = "openai-compatible";
1609
+ this.name = "OpenAI-compatible";
1610
+ this.supportsStreaming = false;
1611
+ }
1612
+ /**
1613
+ * Build OpenAI Chat Completion payload from test case
1614
+ */
1615
+ buildPayload(request) {
1616
+ const messages = [];
1617
+ if (request.testCase.context && request.testCase.context.length > 0) {
1618
+ const contextText = request.testCase.context.map((c) => typeof c === "string" ? c : JSON.stringify(c)).join("\n");
1619
+ messages.push({
1620
+ role: "system",
1621
+ content: contextText
1622
+ });
1623
+ }
1624
+ messages.push({
1625
+ role: "user",
1626
+ content: request.testCase.initialPrompt
1627
+ });
1628
+ const payload = {
1629
+ model: request.modelId,
1630
+ messages
1631
+ };
1632
+ if (request.testCase.tools && request.testCase.tools.length > 0) {
1633
+ payload.tools = request.testCase.tools.map((tool) => ({
1634
+ type: "function",
1635
+ function: {
1636
+ name: tool.name,
1637
+ description: tool.description || "",
1638
+ parameters: tool.parameters || {}
1639
+ }
1640
+ }));
1641
+ }
1642
+ return payload;
1643
+ }
1644
+ /**
1645
+ * Execute OpenAI-compatible Chat Completion request
1646
+ */
1647
+ async execute(endpoint, request, auth, onProgress, onRawEvent) {
1648
+ const payload = request.payload || this.buildPayload(request);
1649
+ const headers = this.buildAuthHeaders(auth);
1650
+ this.debug("Executing OpenAI-compatible request");
1651
+ this.debug("Endpoint:", endpoint);
1652
+ this.debug("Model:", payload.model);
1653
+ const response = await fetch(endpoint, {
1654
+ method: "POST",
1655
+ headers: {
1656
+ "Content-Type": "application/json",
1657
+ ...headers
1658
+ },
1659
+ body: JSON.stringify(payload)
1660
+ });
1661
+ if (!response.ok) {
1662
+ const errorText = await response.text();
1663
+ throw new Error(`OpenAI-compatible request failed: ${response.status} - ${errorText}`);
1664
+ }
1665
+ const data = await response.json();
1666
+ onRawEvent?.(data);
1667
+ const trajectory = this.parseResponse(data);
1668
+ trajectory.forEach((step) => onProgress?.(step));
1669
+ return {
1670
+ trajectory,
1671
+ runId: data.id || null,
1672
+ rawEvents: [data],
1673
+ metadata: {
1674
+ model: data.model,
1675
+ usage: data.usage,
1676
+ finishReason: data.choices?.[0]?.finish_reason
1677
+ }
1678
+ };
1679
+ }
1680
+ /**
1681
+ * Parse OpenAI Chat Completion response into trajectory steps
1682
+ */
1683
+ parseResponse(data) {
1684
+ const steps = [];
1685
+ const choice = data.choices?.[0];
1686
+ if (!choice) {
1687
+ steps.push(this.createStep("response", JSON.stringify(data, null, 2)));
1688
+ return steps;
1689
+ }
1690
+ const message = choice.message;
1691
+ if (message.tool_calls && message.tool_calls.length > 0) {
1692
+ for (const toolCall of message.tool_calls) {
1693
+ let toolArgs;
1694
+ try {
1695
+ toolArgs = JSON.parse(toolCall.function.arguments);
1696
+ } catch {
1697
+ toolArgs = toolCall.function.arguments;
1698
+ }
1699
+ steps.push(this.createStep("action", `Calling ${toolCall.function.name}...`, {
1700
+ toolName: toolCall.function.name,
1701
+ toolArgs
1702
+ }));
1703
+ }
1704
+ }
1705
+ if (message.content) {
1706
+ steps.push(this.createStep("response", message.content));
1707
+ }
1708
+ if (steps.length === 0) {
1709
+ steps.push(this.createStep("response", "(empty response)"));
1710
+ }
1711
+ return steps;
1712
+ }
1713
+ };
1714
+ var openaiCompatibleConnector = new OpenAICompatibleConnector();
1715
+
1523
1716
  // services/connectors/index.ts
1524
1717
  connectorRegistry.register(aguiStreamingConnector);
1525
1718
  connectorRegistry.register(mockConnector);
1526
1719
  connectorRegistry.register(restConnector);
1720
+ connectorRegistry.register(openaiCompatibleConnector);
1527
1721
  console.log("[Connectors] Browser-safe connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
1528
1722
 
1529
1723
  // services/connectors/subprocess/SubprocessConnector.ts
@@ -1890,7 +2084,39 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
1890
2084
  this.isInThinking = false;
1891
2085
  }
1892
2086
  /**
1893
- * Override execute to reset state
2087
+ * Build CLI args from ClaudeCodeConnectorConfig
2088
+ */
2089
+ buildConfigArgs(config) {
2090
+ const args = [];
2091
+ if (config.dangerouslySkipPermissions) {
2092
+ args.push("--dangerously-skip-permissions");
2093
+ }
2094
+ if (config.systemPrompt) {
2095
+ args.push("--system-prompt", config.systemPrompt);
2096
+ } else if (config.appendSystemPrompt) {
2097
+ args.push("--append-system-prompt", config.appendSystemPrompt);
2098
+ }
2099
+ if (config.allowedTools?.length) {
2100
+ args.push("--allowed-tools", ...config.allowedTools);
2101
+ }
2102
+ if (config.disallowedTools?.length) {
2103
+ args.push("--disallowed-tools", ...config.disallowedTools);
2104
+ }
2105
+ if (config.mcpConfigPath) {
2106
+ args.push("--mcp-config", config.mcpConfigPath);
2107
+ } else if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
2108
+ args.push("--mcp-config", JSON.stringify({ mcpServers: config.mcpServers }));
2109
+ }
2110
+ if (config.strictMcpConfig) {
2111
+ args.push("--strict-mcp-config");
2112
+ }
2113
+ if (config.additionalArgs) {
2114
+ args.push(...config.additionalArgs);
2115
+ }
2116
+ return args;
2117
+ }
2118
+ /**
2119
+ * Override execute to reset state and apply connectorConfig
1894
2120
  */
1895
2121
  async execute(endpoint, request, auth, onProgress, onRawEvent) {
1896
2122
  this.debug("========== execute() STARTED ==========");
@@ -1898,11 +2124,55 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
1898
2124
  this.debug("Test case:", request.testCase.name);
1899
2125
  this.debug("Config:", this["config"]);
1900
2126
  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;
2127
+ const originalEnv = this.config.env ? structuredClone(this.config.env) : {};
2128
+ const originalArgs = this.config.args ? [...this.config.args] : [];
2129
+ const originalInputMode = this.config.inputMode;
2130
+ const originalTimeout = this.config.timeout;
2131
+ const originalWorkingDir = this.config.workingDir;
2132
+ const ccConfig = request.connectorConfig;
2133
+ if (ccConfig) {
2134
+ this.debug("Applying connectorConfig:", Object.keys(ccConfig));
2135
+ if (ccConfig.env) {
2136
+ this.config.env = { ...this.config.env, ...ccConfig.env };
2137
+ }
2138
+ if (ccConfig.usePromptArg) {
2139
+ this.config.inputMode = "arg";
2140
+ }
2141
+ if (ccConfig.timeout !== void 0) {
2142
+ this.config.timeout = ccConfig.timeout;
2143
+ }
2144
+ if (ccConfig.workingDir) {
2145
+ this.config.workingDir = ccConfig.workingDir;
2146
+ }
2147
+ }
2148
+ if (this.config.env?.CLAUDE_CODE_USE_BEDROCK === "1") {
2149
+ this.config.env = { ...this.config.env, ANTHROPIC_API_KEY: "" };
2150
+ this.debug("Bedrock mode: cleared ANTHROPIC_API_KEY to bypass credit check");
2151
+ }
2152
+ if (request.modelId) {
2153
+ this.config.args = [...this.config.args || [], "--model", request.modelId];
2154
+ this.debug("Model flag added:", request.modelId);
2155
+ }
2156
+ if (ccConfig) {
2157
+ const configArgs = this.buildConfigArgs(ccConfig);
2158
+ if (configArgs.length > 0) {
2159
+ this.config.args = [...this.config.args || [], ...configArgs];
2160
+ this.debug("Config args added:", configArgs);
2161
+ }
2162
+ }
2163
+ try {
2164
+ this.debug("State reset, calling super.execute()...");
2165
+ const result = await super.execute(endpoint, request, auth, onProgress, onRawEvent);
2166
+ this.debug("super.execute() returned with", result.trajectory.length, "steps");
2167
+ this.debug("========== execute() COMPLETED ==========");
2168
+ return result;
2169
+ } finally {
2170
+ this.config.env = originalEnv;
2171
+ this.config.args = originalArgs;
2172
+ this.config.inputMode = originalInputMode;
2173
+ this.config.timeout = originalTimeout;
2174
+ this.config.workingDir = originalWorkingDir;
2175
+ }
1906
2176
  }
1907
2177
  /**
1908
2178
  * Health check - verify claude command exists
@@ -2156,6 +2426,12 @@ function createServerCleanup(result, isCI) {
2156
2426
  }
2157
2427
 
2158
2428
  // cli/utils/apiClient.ts
2429
+ var ServerError = class extends Error {
2430
+ constructor(message) {
2431
+ super(message);
2432
+ this.name = "ServerError";
2433
+ }
2434
+ };
2159
2435
  var ApiClient = class {
2160
2436
  constructor(baseUrl) {
2161
2437
  this.baseUrl = baseUrl;
@@ -2271,7 +2547,7 @@ var ApiClient = class {
2271
2547
  } else if (event.type === "completed" || event.type === "cancelled") {
2272
2548
  finalRun = event.run;
2273
2549
  } else if (event.type === "error") {
2274
- throw new Error(event.error);
2550
+ throw new ServerError(event.error);
2275
2551
  }
2276
2552
  } catch (e) {
2277
2553
  if (e instanceof SyntaxError) continue;
@@ -2281,6 +2557,9 @@ var ApiClient = class {
2281
2557
  }
2282
2558
  }
2283
2559
  } catch (streamError) {
2560
+ if (streamError instanceof ServerError) {
2561
+ throw streamError;
2562
+ }
2284
2563
  if (runId) {
2285
2564
  console.warn(`[ApiClient] SSE stream disconnected: ${streamError instanceof Error ? streamError.message : streamError}`);
2286
2565
  console.warn(`[ApiClient] Falling back to polling for run ${runId}...`);
@@ -2518,7 +2797,7 @@ var ApiClient = class {
2518
2797
  }
2519
2798
  const response = await this.listTestCasesWithMeta();
2520
2799
  return response.data.find(
2521
- (tc) => tc.name.toLowerCase() === identifier.toLowerCase()
2800
+ (tc) => tc.name?.toLowerCase() === identifier.toLowerCase()
2522
2801
  ) || null;
2523
2802
  }
2524
2803
  /**
@@ -2566,7 +2845,7 @@ var ApiClient = class {
2566
2845
  if (event.type === "completed") {
2567
2846
  result = event.report;
2568
2847
  } else if (event.type === "error") {
2569
- throw new Error(event.error);
2848
+ throw new ServerError(event.error);
2570
2849
  }
2571
2850
  } catch (e) {
2572
2851
  if (e instanceof SyntaxError) continue;
@@ -2616,12 +2895,53 @@ var ApiClient = class {
2616
2895
  }
2617
2896
  return res.json();
2618
2897
  }
2898
+ /**
2899
+ * Fetch traces from OpenSearch with optional filters
2900
+ */
2901
+ async fetchTraces(params) {
2902
+ const res = await fetch(`${this.baseUrl}/api/traces`, {
2903
+ method: "POST",
2904
+ headers: { "Content-Type": "application/json" },
2905
+ body: JSON.stringify(params)
2906
+ });
2907
+ if (!res.ok) {
2908
+ const errorBody = await res.text();
2909
+ let errorMessage;
2910
+ try {
2911
+ const parsed = JSON.parse(errorBody);
2912
+ errorMessage = parsed.error || errorBody;
2913
+ } catch {
2914
+ errorMessage = errorBody;
2915
+ }
2916
+ throw new Error(`Failed to fetch traces: ${errorMessage}`);
2917
+ }
2918
+ return res.json();
2919
+ }
2619
2920
  };
2620
2921
 
2621
- // cli/commands/list.ts
2922
+ // cli/utils/formatOutput.ts
2923
+ var OUTPUT_FORMAT_DESCRIPTION = "Output format: table, json, markdown";
2924
+ function formatMarkdownTable(headers, rows) {
2925
+ const separator = headers.map(() => "---");
2926
+ const lines = [
2927
+ `| ${headers.join(" | ")} |`,
2928
+ `| ${separator.join(" | ")} |`,
2929
+ ...rows.map((row) => `| ${row.join(" | ")} |`)
2930
+ ];
2931
+ return lines.join("\n");
2932
+ }
2622
2933
  function formatJson(data) {
2623
2934
  return JSON.stringify(data, null, 2);
2624
2935
  }
2936
+ function parseOutputFormat(format) {
2937
+ const normalized = format.toLowerCase();
2938
+ if (normalized === "table" || normalized === "json" || normalized === "markdown" || normalized === "md") {
2939
+ return normalized === "md" ? "markdown" : normalized;
2940
+ }
2941
+ return "table";
2942
+ }
2943
+
2944
+ // cli/commands/list.ts
2625
2945
  function displayStorageWarnings(meta) {
2626
2946
  if (!meta.storageConfigured) {
2627
2947
  console.log(chalk.yellow("\n \u26A0 Storage not configured"));
@@ -2642,25 +2962,24 @@ async function listAgents(format, config) {
2642
2962
  console.log(formatJson(agents));
2643
2963
  return;
2644
2964
  }
2965
+ const headers = ["Key", "Name", "Connector", "Endpoint"];
2966
+ const rows = agents.map((agent) => [
2967
+ agent.key,
2968
+ agent.name,
2969
+ agent.connectorType || "agui-streaming",
2970
+ agent.endpoint.substring(0, 47) + (agent.endpoint.length > 47 ? "..." : "")
2971
+ ]);
2972
+ if (format === "markdown") {
2973
+ console.log(formatMarkdownTable(headers, rows));
2974
+ return;
2975
+ }
2645
2976
  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],
2977
+ head: headers.map((h) => chalk.cyan(h)),
2978
+ colWidths: [15, 20, 15, 50],
2654
2979
  wordWrap: true
2655
2980
  });
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
- ]);
2981
+ for (const row of rows) {
2982
+ table.push(row);
2664
2983
  }
2665
2984
  console.log(chalk.bold("\nAvailable Agents:\n"));
2666
2985
  console.log(table.toString());
@@ -2682,31 +3001,35 @@ async function listTestCases(format, config) {
2682
3001
  try {
2683
3002
  const client = new ApiClient(serverResult.baseUrl);
2684
3003
  const response = await client.listTestCasesWithMeta();
2685
- displayStorageWarnings(response.meta);
3004
+ if (format !== "markdown") {
3005
+ displayStorageWarnings(response.meta);
3006
+ }
2686
3007
  if (format === "json") {
2687
3008
  console.log(formatJson(response));
2688
3009
  return;
2689
3010
  }
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) {
3011
+ const headers = ["ID", "Name", "Labels", "Version", "Source"];
3012
+ const rows = response.data.map((tc) => {
2702
3013
  const isDemo = tc.id.startsWith("demo-");
2703
- table.push([
3014
+ return [
2704
3015
  tc.id,
2705
3016
  tc.name,
2706
3017
  tc.labels?.slice(0, 3).join(", ") || "",
2707
3018
  `v${tc.currentVersion || 1}`,
2708
- isDemo ? chalk.gray("Sample") : chalk.green("Stored")
2709
- ]);
3019
+ isDemo ? "Sample" : "Stored"
3020
+ ];
3021
+ });
3022
+ if (format === "markdown") {
3023
+ console.log(formatMarkdownTable(headers, rows));
3024
+ return;
3025
+ }
3026
+ const table = new Table({
3027
+ head: headers.map((h) => chalk.cyan(h)),
3028
+ colWidths: [25, 28, 28, 10, 10],
3029
+ wordWrap: true
3030
+ });
3031
+ for (const row of rows) {
3032
+ table.push(row);
2710
3033
  }
2711
3034
  console.log(chalk.bold("\nAvailable Test Cases:\n"));
2712
3035
  console.log(table.toString());
@@ -2735,31 +3058,35 @@ async function listBenchmarks(format, config) {
2735
3058
  try {
2736
3059
  const client = new ApiClient(serverResult.baseUrl);
2737
3060
  const response = await client.listBenchmarksWithMeta();
2738
- displayStorageWarnings(response.meta);
3061
+ if (format !== "markdown") {
3062
+ displayStorageWarnings(response.meta);
3063
+ }
2739
3064
  if (format === "json") {
2740
3065
  console.log(formatJson(response));
2741
3066
  return;
2742
3067
  }
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) {
3068
+ const headers = ["ID", "Name", "Test Cases", "Created", "Source"];
3069
+ const rows = response.data.map((b) => {
2755
3070
  const isDemo = b.id.startsWith("demo-");
2756
- table.push([
3071
+ return [
2757
3072
  b.id,
2758
3073
  b.name,
2759
3074
  b.testCaseIds.length.toString(),
2760
3075
  new Date(b.createdAt).toLocaleDateString(),
2761
- isDemo ? chalk.gray("Sample") : chalk.green("Stored")
2762
- ]);
3076
+ isDemo ? "Sample" : "Stored"
3077
+ ];
3078
+ });
3079
+ if (format === "markdown") {
3080
+ console.log(formatMarkdownTable(headers, rows));
3081
+ return;
3082
+ }
3083
+ const table = new Table({
3084
+ head: headers.map((h) => chalk.cyan(h)),
3085
+ colWidths: [28, 28, 12, 22, 10],
3086
+ wordWrap: true
3087
+ });
3088
+ for (const row of rows) {
3089
+ table.push(row);
2763
3090
  }
2764
3091
  console.log(chalk.bold("\nAvailable Benchmarks:\n"));
2765
3092
  console.log(table.toString());
@@ -2796,19 +3123,25 @@ function listConnectors(format) {
2796
3123
  console.log(formatJson(connectors));
2797
3124
  return;
2798
3125
  }
3126
+ const headers = ["Type", "Name", "Streaming"];
3127
+ const rows = connectors.map((c) => [
3128
+ c.type,
3129
+ c.name,
3130
+ c.streaming ? "Yes" : "No"
3131
+ ]);
3132
+ if (format === "markdown") {
3133
+ console.log(formatMarkdownTable(headers, rows));
3134
+ return;
3135
+ }
2799
3136
  const table = new Table({
2800
- head: [
2801
- chalk.cyan("Type"),
2802
- chalk.cyan("Name"),
2803
- chalk.cyan("Streaming")
2804
- ],
3137
+ head: headers.map((h) => chalk.cyan(h)),
2805
3138
  colWidths: [20, 25, 12]
2806
3139
  });
2807
- for (const c of connectors) {
3140
+ for (const row of rows) {
2808
3141
  table.push([
2809
- c.type,
2810
- c.name,
2811
- c.streaming ? chalk.green("Yes") : chalk.gray("No")
3142
+ row[0],
3143
+ row[1],
3144
+ row[2] === "Yes" ? chalk.green("Yes") : chalk.gray("No")
2812
3145
  ]);
2813
3146
  }
2814
3147
  console.log(chalk.bold("\nRegistered Connectors:\n"));
@@ -2827,23 +3160,24 @@ async function listModels(format, config) {
2827
3160
  console.log(formatJson(models));
2828
3161
  return;
2829
3162
  }
3163
+ const headers = ["Key", "Display Name", "Provider", "Context"];
3164
+ const rows = models.map((m) => [
3165
+ m.key,
3166
+ m.display_name || m.key,
3167
+ m.provider || "bedrock",
3168
+ m.context_window ? `${Math.round(m.context_window / 1e3)}k` : "-"
3169
+ ]);
3170
+ if (format === "markdown") {
3171
+ console.log(formatMarkdownTable(headers, rows));
3172
+ return;
3173
+ }
2830
3174
  const table = new Table({
2831
- head: [
2832
- chalk.cyan("Key"),
2833
- chalk.cyan("Display Name"),
2834
- chalk.cyan("Provider"),
2835
- chalk.cyan("Context")
2836
- ],
3175
+ head: headers.map((h) => chalk.cyan(h)),
2837
3176
  colWidths: [25, 30, 12, 12],
2838
3177
  wordWrap: true
2839
3178
  });
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
- ]);
3179
+ for (const row of rows) {
3180
+ table.push(row);
2847
3181
  }
2848
3182
  console.log(chalk.bold("\nAvailable Models:\n"));
2849
3183
  console.log(table.toString());
@@ -2860,8 +3194,8 @@ async function listModels(format, config) {
2860
3194
  }
2861
3195
  }
2862
3196
  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;
3197
+ 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_DESCRIPTION, "table").action(async (resource, options) => {
3198
+ const format = parseOutputFormat(options.output);
2865
3199
  const config = await loadConfig();
2866
3200
  for (const connector of config.connectors) {
2867
3201
  connectorRegistry.register(connector);
@@ -2905,8 +3239,8 @@ function findAgent(identifier, config) {
2905
3239
  (a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
2906
3240
  );
2907
3241
  }
2908
- function getDefaultModel(agent) {
2909
- return agent.models[0] || "claude-sonnet";
3242
+ function getDefaultModel(config) {
3243
+ return Object.keys(config.models)[0] || "claude-sonnet";
2910
3244
  }
2911
3245
  async function commandExists(command) {
2912
3246
  const { execSync: execSync2 } = await import("child_process");
@@ -2960,26 +3294,36 @@ async function runForAgent(client, testCaseId, agent, modelId, verbose) {
2960
3294
  throw error;
2961
3295
  }
2962
3296
  }
2963
- function displayTableResults(results) {
3297
+ function buildResultRows(results) {
3298
+ return results.map((r) => {
3299
+ if (!r.report) {
3300
+ return [r.agent.name, "ERROR", "-", "-", "-"];
3301
+ }
3302
+ const status = r.report.passFailStatus === "passed" ? "PASSED" : r.report.passFailStatus === "failed" ? "FAILED" : r.report.status;
3303
+ return [
3304
+ r.agent.name,
3305
+ status,
3306
+ r.report.metrics?.accuracy ? `${Math.round(r.report.metrics.accuracy)}%` : "-",
3307
+ r.report.trajectorySteps.toString(),
3308
+ r.report.id?.substring(0, 27) + "..." || "-"
3309
+ ];
3310
+ });
3311
+ }
3312
+ function displayResults(results, format) {
3313
+ const headers = ["Agent", "Status", "Accuracy", "Steps", "Report ID"];
3314
+ const rows = buildResultRows(results);
3315
+ if (format === "markdown") {
3316
+ console.log("\n");
3317
+ console.log(formatMarkdownTable(headers, rows));
3318
+ return;
3319
+ }
2964
3320
  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
- ],
3321
+ head: headers.map((h) => chalk2.cyan(h)),
2972
3322
  colWidths: [20, 12, 12, 10, 30]
2973
3323
  });
2974
3324
  for (const r of results) {
2975
3325
  if (!r.report) {
2976
- table.push([
2977
- r.agent.name,
2978
- chalk2.red("ERROR"),
2979
- "-",
2980
- "-",
2981
- "-"
2982
- ]);
3326
+ table.push([r.agent.name, chalk2.red("ERROR"), "-", "-", "-"]);
2983
3327
  continue;
2984
3328
  }
2985
3329
  const statusStr = r.report.passFailStatus === "passed" ? chalk2.green("PASSED") : r.report.passFailStatus === "failed" ? chalk2.red("FAILED") : chalk2.yellow(r.report.status);
@@ -2995,7 +3339,7 @@ function displayTableResults(results) {
2995
3339
  console.log(table.toString());
2996
3340
  }
2997
3341
  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) => {
3342
+ 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_DESCRIPTION, "table").option("-v, --verbose", "Show detailed trajectory output").action(async (options) => {
2999
3343
  console.log(chalk2.bold("\nAgent Health - Test Case Runner\n"));
3000
3344
  const config = await loadConfig();
3001
3345
  for (const connector of config.connectors) {
@@ -3033,7 +3377,7 @@ function createRunCommand() {
3033
3377
  console.log("");
3034
3378
  const results = [];
3035
3379
  for (const agent of agents) {
3036
- const modelId = options.model || getDefaultModel(agent);
3380
+ const modelId = options.model || getDefaultModel(config);
3037
3381
  const validationError = await validateAgentRequirements(agent);
3038
3382
  if (validationError) {
3039
3383
  console.error(chalk2.red(` Error: ${validationError}`));
@@ -3044,17 +3388,27 @@ function createRunCommand() {
3044
3388
  const report = await runForAgent(client, testCase.id, agent, modelId, options.verbose || false);
3045
3389
  results.push({ agent, report });
3046
3390
  } catch (error) {
3047
- console.error(chalk2.red(` Error running ${agent.name}: ${error instanceof Error ? error.message : error}`));
3391
+ const errorMsg = error instanceof Error ? error.message : String(error);
3392
+ console.error(chalk2.red(` Error running ${agent.name}: ${errorMsg}`));
3393
+ const lowerError = errorMsg.toLowerCase();
3394
+ if (lowerError.includes("401") || lowerError.includes("403") || lowerError.includes("unauthorized") || lowerError.includes("forbidden") || lowerError.includes("token") || lowerError.includes("auth")) {
3395
+ console.log(chalk2.gray(` Hint: Authentication issue. Check agent-health.config.ts (headers, hooks.beforeRequest, or credentials).`));
3396
+ } else if (lowerError.includes("econnrefused") || lowerError.includes("enotfound") || lowerError.includes("connect")) {
3397
+ console.log(chalk2.gray(` Hint: Cannot reach agent endpoint. Run: npx @opensearch-project/agent-health doctor`));
3398
+ } else if (lowerError.includes("hook") || lowerError.includes("beforerequest")) {
3399
+ console.log(chalk2.gray(` Hint: The beforeRequest hook in agent-health.config.ts threw an error.`));
3400
+ }
3048
3401
  results.push({ agent, report: null });
3049
3402
  }
3050
3403
  }
3051
- if (options.output === "json") {
3052
- console.log(JSON.stringify(results.map((r) => ({
3404
+ const outputFormat = parseOutputFormat(options.output);
3405
+ if (outputFormat === "json") {
3406
+ console.log(formatJson(results.map((r) => ({
3053
3407
  agent: { key: r.agent.key, name: r.agent.name },
3054
3408
  report: r.report
3055
- })), null, 2));
3409
+ }))));
3056
3410
  } else {
3057
- displayTableResults(results);
3411
+ displayResults(results, outputFormat);
3058
3412
  }
3059
3413
  } catch (error) {
3060
3414
  console.error(chalk2.red(`
@@ -3201,8 +3555,8 @@ function findAgent2(identifier, config) {
3201
3555
  (a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
3202
3556
  );
3203
3557
  }
3204
- function getDefaultModel2(agent) {
3205
- return agent.models[0] || "claude-sonnet";
3558
+ function getDefaultModel2(config) {
3559
+ return Object.keys(config.models)[0] || "claude-sonnet";
3206
3560
  }
3207
3561
  function isFilePath(value) {
3208
3562
  return value.toLowerCase().endsWith(".json");
@@ -3238,7 +3592,7 @@ async function fetchReportsForRun(api, run) {
3238
3592
  );
3239
3593
  return reportsMap;
3240
3594
  }
3241
- async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3595
+ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose, concurrency) {
3242
3596
  const results = {
3243
3597
  agent,
3244
3598
  passed: 0,
@@ -3253,7 +3607,8 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3253
3607
  {
3254
3608
  name: `CLI Run - ${agent.name}`,
3255
3609
  agentKey: agent.key,
3256
- modelId
3610
+ modelId,
3611
+ ...concurrency && concurrency > 1 ? { concurrency } : {}
3257
3612
  },
3258
3613
  (event) => {
3259
3614
  if (event.type === "started") {
@@ -3262,9 +3617,13 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3262
3617
  const current = event.currentTestCaseIndex + 1;
3263
3618
  const testCaseName = event.currentTestCase?.name || `Test ${current}`;
3264
3619
  spinner.text = `${agent.name}: ${testCaseName} (${current}/${totalTestCases})`;
3265
- if (verbose && event.result) {
3620
+ if (event.result) {
3266
3621
  const status = event.result.status === "completed" ? chalk3.green("\u2713") : chalk3.red("\u2717");
3267
3622
  spinner.text = `${agent.name}: ${testCaseName} ${status} (${current}/${totalTestCases})`;
3623
+ if (verbose && event.result.status === "failed" && event.result.error) {
3624
+ spinner.info(`${agent.name}: ${testCaseName} ${chalk3.red("\u2717")} - ${event.result.error}`);
3625
+ spinner.start(`${agent.name}: (${current}/${totalTestCases})`);
3626
+ }
3268
3627
  }
3269
3628
  }
3270
3629
  }
@@ -3291,6 +3650,7 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3291
3650
  }
3292
3651
  } catch (error) {
3293
3652
  const errorMessage = error instanceof Error ? error.message : String(error);
3653
+ const isServerError = error instanceof ServerError;
3294
3654
  if (startedRunId) {
3295
3655
  results.runId = startedRunId;
3296
3656
  try {
@@ -3319,29 +3679,71 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3319
3679
  }
3320
3680
  return results;
3321
3681
  }
3682
+ if (run.status === "failed") {
3683
+ const runError = run.error || errorMessage;
3684
+ spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${runError}`);
3685
+ if (stats.passed > 0 || stats.failed > 0) {
3686
+ console.log(chalk3.gray(` Partial results: ${stats.passed} passed, ${stats.failed} failed out of ${stats.total}`));
3687
+ }
3688
+ return results;
3689
+ }
3322
3690
  }
3323
3691
  } catch {
3324
3692
  }
3325
3693
  }
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 {
3694
+ if (isServerError) {
3331
3695
  spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${errorMessage}`);
3696
+ } else {
3697
+ const isStreamError = errorMessage.includes("terminated") || errorMessage.includes("network") || errorMessage.includes("stream") || errorMessage.includes("aborted");
3698
+ if (isStreamError && startedRunId) {
3699
+ spinner.warn(`${agent.name}: ${chalk3.yellow("Stream disconnected")} - server may still be processing`);
3700
+ console.log(chalk3.gray(` Check status: Use the UI to monitor progress`));
3701
+ } else {
3702
+ spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${errorMessage}`);
3703
+ }
3704
+ }
3705
+ const lowerError = errorMessage.toLowerCase();
3706
+ if (lowerError.includes("401") || lowerError.includes("403") || lowerError.includes("unauthorized") || lowerError.includes("forbidden") || lowerError.includes("token") || lowerError.includes("auth")) {
3707
+ console.log(chalk3.gray(` Hint: This looks like an authentication issue. Check your agent-health.config.ts`));
3708
+ console.log(chalk3.gray(` (headers, hooks.beforeRequest, or credentials) and re-run.`));
3709
+ } else if (lowerError.includes("econnrefused") || lowerError.includes("enotfound") || lowerError.includes("connect")) {
3710
+ console.log(chalk3.gray(` Hint: Could not connect to the agent endpoint. Verify the endpoint in agent-health.config.ts`));
3711
+ console.log(chalk3.gray(` is reachable: npx @opensearch-project/agent-health doctor`));
3712
+ } else if (lowerError.includes("not found") || lowerError.includes("agent not found")) {
3713
+ console.log(chalk3.gray(` Hint: Agent key not found. List available agents: npx @opensearch-project/agent-health list agents`));
3714
+ } else if (lowerError.includes("hook") || lowerError.includes("beforerequest")) {
3715
+ console.log(chalk3.gray(` Hint: The beforeRequest hook in agent-health.config.ts threw an error.`));
3716
+ console.log(chalk3.gray(` Check the hook logic and any external services it calls.`));
3717
+ }
3718
+ if (errorMessage !== "terminated") {
3719
+ console.log(chalk3.gray(` Debug: Run with DEBUG=true for verbose server logs`));
3332
3720
  }
3333
3721
  }
3334
3722
  return results;
3335
3723
  }
3336
- function displaySummaryTable(allResults, totalTestCases) {
3724
+ function buildSummaryRows(allResults, totalTestCases) {
3725
+ return allResults.map((results) => {
3726
+ const passRate = totalTestCases > 0 ? results.passed / totalTestCases * 100 : 0;
3727
+ return [
3728
+ results.agent.name,
3729
+ results.passed.toString(),
3730
+ results.failed.toString(),
3731
+ `${passRate.toFixed(0)}%`,
3732
+ results.run?.id || results.runId || "N/A"
3733
+ ];
3734
+ });
3735
+ }
3736
+ function displaySummary(allResults, totalTestCases, format) {
3737
+ const headers = ["Agent", "Passed", "Failed", "Pass Rate", "Run ID"];
3738
+ const rows = buildSummaryRows(allResults, totalTestCases);
3739
+ if (format === "markdown") {
3740
+ console.log("\n");
3741
+ console.log("## Benchmark Summary\n");
3742
+ console.log(formatMarkdownTable(headers, rows));
3743
+ return;
3744
+ }
3337
3745
  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
- ],
3746
+ head: headers.map((h) => chalk3.cyan(h)),
3345
3747
  colWidths: [25, 10, 10, 12, 35]
3346
3748
  });
3347
3749
  for (const results of allResults) {
@@ -3359,26 +3761,50 @@ function displaySummaryTable(allResults, totalTestCases) {
3359
3761
  console.log(chalk3.bold("Benchmark Summary"));
3360
3762
  console.log(table.toString());
3361
3763
  }
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));
3764
+ async function exportResults(benchmark, allResults, exportPath, format, serverBaseUrl) {
3765
+ if (format !== "json") {
3766
+ const runIds = allResults.map((r) => r.run?.id || r.runId).filter((id) => !!id);
3767
+ const params = new URLSearchParams({ format });
3768
+ if (runIds.length > 0) {
3769
+ params.set("runIds", runIds.join(","));
3770
+ }
3771
+ const url = `${serverBaseUrl}/api/storage/benchmarks/${encodeURIComponent(benchmark.id)}/report?${params.toString()}`;
3772
+ const response = await fetch(url);
3773
+ if (!response.ok) {
3774
+ const errorBody = await response.json().catch(() => ({ error: "Unknown error" }));
3775
+ console.error(chalk3.red(`
3776
+ Export failed: ${errorBody.error}`));
3777
+ return;
3778
+ }
3779
+ const contentType = response.headers.get("content-type") || "";
3780
+ if (contentType.includes("application/pdf")) {
3781
+ const buffer = Buffer.from(await response.arrayBuffer());
3782
+ writeFileSync(exportPath, buffer);
3783
+ } else {
3784
+ const text = await response.text();
3785
+ writeFileSync(exportPath, text);
3786
+ }
3787
+ } else {
3788
+ const exportData = {
3789
+ benchmark: {
3790
+ id: benchmark.id,
3791
+ name: benchmark.name,
3792
+ testCaseCount: benchmark.testCaseIds.length
3793
+ },
3794
+ runs: allResults.map((r) => ({
3795
+ agent: { key: r.agent.key, name: r.agent.name },
3796
+ runId: r.run?.id || r.runId,
3797
+ status: r.run?.status,
3798
+ passed: r.passed,
3799
+ failed: r.failed,
3800
+ passRate: benchmark.testCaseIds.length > 0 ? r.passed / benchmark.testCaseIds.length * 100 : 0,
3801
+ results: r.run?.results,
3802
+ reports: r.reports
3803
+ })),
3804
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString()
3805
+ };
3806
+ writeFileSync(exportPath, JSON.stringify(exportData, null, 2));
3807
+ }
3382
3808
  console.log(chalk3.green(`
3383
3809
  Results exported to: ${exportPath}`));
3384
3810
  }
@@ -3388,7 +3814,7 @@ function createBenchmarkCommand() {
3388
3814
  "Agent key (can be specified multiple times)",
3389
3815
  (val, arr) => [...arr, val],
3390
3816
  []
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) => {
3817
+ ).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>", OUTPUT_FORMAT_DESCRIPTION, "table").option("--export <path>", "Export results to file").option("--format <type>", "Report format for --export: json (default), html, pdf", "json").option("-c, --concurrency <n>", "Number of test cases to run in parallel (default: 1)", "1").option("-v, --verbose", "Show detailed output").option("--stop-server", "Stop the server after benchmark completes (default: keep running)").action(async (options) => {
3392
3818
  console.log(chalk3.bold("\nAgent Health - Benchmark Runner\n"));
3393
3819
  const config = await loadConfig();
3394
3820
  const serverConfig = { ...DEFAULT_SERVER_CONFIG, ...config.server };
@@ -3522,6 +3948,9 @@ function createBenchmarkCommand() {
3522
3948
  console.log(chalk3.gray(` - ${a.name} (${a.key})`));
3523
3949
  }
3524
3950
  console.log("");
3951
+ console.log(chalk3.gray(" To add a custom agent, configure it in agent-health.config.ts"));
3952
+ console.log(chalk3.gray(" Generate one with: npx @opensearch-project/agent-health init"));
3953
+ console.log("");
3525
3954
  process.exit(1);
3526
3955
  }
3527
3956
  agents.push(agent);
@@ -3529,19 +3958,25 @@ function createBenchmarkCommand() {
3529
3958
  console.log(chalk3.gray(` Agents: ${agents.map((a) => a.name).join(", ")}`));
3530
3959
  }
3531
3960
  console.log("");
3961
+ const concurrency = Math.max(1, Math.min(20, parseInt(options.concurrency, 10) || 1));
3962
+ if (concurrency > 1) {
3963
+ console.log(chalk3.gray(` Concurrency: ${concurrency}`));
3964
+ }
3532
3965
  const allResults = [];
3533
3966
  for (const agent of agents) {
3534
- const modelId = options.model || getDefaultModel2(agent);
3967
+ const modelId = options.model || getDefaultModel2(config);
3535
3968
  const results = await runBenchmarkForAgent(
3536
3969
  api,
3537
3970
  agent,
3538
3971
  modelId,
3539
3972
  benchmark,
3540
- options.verbose || false
3973
+ options.verbose || false,
3974
+ concurrency
3541
3975
  );
3542
3976
  allResults.push(results);
3543
3977
  }
3544
- if (options.output === "json") {
3978
+ const outputFormat = parseOutputFormat(options.output);
3979
+ if (outputFormat === "json") {
3545
3980
  const jsonOutput = allResults.map((r) => ({
3546
3981
  agent: { key: r.agent.key, name: r.agent.name },
3547
3982
  runId: r.run?.id || r.runId,
@@ -3550,12 +3985,12 @@ function createBenchmarkCommand() {
3550
3985
  passRate: benchmark.testCaseIds.length > 0 ? r.passed / benchmark.testCaseIds.length * 100 : 0,
3551
3986
  results: r.run?.results
3552
3987
  }));
3553
- console.log(JSON.stringify(jsonOutput, null, 2));
3988
+ console.log(formatJson(jsonOutput));
3554
3989
  } else {
3555
- displaySummaryTable(allResults, benchmark.testCaseIds.length);
3990
+ displaySummary(allResults, benchmark.testCaseIds.length, outputFormat);
3556
3991
  }
3557
3992
  if (options.export) {
3558
- exportResults(benchmark, allResults, options.export);
3993
+ await exportResults(benchmark, allResults, options.export, options.format, serverResult.baseUrl);
3559
3994
  }
3560
3995
  console.log("");
3561
3996
  console.log(chalk3.cyan("View results:"));
@@ -3630,9 +4065,86 @@ function createExportCommand() {
3630
4065
  return command;
3631
4066
  }
3632
4067
 
3633
- // cli/commands/doctor.ts
4068
+ // cli/commands/report.ts
3634
4069
  import { Command as Command5 } from "commander";
3635
4070
  import chalk5 from "chalk";
4071
+ import ora3 from "ora";
4072
+ import { writeFileSync as writeFileSync3 } from "fs";
4073
+ function createReportCommand() {
4074
+ const command = new Command5("report").description("Generate a report for a benchmark").requiredOption("-b, --benchmark <id>", "Benchmark name or ID").option("-r, --runs <ids>", "Comma-separated run IDs (default: all runs)").option("-f, --format <type>", "Report format: json, html, pdf", "html").option("-o, --output <file>", "Output file path (auto-generates filename if omitted)").option("--stdout", "Write to stdout (JSON format only)").action(async (options) => {
4075
+ const config = await loadConfig();
4076
+ const serverConfig = { ...DEFAULT_SERVER_CONFIG, ...config.server };
4077
+ const connectSpinner = ora3("Connecting to server...").start();
4078
+ let serverResult;
4079
+ let cleanup;
4080
+ try {
4081
+ serverResult = await ensureServer(serverConfig);
4082
+ cleanup = createServerCleanup(serverResult, false);
4083
+ if (serverResult.wasStarted) {
4084
+ connectSpinner.succeed(`Started server on port ${serverConfig.port}`);
4085
+ } else {
4086
+ connectSpinner.succeed(`Connected to existing server on port ${serverConfig.port}`);
4087
+ }
4088
+ } catch (error) {
4089
+ connectSpinner.fail(
4090
+ `Failed to connect to server: ${error instanceof Error ? error.message : error}`
4091
+ );
4092
+ process.exit(1);
4093
+ }
4094
+ const api = new ApiClient(serverResult.baseUrl);
4095
+ try {
4096
+ const spinner = ora3("Finding benchmark...").start();
4097
+ const benchmark = await api.findBenchmark(options.benchmark);
4098
+ if (!benchmark) {
4099
+ spinner.fail(`Benchmark not found: "${options.benchmark}"`);
4100
+ console.log("");
4101
+ console.log(chalk5.cyan(" Available benchmarks:"));
4102
+ console.log(chalk5.gray(" npx agent-health list benchmarks"));
4103
+ console.log("");
4104
+ process.exit(1);
4105
+ }
4106
+ spinner.succeed(`Found benchmark: ${benchmark.name} (${benchmark.id})`);
4107
+ const params = new URLSearchParams({ format: options.format });
4108
+ if (options.runs) {
4109
+ params.set("runIds", options.runs);
4110
+ }
4111
+ const reportSpinner = ora3(`Generating ${options.format.toUpperCase()} report...`).start();
4112
+ const url = `${serverResult.baseUrl}/api/storage/benchmarks/${encodeURIComponent(benchmark.id)}/report?${params.toString()}`;
4113
+ const response = await fetch(url);
4114
+ if (!response.ok) {
4115
+ const errorBody = await response.json().catch(() => ({ error: "Unknown error" }));
4116
+ reportSpinner.fail(`Report generation failed: ${errorBody.error}`);
4117
+ process.exit(1);
4118
+ }
4119
+ const contentDisposition = response.headers.get("content-disposition") || "";
4120
+ const filenameMatch = contentDisposition.match(/filename="([^"]+)"/);
4121
+ const defaultFilename = filenameMatch?.[1] || `report.${options.format}`;
4122
+ if (options.stdout) {
4123
+ const text = await response.text();
4124
+ reportSpinner.stop();
4125
+ process.stdout.write(text);
4126
+ } else {
4127
+ const outputPath = options.output || defaultFilename;
4128
+ const contentType = response.headers.get("content-type") || "";
4129
+ if (contentType.includes("application/pdf")) {
4130
+ const buffer = Buffer.from(await response.arrayBuffer());
4131
+ writeFileSync3(outputPath, buffer);
4132
+ } else {
4133
+ const text = await response.text();
4134
+ writeFileSync3(outputPath, text);
4135
+ }
4136
+ reportSpinner.succeed(`Report saved to: ${outputPath}`);
4137
+ }
4138
+ } finally {
4139
+ cleanup();
4140
+ }
4141
+ });
4142
+ return command;
4143
+ }
4144
+
4145
+ // cli/commands/doctor.ts
4146
+ import { Command as Command6 } from "commander";
4147
+ import chalk6 from "chalk";
3636
4148
  import { existsSync as existsSync3 } from "fs";
3637
4149
  import { resolve as resolve2 } from "path";
3638
4150
  function checkConfigFile() {
@@ -3791,23 +4303,23 @@ function checkOpenSearchObservability() {
3791
4303
  ]
3792
4304
  };
3793
4305
  }
3794
- function displayResults(results) {
3795
- console.log(chalk5.bold("\n Configuration Check\n"));
4306
+ function displayResults2(results) {
4307
+ console.log(chalk6.bold("\n Configuration Check\n"));
3796
4308
  for (const result of results) {
3797
4309
  const icon = {
3798
- ok: chalk5.green("\u2713"),
3799
- warning: chalk5.yellow("\u26A0"),
3800
- error: chalk5.red("\u2717")
4310
+ ok: chalk6.green("\u2713"),
4311
+ warning: chalk6.yellow("\u26A0"),
4312
+ error: chalk6.red("\u2717")
3801
4313
  }[result.status];
3802
4314
  const messageColor = {
3803
- ok: chalk5.green,
3804
- warning: chalk5.yellow,
3805
- error: chalk5.red
4315
+ ok: chalk6.green,
4316
+ warning: chalk6.yellow,
4317
+ error: chalk6.red
3806
4318
  }[result.status];
3807
- console.log(` ${icon} ${chalk5.bold(result.name)}: ${messageColor(result.message)}`);
4319
+ console.log(` ${icon} ${chalk6.bold(result.name)}: ${messageColor(result.message)}`);
3808
4320
  if (result.details) {
3809
4321
  for (const detail of result.details) {
3810
- console.log(chalk5.gray(` ${detail}`));
4322
+ console.log(chalk6.gray(` ${detail}`));
3811
4323
  }
3812
4324
  }
3813
4325
  }
@@ -3815,17 +4327,17 @@ function displayResults(results) {
3815
4327
  const errors = results.filter((r) => r.status === "error").length;
3816
4328
  const warnings = results.filter((r) => r.status === "warning").length;
3817
4329
  if (errors > 0) {
3818
- console.log(chalk5.red(` ${errors} error(s) found. Fix these before running evaluations.
4330
+ console.log(chalk6.red(` ${errors} error(s) found. Fix these before running evaluations.
3819
4331
  `));
3820
4332
  } else if (warnings > 0) {
3821
- console.log(chalk5.yellow(` ${warnings} warning(s). Some features may be limited.
4333
+ console.log(chalk6.yellow(` ${warnings} warning(s). Some features may be limited.
3822
4334
  `));
3823
4335
  } else {
3824
- console.log(chalk5.green(" All checks passed!\n"));
4336
+ console.log(chalk6.green(" All checks passed!\n"));
3825
4337
  }
3826
4338
  }
3827
4339
  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) => {
4340
+ const command = new Command6("doctor").description("Check configuration and system requirements").option("-o, --output <format>", "Output format: text, json", "text").action(async (options) => {
3829
4341
  const results = [];
3830
4342
  const config = await loadConfig();
3831
4343
  for (const connector of config.connectors) {
@@ -3842,16 +4354,16 @@ function createDoctorCommand() {
3842
4354
  if (options.output === "json") {
3843
4355
  console.log(JSON.stringify(results, null, 2));
3844
4356
  } else {
3845
- displayResults(results);
4357
+ displayResults2(results);
3846
4358
  }
3847
4359
  });
3848
4360
  return command;
3849
4361
  }
3850
4362
 
3851
4363
  // 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";
4364
+ import { Command as Command7 } from "commander";
4365
+ import chalk7 from "chalk";
4366
+ import { writeFileSync as writeFileSync4, existsSync as existsSync4 } from "fs";
3855
4367
  import { resolve as resolve3 } from "path";
3856
4368
  var TYPESCRIPT_CONFIG = `/*
3857
4369
  * Agent Health Configuration
@@ -3873,7 +4385,6 @@ export default defineConfig({
3873
4385
  username: process.env.OPENSEARCH_USER || 'admin',
3874
4386
  password: process.env.OPENSEARCH_PASS || 'admin',
3875
4387
  },
3876
- models: ['claude-sonnet'],
3877
4388
  },
3878
4389
 
3879
4390
  // Claude Code CLI agent (optional)
@@ -3890,7 +4401,6 @@ export default defineConfig({
3890
4401
  },
3891
4402
  }),
3892
4403
  endpoint: 'claude', // Command name
3893
- models: ['claude-sonnet-4'],
3894
4404
  },
3895
4405
  */
3896
4406
  ],
@@ -3970,8 +4480,8 @@ expectedOutcomes:
3970
4480
  - The agent should suggest investigating network connectivity
3971
4481
  `;
3972
4482
  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"));
4483
+ const command = new Command7("init").description("Initialize configuration files").option("--force", "Overwrite existing files").option("--with-examples", "Include example test case").action(async (options) => {
4484
+ console.log(chalk7.bold("\n Agent Health - Initialize Configuration\n"));
3975
4485
  const cwd = process.cwd();
3976
4486
  const files = [];
3977
4487
  files.push({
@@ -4000,24 +4510,24 @@ function createInitCommand() {
4000
4510
  let skipped = 0;
4001
4511
  for (const file of files) {
4002
4512
  if (existsSync4(file.path) && !options.force) {
4003
- console.log(chalk6.yellow(` \u26A0 Skipped: ${file.name} (already exists, use --force to overwrite)`));
4513
+ console.log(chalk7.yellow(` \u26A0 Skipped: ${file.name} (already exists, use --force to overwrite)`));
4004
4514
  skipped++;
4005
4515
  } else {
4006
- writeFileSync3(file.path, file.content);
4007
- console.log(chalk6.green(` \u2713 Created: ${file.name}`));
4516
+ writeFileSync4(file.path, file.content);
4517
+ console.log(chalk7.green(` \u2713 Created: ${file.name}`));
4008
4518
  created++;
4009
4519
  }
4010
4520
  }
4011
4521
  console.log("");
4012
4522
  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"));
4523
+ console.log(chalk7.gray(" Next steps:"));
4524
+ console.log(chalk7.gray(" 1. Copy .env.example to .env and fill in your values"));
4525
+ console.log(chalk7.gray(" 2. Update the config file with your agent endpoint"));
4526
+ console.log(chalk7.gray(" 3. Run `agent-health doctor` to verify configuration"));
4527
+ console.log(chalk7.gray(" 4. Run `agent-health run -t sample-rca-001` to test\n"));
4018
4528
  }
4019
4529
  if (skipped > 0) {
4020
- console.log(chalk6.yellow(` ${skipped} file(s) skipped. Use --force to overwrite.
4530
+ console.log(chalk7.yellow(` ${skipped} file(s) skipped. Use --force to overwrite.
4021
4531
  `));
4022
4532
  }
4023
4533
  });
@@ -4025,9 +4535,9 @@ function createInitCommand() {
4025
4535
  }
4026
4536
 
4027
4537
  // cli/commands/migrate.ts
4028
- import { Command as Command7 } from "commander";
4029
- import chalk7 from "chalk";
4030
- import ora3 from "ora";
4538
+ import { Command as Command8 } from "commander";
4539
+ import chalk8 from "chalk";
4540
+ import ora4 from "ora";
4031
4541
  function computeStatsFromReports(run, reports) {
4032
4542
  const reportsMap = new Map(reports.map((r) => [r.id, r]));
4033
4543
  let passed = 0;
@@ -4065,26 +4575,26 @@ function computeStatsFromReports(run, reports) {
4065
4575
  return { passed, failed, pending, total };
4066
4576
  }
4067
4577
  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"));
4578
+ const command = new Command8("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) => {
4579
+ console.log(chalk8.cyan.bold("\n Benchmark Stats Migration\n"));
4070
4580
  const config = await loadConfig();
4071
4581
  const serverResult = await ensureServer(config.server);
4072
4582
  const cleanup = createServerCleanup(serverResult, config.server.reuseExistingServer === false);
4073
4583
  try {
4074
4584
  const client = new ApiClient(serverResult.baseUrl);
4075
- const spinner = ora3("Fetching benchmarks...").start();
4585
+ const spinner = ora4("Fetching benchmarks...").start();
4076
4586
  const benchmarks = await client.listBenchmarks();
4077
4587
  spinner.succeed(`Found ${benchmarks.length} benchmarks`);
4078
4588
  const migratable = benchmarks.filter(
4079
4589
  (b) => !b.id.startsWith("demo-") && (b.runs?.length ?? 0) > 0
4080
4590
  );
4081
4591
  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"));
4592
+ console.log(chalk8.yellow("\n No benchmarks to migrate.\n"));
4593
+ console.log(chalk8.gray(" Only user-created benchmarks with runs can be migrated."));
4594
+ console.log(chalk8.gray(" Sample data (demo-*) already has stats computed.\n"));
4085
4595
  return;
4086
4596
  }
4087
- console.log(chalk7.gray(`
4597
+ console.log(chalk8.gray(`
4088
4598
  Migrating ${migratable.length} benchmarks with runs...
4089
4599
  `));
4090
4600
  let totalRuns = 0;
@@ -4095,13 +4605,13 @@ function createMigrateCommand() {
4095
4605
  const runs = benchmark.runs || [];
4096
4606
  totalRuns += runs.length;
4097
4607
  if (options.verbose) {
4098
- console.log(chalk7.gray(` Processing: ${benchmark.name} (${runs.length} runs)`));
4608
+ console.log(chalk8.gray(` Processing: ${benchmark.name} (${runs.length} runs)`));
4099
4609
  }
4100
4610
  for (const run of runs) {
4101
4611
  if (run.stats && typeof run.stats.passed === "number") {
4102
4612
  skippedRuns++;
4103
4613
  if (options.verbose) {
4104
- console.log(chalk7.gray(` \u2713 ${run.name} - already has stats`));
4614
+ console.log(chalk8.gray(` \u2713 ${run.name} - already has stats`));
4105
4615
  }
4106
4616
  continue;
4107
4617
  }
@@ -4115,7 +4625,7 @@ function createMigrateCommand() {
4115
4625
  const { runs: reports } = await reportsRes.json();
4116
4626
  const stats = computeStatsFromReports(run, reports || []);
4117
4627
  if (options.verbose) {
4118
- console.log(chalk7.gray(
4628
+ console.log(chalk8.gray(
4119
4629
  ` \u2192 ${run.name}: passed=${stats.passed}, failed=${stats.failed}, pending=${stats.pending}`
4120
4630
  ));
4121
4631
  }
@@ -4138,30 +4648,30 @@ function createMigrateCommand() {
4138
4648
  errors++;
4139
4649
  const msg = error instanceof Error ? error.message : "Unknown error";
4140
4650
  if (options.verbose) {
4141
- console.log(chalk7.red(` \u2717 ${run.name} - ${msg}`));
4651
+ console.log(chalk8.red(` \u2717 ${run.name} - ${msg}`));
4142
4652
  }
4143
4653
  }
4144
4654
  }
4145
4655
  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`)
4656
+ options.dryRun ? chalk8.blue(` [DRY RUN] ${benchmark.name} - ${runs.length} runs would be processed`) : chalk8.green(` \u2713 ${benchmark.name} - ${runs.length} runs`)
4147
4657
  );
4148
4658
  }
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}`));
4659
+ console.log(chalk8.bold("\n Migration Summary\n"));
4660
+ console.log(chalk8.gray(` Total runs: ${totalRuns}`));
4661
+ console.log(chalk8.green(` Migrated: ${migratedRuns}`));
4662
+ console.log(chalk8.yellow(` Already done: ${skippedRuns}`));
4153
4663
  if (errors > 0) {
4154
- console.log(chalk7.red(` Errors: ${errors}`));
4664
+ console.log(chalk8.red(` Errors: ${errors}`));
4155
4665
  }
4156
4666
  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"));
4667
+ console.log(chalk8.blue("\n This was a dry run. No changes were made."));
4668
+ console.log(chalk8.blue(" Run without --dry-run to apply changes.\n"));
4159
4669
  } else {
4160
- console.log(chalk7.green("\n Migration complete!\n"));
4670
+ console.log(chalk8.green("\n Migration complete!\n"));
4161
4671
  }
4162
4672
  } catch (error) {
4163
4673
  const msg = error instanceof Error ? error.message : "Unknown error";
4164
- console.error(chalk7.red(`
4674
+ console.error(chalk8.red(`
4165
4675
  Error: ${msg}
4166
4676
  `));
4167
4677
  process.exit(1);
@@ -4172,6 +4682,222 @@ function createMigrateCommand() {
4172
4682
  return command;
4173
4683
  }
4174
4684
 
4685
+ // cli/commands/compare-services.ts
4686
+ import { Command as Command9 } from "commander";
4687
+ import chalk9 from "chalk";
4688
+ function analyzeErrorPatterns(spans) {
4689
+ const errorSpans = spans.filter((s) => s.status === "ERROR");
4690
+ if (errorSpans.length === 0) {
4691
+ return { errorSpans, patterns: [], avgDurationMs: 0 };
4692
+ }
4693
+ const patternMap = /* @__PURE__ */ new Map();
4694
+ let totalDuration = 0;
4695
+ for (const span of errorSpans) {
4696
+ const errorType = extractErrorType(span);
4697
+ const spanName = span.name || "Unknown";
4698
+ const duration = span.duration || 0;
4699
+ const errorMsg = extractErrorMessage(span);
4700
+ totalDuration += duration;
4701
+ if (!patternMap.has(errorType)) {
4702
+ patternMap.set(errorType, {
4703
+ count: 0,
4704
+ spanNames: /* @__PURE__ */ new Set(),
4705
+ durations: [],
4706
+ messages: /* @__PURE__ */ new Set()
4707
+ });
4708
+ }
4709
+ const pattern = patternMap.get(errorType);
4710
+ pattern.count++;
4711
+ pattern.spanNames.add(spanName);
4712
+ pattern.durations.push(duration);
4713
+ if (errorMsg) pattern.messages.add(errorMsg);
4714
+ }
4715
+ const patterns = Array.from(patternMap.entries()).map(([errorType, data]) => ({
4716
+ errorType,
4717
+ count: data.count,
4718
+ spanNames: Array.from(data.spanNames),
4719
+ avgDurationMs: data.durations.reduce((a, b) => a + b, 0) / data.durations.length,
4720
+ exampleMessages: Array.from(data.messages).slice(0, 3)
4721
+ // Top 3 examples
4722
+ })).sort((a, b) => b.count - a.count);
4723
+ const avgDurationMs = totalDuration / errorSpans.length;
4724
+ return { errorSpans, patterns, avgDurationMs };
4725
+ }
4726
+ function extractErrorType(span) {
4727
+ const attrs = span.attributes || {};
4728
+ if (attrs["error.type"]) return attrs["error.type"];
4729
+ if (attrs["exception.type"]) return attrs["exception.type"];
4730
+ if (attrs["http.status_code"] >= 400) return `HTTP ${attrs["http.status_code"]}`;
4731
+ const name = span.name || "";
4732
+ if (name.includes("timeout")) return "Timeout";
4733
+ if (name.includes("connection")) return "Connection Error";
4734
+ if (name.includes("auth")) return "Authentication Error";
4735
+ return "Unknown Error";
4736
+ }
4737
+ function extractErrorMessage(span) {
4738
+ const attrs = span.attributes || {};
4739
+ if (attrs["error.message"]) return attrs["error.message"];
4740
+ if (attrs["exception.message"]) return attrs["exception.message"];
4741
+ if (span.events && Array.isArray(span.events)) {
4742
+ for (const event of span.events) {
4743
+ if (event.name === "exception" && event.attributes?.["exception.message"]) {
4744
+ return event.attributes["exception.message"];
4745
+ }
4746
+ }
4747
+ }
4748
+ return null;
4749
+ }
4750
+ async function analyzeServiceErrors(client, serviceName, startTime, endTime, limit = 1e3) {
4751
+ console.log(chalk9.gray(`
4752
+ Fetching traces for service: ${serviceName}...`));
4753
+ const response = await client.fetchTraces({
4754
+ serviceName,
4755
+ startTime,
4756
+ endTime,
4757
+ size: limit
4758
+ });
4759
+ const spans = response.spans || [];
4760
+ console.log(chalk9.gray(` Found ${spans.length} spans`));
4761
+ const traceMap = /* @__PURE__ */ new Map();
4762
+ for (const span of spans) {
4763
+ if (!traceMap.has(span.traceId)) {
4764
+ traceMap.set(span.traceId, []);
4765
+ }
4766
+ traceMap.get(span.traceId).push(span);
4767
+ }
4768
+ const totalTraces = traceMap.size;
4769
+ let tracesWithErrors = 0;
4770
+ for (const traceSpans of traceMap.values()) {
4771
+ if (traceSpans.some((s) => s.status === "ERROR")) {
4772
+ tracesWithErrors++;
4773
+ }
4774
+ }
4775
+ const errorRate = totalTraces > 0 ? tracesWithErrors / totalTraces * 100 : 0;
4776
+ const { errorSpans, patterns, avgDurationMs } = analyzeErrorPatterns(spans);
4777
+ return {
4778
+ serviceName,
4779
+ totalTraces,
4780
+ tracesWithErrors,
4781
+ errorRate,
4782
+ totalErrorSpans: errorSpans.length,
4783
+ errorPatterns: patterns,
4784
+ avgErrorDurationMs: avgDurationMs
4785
+ };
4786
+ }
4787
+ function printServiceAnalysis(analysis) {
4788
+ console.log(chalk9.bold.cyan(`
4789
+ ${"=".repeat(60)}`));
4790
+ console.log(chalk9.bold.cyan(`Service: ${analysis.serviceName}`));
4791
+ console.log(chalk9.bold.cyan("=".repeat(60)));
4792
+ console.log(chalk9.white(`Total Traces: ${analysis.totalTraces}`));
4793
+ console.log(chalk9.white(`Traces with Errors: ${analysis.tracesWithErrors}`));
4794
+ const errorRateColor = analysis.errorRate > 10 ? chalk9.red : analysis.errorRate > 5 ? chalk9.yellow : chalk9.green;
4795
+ console.log(errorRateColor(`Error Rate: ${analysis.errorRate.toFixed(2)}%`));
4796
+ console.log(chalk9.white(`Total Error Spans: ${analysis.totalErrorSpans}`));
4797
+ console.log(chalk9.white(`Avg Error Span Duration: ${analysis.avgErrorDurationMs.toFixed(2)}ms`));
4798
+ if (analysis.errorPatterns.length > 0) {
4799
+ console.log(chalk9.bold.white("\nError Patterns:"));
4800
+ for (const pattern of analysis.errorPatterns) {
4801
+ console.log(chalk9.yellow(`
4802
+ \u2022 ${pattern.errorType}`));
4803
+ console.log(chalk9.gray(` Count: ${pattern.count}`));
4804
+ console.log(chalk9.gray(` Avg Duration: ${pattern.avgDurationMs.toFixed(2)}ms`));
4805
+ console.log(chalk9.gray(` Affected Spans: ${pattern.spanNames.join(", ")}`));
4806
+ if (pattern.exampleMessages.length > 0) {
4807
+ console.log(chalk9.gray(` Example Messages:`));
4808
+ pattern.exampleMessages.forEach((msg) => {
4809
+ console.log(chalk9.gray(` - ${msg.substring(0, 80)}${msg.length > 80 ? "..." : ""}`));
4810
+ });
4811
+ }
4812
+ }
4813
+ } else {
4814
+ console.log(chalk9.green("\n\u2713 No error patterns detected"));
4815
+ }
4816
+ }
4817
+ function printComparison(service1, service2) {
4818
+ console.log(chalk9.bold.magenta(`
4819
+ ${"=".repeat(60)}`));
4820
+ console.log(chalk9.bold.magenta("COMPARISON SUMMARY"));
4821
+ console.log(chalk9.bold.magenta("=".repeat(60)));
4822
+ const errorRateDiff = service1.errorRate - service2.errorRate;
4823
+ const diffColor = Math.abs(errorRateDiff) < 1 ? chalk9.white : errorRateDiff > 0 ? chalk9.red : chalk9.green;
4824
+ const diffSymbol = errorRateDiff > 0 ? "\u2191" : errorRateDiff < 0 ? "\u2193" : "=";
4825
+ console.log(chalk9.bold.white("\nError Rate:"));
4826
+ console.log(` ${service1.serviceName}: ${service1.errorRate.toFixed(2)}%`);
4827
+ console.log(` ${service2.serviceName}: ${service2.errorRate.toFixed(2)}%`);
4828
+ console.log(diffColor(` Difference: ${diffSymbol} ${Math.abs(errorRateDiff).toFixed(2)}%`));
4829
+ console.log(chalk9.bold.white("\nUnique Error Patterns:"));
4830
+ const patterns1 = new Set(service1.errorPatterns.map((p) => p.errorType));
4831
+ const patterns2 = new Set(service2.errorPatterns.map((p) => p.errorType));
4832
+ const onlyIn1 = Array.from(patterns1).filter((p) => !patterns2.has(p));
4833
+ const onlyIn2 = Array.from(patterns2).filter((p) => !patterns1.has(p));
4834
+ const inBoth = Array.from(patterns1).filter((p) => patterns2.has(p));
4835
+ if (onlyIn1.length > 0) {
4836
+ console.log(chalk9.cyan(`
4837
+ Only in ${service1.serviceName}:`));
4838
+ onlyIn1.forEach((p) => console.log(chalk9.gray(` \u2022 ${p}`)));
4839
+ }
4840
+ if (onlyIn2.length > 0) {
4841
+ console.log(chalk9.cyan(`
4842
+ Only in ${service2.serviceName}:`));
4843
+ onlyIn2.forEach((p) => console.log(chalk9.gray(` \u2022 ${p}`)));
4844
+ }
4845
+ if (inBoth.length > 0) {
4846
+ console.log(chalk9.cyan(`
4847
+ Common error patterns:`));
4848
+ inBoth.forEach((p) => {
4849
+ const count1 = service1.errorPatterns.find((x) => x.errorType === p)?.count || 0;
4850
+ const count2 = service2.errorPatterns.find((x) => x.errorType === p)?.count || 0;
4851
+ console.log(chalk9.gray(` \u2022 ${p}: ${count1} vs ${count2}`));
4852
+ });
4853
+ }
4854
+ console.log(chalk9.bold.yellow("\nRecommendations:"));
4855
+ if (service1.errorRate > service2.errorRate * 1.5) {
4856
+ console.log(chalk9.yellow(` \u26A0 ${service1.serviceName} has significantly higher error rate - investigate urgently`));
4857
+ } else if (service2.errorRate > service1.errorRate * 1.5) {
4858
+ console.log(chalk9.yellow(` \u26A0 ${service2.serviceName} has significantly higher error rate - investigate urgently`));
4859
+ } else {
4860
+ console.log(chalk9.green(` \u2713 Error rates are comparable`));
4861
+ }
4862
+ if (onlyIn1.length > 2) {
4863
+ console.log(chalk9.yellow(` \u26A0 ${service1.serviceName} has ${onlyIn1.length} unique error types - review configuration`));
4864
+ }
4865
+ if (onlyIn2.length > 2) {
4866
+ console.log(chalk9.yellow(` \u26A0 ${service2.serviceName} has ${onlyIn2.length} unique error types - review configuration`));
4867
+ }
4868
+ }
4869
+ function createCompareServicesCommand() {
4870
+ const cmd = new Command9("compare-services");
4871
+ cmd.description("Compare error patterns between two services from trace data").requiredOption("-s, --services <service1,service2>", 'Comma-separated service names (e.g., "lambda-api,eks-api")').option("--start <time>", 'Start time (ISO 8601 format or relative like "1h", "24h")').option("--end <time>", "End time (ISO 8601 format)").option("--limit <number>", "Maximum number of spans to fetch per service", "1000").action(async (options) => {
4872
+ try {
4873
+ const config = await loadConfig();
4874
+ const serverResult = await ensureServer(config.server);
4875
+ const client = new ApiClient(serverResult.baseUrl);
4876
+ const serviceNames = options.services.split(",").map((s) => s.trim());
4877
+ if (serviceNames.length !== 2) {
4878
+ console.error(chalk9.red("Error: Please provide exactly two service names"));
4879
+ process.exit(1);
4880
+ }
4881
+ const [service1Name, service2Name] = serviceNames;
4882
+ const limit = parseInt(options.limit, 10);
4883
+ console.log(chalk9.bold.cyan("\nComparing Error Patterns Between Services"));
4884
+ console.log(chalk9.gray(`Service 1: ${service1Name}`));
4885
+ console.log(chalk9.gray(`Service 2: ${service2Name}`));
4886
+ if (options.start) console.log(chalk9.gray(`Time Range: ${options.start} to ${options.end || "now"}`));
4887
+ const analysis1 = await analyzeServiceErrors(client, service1Name, options.start, options.end, limit);
4888
+ const analysis2 = await analyzeServiceErrors(client, service2Name, options.start, options.end, limit);
4889
+ printServiceAnalysis(analysis1);
4890
+ printServiceAnalysis(analysis2);
4891
+ printComparison(analysis1, analysis2);
4892
+ console.log();
4893
+ } catch (error) {
4894
+ console.error(chalk9.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
4895
+ process.exit(1);
4896
+ }
4897
+ });
4898
+ return cmd;
4899
+ }
4900
+
4175
4901
  // cli/index.ts
4176
4902
  var __filename3 = fileURLToPath3(import.meta.url);
4177
4903
  var __dirname3 = dirname3(__filename3);
@@ -4185,59 +4911,112 @@ try {
4185
4911
  function loadEnvFile(envPath) {
4186
4912
  const absolutePath = resolve4(process.cwd(), envPath);
4187
4913
  if (!existsSync5(absolutePath)) {
4188
- console.error(chalk8.red(`
4914
+ console.error(chalk10.red(`
4189
4915
  Error: Environment file not found: ${absolutePath}
4190
4916
  `));
4191
4917
  process.exit(1);
4192
4918
  }
4193
4919
  const result = loadDotenv({ path: absolutePath });
4194
4920
  if (result.error) {
4195
- console.error(chalk8.red(`
4921
+ console.error(chalk10.red(`
4196
4922
  Error loading environment file: ${result.error.message}
4197
4923
  `));
4198
4924
  process.exit(1);
4199
4925
  }
4200
- console.log(chalk8.gray(` Loaded environment from: ${envPath}`));
4926
+ console.log(chalk10.gray(` Loaded environment from: ${envPath}`));
4201
4927
  }
4202
4928
  var defaultEnvPath = resolve4(process.cwd(), ".env");
4203
4929
  if (existsSync5(defaultEnvPath)) {
4204
4930
  loadDotenv({ path: defaultEnvPath });
4205
4931
  }
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();
4932
+ var program = new Command10();
4933
+ program.name("agent-health").description("Agent Health Evaluation Framework - Evaluate and monitor AI agent performance").version(version).enablePositionalOptions().passThroughOptions().configureHelp({
4934
+ sortSubcommands: false,
4935
+ // Hide default command list — replaced by grouped custom help below
4936
+ subcommandTerm: () => "",
4937
+ formatHelp: (cmd, helper) => {
4938
+ const termWidth = helper.padWidth(cmd, helper);
4939
+ const helpWidth = helper.helpWidth || 80;
4940
+ const output = [];
4941
+ const desc = helper.commandDescription(cmd);
4942
+ if (desc) {
4943
+ output.push(desc, "");
4944
+ }
4945
+ output.push(`${chalk10.cyan.bold("Usage:")} ${helper.commandUsage(cmd)}`, "");
4946
+ const optionList = helper.visibleOptions(cmd).map((opt) => {
4947
+ const term = helper.optionTerm(opt);
4948
+ const desc2 = helper.optionDescription(opt);
4949
+ return ` ${term.padEnd(termWidth)} ${desc2}`;
4950
+ }).join("\n");
4951
+ if (optionList) {
4952
+ output.push(`${chalk10.cyan.bold("Options:")}`, optionList, "");
4953
+ }
4954
+ return output.join("\n");
4955
+ }
4956
+ });
4957
+ program.addHelpText("after", `
4958
+ ${chalk10.cyan.bold("Getting Started:")}
4959
+ ${chalk10.yellow("agent-health")} Launch the web UI and evaluation server
4960
+ ${chalk10.yellow("agent-health init")} Generate an agent-health.config.ts file
4961
+ ${chalk10.yellow("agent-health doctor")} Verify your setup (AWS creds, OpenSearch, agents)
4962
+
4963
+ ${chalk10.cyan.bold("Running Evaluations:")}
4964
+ ${chalk10.yellow("agent-health run")} ${chalk10.gray("-t <case> -a <agent>")} Run a single test case against an agent
4965
+ ${chalk10.yellow("agent-health benchmark")} ${chalk10.gray("-f <file>")} Run a full benchmark from a test cases JSON file
4966
+ ${chalk10.yellow("agent-health benchmark")} ${chalk10.gray("-b <id>")} Re-run an existing benchmark
4967
+
4968
+ ${chalk10.cyan.bold("Viewing Results:")}
4969
+ ${chalk10.yellow("agent-health list")} ${chalk10.gray("agents|benchmarks|...")} List agents, connectors, test cases, or benchmarks
4970
+ ${chalk10.yellow("agent-health report")} ${chalk10.gray("-b <benchmark>")} Generate an HTML/PDF/JSON report
4971
+ ${chalk10.yellow("agent-health export")} ${chalk10.gray("-b <benchmark>")} Export test cases as re-importable JSON
4972
+ ${chalk10.yellow("agent-health compare-services")} ${chalk10.gray("-s A B")} Compare error patterns between services
4973
+
4974
+ ${chalk10.cyan.bold("Maintenance:")}
4975
+ ${chalk10.yellow("agent-health migrate")} Migrate legacy benchmark data to current format
4976
+ ${chalk10.yellow("agent-health serve")} Start the server (same as default, explicit command)
4977
+
4978
+ ${chalk10.cyan.bold("Examples:")}
4979
+ ${chalk10.gray("$")} npx @opensearch-project/agent-health
4980
+ ${chalk10.gray("$")} npx @opensearch-project/agent-health --port 8080 --no-browser
4981
+ ${chalk10.gray("$")} npx @opensearch-project/agent-health run -t "RCA for 500 errors" -a langgraph
4982
+ ${chalk10.gray("$")} npx @opensearch-project/agent-health benchmark -f ./test-cases.json -a my-agent
4983
+ ${chalk10.gray("$")} npx @opensearch-project/agent-health list agents
4984
+ ${chalk10.gray("$")} npx @opensearch-project/agent-health report -b bench-123 -f pdf -o report.pdf
4985
+ `);
4208
4986
  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
4987
  program.action(async (options) => {
4210
- console.log(chalk8.cyan.bold(`
4988
+ console.log(chalk10.cyan.bold(`
4211
4989
  Agent Health v${version} - AI Agent Evaluation Framework
4212
4990
  `));
4213
- console.log(chalk8.gray(` Working directory: ${process.cwd()}`));
4214
- console.log(chalk8.gray(` Package directory: ${__dirname3}`));
4991
+ console.log(chalk10.gray(` Working directory: ${process.cwd()}`));
4992
+ console.log(chalk10.gray(` Package directory: ${__dirname3}`));
4215
4993
  if (options.envFile) {
4216
4994
  loadEnvFile(options.envFile);
4217
4995
  } else if (existsSync5(defaultEnvPath)) {
4218
- console.log(chalk8.gray(" Auto-loaded .env from current directory"));
4996
+ console.log(chalk10.gray(" Auto-loaded .env from current directory"));
4219
4997
  }
4220
4998
  const port = parseInt(options.port, 10);
4221
- const spinner = ora4("Starting server...").start();
4999
+ const spinner = ora5("Starting server...").start();
4222
5000
  try {
4223
5001
  await startServer({ port });
4224
5002
  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)
5003
+ console.log(chalk10.gray("\n Configuration:"));
5004
+ console.log(chalk10.gray(` Storage: Sample data (configure OpenSearch for persistence)`));
5005
+ console.log(chalk10.gray(` Agent: Select in UI (Demo Agent for mock, real agents require endpoints)`));
5006
+ console.log(chalk10.gray(` Judge: Select in UI (Demo Judge for mock, Bedrock requires AWS creds)
4229
5007
  `));
4230
5008
  const url = `http://localhost:${port}`;
4231
- console.log(chalk8.green(` Server running at ${chalk8.bold(url)}
5009
+ console.log(chalk10.green(` Server running at ${chalk10.bold(url)}
4232
5010
  `));
5011
+ console.log(chalk10.green(` Demo data loaded`));
4233
5012
  if (options.browser !== false) {
4234
- console.log(chalk8.gray(" Opening browser..."));
5013
+ console.log(chalk10.gray(" Opening browser..."));
4235
5014
  await open(url);
4236
5015
  }
4237
- console.log(chalk8.gray(" Press Ctrl+C to stop\n"));
5016
+ console.log(chalk10.gray(" Press Ctrl+C to stop\n"));
4238
5017
  } catch (error) {
4239
5018
  spinner.fail("Failed to start server");
4240
- console.error(chalk8.red(`
5019
+ console.error(chalk10.red(`
4241
5020
  Error: ${error instanceof Error ? error.message : error}
4242
5021
  `));
4243
5022
  process.exit(1);
@@ -4247,29 +5026,31 @@ program.addCommand(createListCommand());
4247
5026
  program.addCommand(createRunCommand());
4248
5027
  program.addCommand(createBenchmarkCommand());
4249
5028
  program.addCommand(createExportCommand());
5029
+ program.addCommand(createReportCommand());
4250
5030
  program.addCommand(createDoctorCommand());
4251
5031
  program.addCommand(createInitCommand());
4252
5032
  program.addCommand(createMigrateCommand());
5033
+ program.addCommand(createCompareServicesCommand());
4253
5034
  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(`
5035
+ console.log(chalk10.cyan.bold(`
4255
5036
  Agent Health v${version} - AI Agent Evaluation Framework
4256
5037
  `));
4257
5038
  const port = parseInt(options.port, 10);
4258
- const spinner = ora4("Starting server...").start();
5039
+ const spinner = ora5("Starting server...").start();
4259
5040
  try {
4260
5041
  await startServer({ port });
4261
5042
  spinner.succeed("Server started");
4262
5043
  const url = `http://localhost:${port}`;
4263
- console.log(chalk8.green(` Server running at ${chalk8.bold(url)}
5044
+ console.log(chalk10.green(` Server running at ${chalk10.bold(url)}
4264
5045
  `));
4265
5046
  if (options.browser !== false) {
4266
- console.log(chalk8.gray(" Opening browser..."));
5047
+ console.log(chalk10.gray(" Opening browser..."));
4267
5048
  await open(url);
4268
5049
  }
4269
- console.log(chalk8.gray(" Press Ctrl+C to stop\n"));
5050
+ console.log(chalk10.gray(" Press Ctrl+C to stop\n"));
4270
5051
  } catch (error) {
4271
5052
  spinner.fail("Failed to start server");
4272
- console.error(chalk8.red(`
5053
+ console.error(chalk10.red(`
4273
5054
  Error: ${error instanceof Error ? error.message : error}
4274
5055
  `));
4275
5056
  process.exit(1);
@@ -4278,15 +5059,15 @@ program.command("serve").description("Start the Agent Health server (same as def
4278
5059
  program.on("command:*", (operands) => {
4279
5060
  const unknownCommand = operands[0];
4280
5061
  const availableCommands = program.commands.map((cmd) => cmd.name());
4281
- console.error(chalk8.red(`
5062
+ console.error(chalk10.red(`
4282
5063
  Error: Unknown command '${unknownCommand}'`));
4283
5064
  console.log("");
4284
- console.log(chalk8.cyan(" Available commands:"));
5065
+ console.log(chalk10.cyan(" Available commands:"));
4285
5066
  for (const cmd of availableCommands) {
4286
- console.log(chalk8.gray(` - ${cmd}`));
5067
+ console.log(chalk10.gray(` - ${cmd}`));
4287
5068
  }
4288
5069
  console.log("");
4289
- console.log(chalk8.gray(` Run ${chalk8.cyan("agent-health --help")} for usage information.
5070
+ console.log(chalk10.gray(` Run ${chalk10.cyan("agent-health --help")} for usage information.
4290
5071
  `));
4291
5072
  process.exitCode = 1;
4292
5073
  });