@opensearch-project/agent-health 0.1.1 → 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,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // cli/index.ts
4
- import { Command as Command9 } from "commander";
5
- import chalk9 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";
@@ -96,9 +96,9 @@ var ENV_CONFIG = {
96
96
  mlcommonsHeaderAwsSessionToken: getEnvVar("MLCOMMONS_HEADER_AWS_SESSION_TOKEN", ""),
97
97
  // Travel Planner multi-agent endpoint (OTel Demo in Docker)
98
98
  travelPlannerEndpoint: getEnvVar("TRAVEL_PLANNER_ENDPOINT", "http://localhost:3000"),
99
- // LiteLLM (optional - for OpenAI-compatible judge/agent endpoints)
100
- litellmApiKey: getEnvVar("LITELLM_API_KEY", ""),
101
- litellmEndpoint: getEnvVar("LITELLM_ENDPOINT", "http://localhost:4000/v1/chat/completions"),
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"),
102
102
  // Claude Code Telemetry (optional - for OTEL traces from Claude Code)
103
103
  claudeCodeTelemetryEnabled: getEnvVar("CLAUDE_CODE_TELEMETRY_ENABLED", "false") === "true",
104
104
  otelExporterEndpoint: getEnvVar("OTEL_EXPORTER_OTLP_ENDPOINT", ""),
@@ -106,39 +106,47 @@ var ENV_CONFIG = {
106
106
  otelExporterProtocol: getEnvVar("OTEL_EXPORTER_OTLP_PROTOCOL", ""),
107
107
  otelExporterHeaders: getEnvVar("OTEL_EXPORTER_OTLP_HEADERS", "")
108
108
  };
109
- function buildMLCommonsHeaders() {
110
- const headers = {};
111
- if (ENV_CONFIG.mlcommonsHeaderOpenSearchUrl) {
112
- headers["opensearch-url"] = ENV_CONFIG.mlcommonsHeaderOpenSearchUrl;
113
- }
114
- if (ENV_CONFIG.mlcommonsHeaderAwsRegion) {
115
- headers["aws-region"] = ENV_CONFIG.mlcommonsHeaderAwsRegion;
116
- }
117
- if (ENV_CONFIG.mlcommonsHeaderAuthorization) {
118
- headers["Authorization"] = ENV_CONFIG.mlcommonsHeaderAuthorization;
119
- } else {
120
- if (ENV_CONFIG.mlcommonsHeaderAwsServiceName) {
121
- headers["aws-service-name"] = ENV_CONFIG.mlcommonsHeaderAwsServiceName;
122
- }
123
- if (ENV_CONFIG.mlcommonsHeaderAwsAccessKeyId) {
124
- headers["aws-access-key-id"] = ENV_CONFIG.mlcommonsHeaderAwsAccessKeyId;
125
- }
126
- if (ENV_CONFIG.mlcommonsHeaderAwsSecretAccessKey) {
127
- headers["aws-secret-access-key"] = ENV_CONFIG.mlcommonsHeaderAwsSecretAccessKey;
128
- }
129
- if (ENV_CONFIG.mlcommonsHeaderAwsSessionToken) {
130
- headers["aws-session-token"] = ENV_CONFIG.mlcommonsHeaderAwsSessionToken;
131
- }
132
- }
133
- return headers;
134
- }
135
109
 
136
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);
137
145
  function getClaudeCodeConnectorEnv() {
138
146
  const env = {
139
- AWS_PROFILE: process.env.AWS_PROFILE || "Bedrock",
147
+ AWS_PROFILE: ENV_CONFIG.awsProfile || "Bedrock",
140
148
  CLAUDE_CODE_USE_BEDROCK: "1",
141
- AWS_REGION: process.env.AWS_REGION || "us-west-2",
149
+ AWS_REGION: ENV_CONFIG.awsRegion || "us-west-2",
142
150
  DISABLE_PROMPT_CACHING: "1",
143
151
  DISABLE_ERROR_REPORTING: "1"
144
152
  };
@@ -165,37 +173,15 @@ var DEFAULT_CONFIG = {
165
173
  endpoint: "mock://demo",
166
174
  description: "Mock agent for testing (simulated responses)",
167
175
  connectorType: "mock",
168
- models: ["demo-model"],
169
176
  headers: {},
170
177
  useTraces: false
171
178
  },
172
- {
173
- key: "mlcommons-local",
174
- name: "ML-Commons (Localhost)",
175
- endpoint: ENV_CONFIG.mlcommonsEndpoint,
176
- description: "Local OpenSearch ML-Commons conversational agent",
177
- connectorType: "agui-streaming",
178
- models: ["claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-3.5"],
179
- headers: buildMLCommonsHeaders(),
180
- useTraces: true
181
- },
182
- {
183
- key: "travel-planner",
184
- name: "Travel Planner",
185
- endpoint: ENV_CONFIG.travelPlannerEndpoint,
186
- description: "Multi-agent Travel Planner demo (requires OTel Demo running via Docker)",
187
- connectorType: "agui-streaming",
188
- models: ["claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-3.5"],
189
- headers: {},
190
- useTraces: true
191
- },
192
179
  {
193
180
  key: "claude-code",
194
181
  name: "Claude Code",
195
182
  endpoint: "claude",
196
183
  description: "Claude Code CLI agent (requires claude command installed)",
197
184
  connectorType: "claude-code",
198
- models: ["claude-sonnet-4"],
199
185
  headers: {},
200
186
  useTraces: ENV_CONFIG.claudeCodeTelemetryEnabled && !!ENV_CONFIG.otelExporterEndpoint,
201
187
  connectorConfig: { env: getClaudeCodeConnectorEnv() }
@@ -209,6 +195,48 @@ var DEFAULT_CONFIG = {
209
195
  context_window: 2e5,
210
196
  max_output_tokens: 4096
211
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
+ },
212
240
  "claude-sonnet-4.5": {
213
241
  model_id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
214
242
  display_name: "Claude Sonnet 4.5",
@@ -232,10 +260,24 @@ var DEFAULT_CONFIG = {
232
260
  },
233
261
  "gpt-4o": {
234
262
  model_id: "gpt-4o",
235
- display_name: "GPT-4o (via LiteLLM)",
236
- provider: "litellm",
263
+ display_name: "GPT-4o",
264
+ provider: "openai-compatible",
237
265
  context_window: 128e3,
238
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
239
281
  }
240
282
  },
241
283
  defaults: {
@@ -272,7 +314,6 @@ function toAgentConfig(userAgent) {
272
314
  endpoint: userAgent.endpoint,
273
315
  description: userAgent.description,
274
316
  enabled: userAgent.enabled ?? true,
275
- models: userAgent.models,
276
317
  headers: userAgent.headers ?? {},
277
318
  useTraces: userAgent.useTraces ?? false,
278
319
  connectorType: userAgent.connectorType,
@@ -1560,12 +1601,12 @@ var RESTConnector = class extends BaseConnector {
1560
1601
  };
1561
1602
  var restConnector = new RESTConnector();
1562
1603
 
1563
- // services/connectors/litellm/LiteLLMConnector.ts
1564
- var LiteLLMConnector = class extends BaseConnector {
1604
+ // services/connectors/openai-compatible/OpenAICompatibleConnector.ts
1605
+ var OpenAICompatibleConnector = class extends BaseConnector {
1565
1606
  constructor() {
1566
1607
  super(...arguments);
1567
- this.type = "litellm";
1568
- this.name = "LiteLLM / OpenAI-compatible";
1608
+ this.type = "openai-compatible";
1609
+ this.name = "OpenAI-compatible";
1569
1610
  this.supportsStreaming = false;
1570
1611
  }
1571
1612
  /**
@@ -1606,7 +1647,7 @@ var LiteLLMConnector = class extends BaseConnector {
1606
1647
  async execute(endpoint, request, auth, onProgress, onRawEvent) {
1607
1648
  const payload = request.payload || this.buildPayload(request);
1608
1649
  const headers = this.buildAuthHeaders(auth);
1609
- this.debug("Executing LiteLLM request");
1650
+ this.debug("Executing OpenAI-compatible request");
1610
1651
  this.debug("Endpoint:", endpoint);
1611
1652
  this.debug("Model:", payload.model);
1612
1653
  const response = await fetch(endpoint, {
@@ -1619,7 +1660,7 @@ var LiteLLMConnector = class extends BaseConnector {
1619
1660
  });
1620
1661
  if (!response.ok) {
1621
1662
  const errorText = await response.text();
1622
- throw new Error(`LiteLLM request failed: ${response.status} - ${errorText}`);
1663
+ throw new Error(`OpenAI-compatible request failed: ${response.status} - ${errorText}`);
1623
1664
  }
1624
1665
  const data = await response.json();
1625
1666
  onRawEvent?.(data);
@@ -1670,13 +1711,13 @@ var LiteLLMConnector = class extends BaseConnector {
1670
1711
  return steps;
1671
1712
  }
1672
1713
  };
1673
- var litellmConnector = new LiteLLMConnector();
1714
+ var openaiCompatibleConnector = new OpenAICompatibleConnector();
1674
1715
 
1675
1716
  // services/connectors/index.ts
1676
1717
  connectorRegistry.register(aguiStreamingConnector);
1677
1718
  connectorRegistry.register(mockConnector);
1678
1719
  connectorRegistry.register(restConnector);
1679
- connectorRegistry.register(litellmConnector);
1720
+ connectorRegistry.register(openaiCompatibleConnector);
1680
1721
  console.log("[Connectors] Browser-safe connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
1681
1722
 
1682
1723
  // services/connectors/subprocess/SubprocessConnector.ts
@@ -2043,7 +2084,39 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
2043
2084
  this.isInThinking = false;
2044
2085
  }
2045
2086
  /**
2046
- * 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
2047
2120
  */
2048
2121
  async execute(endpoint, request, auth, onProgress, onRawEvent) {
2049
2122
  this.debug("========== execute() STARTED ==========");
@@ -2051,11 +2124,55 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
2051
2124
  this.debug("Test case:", request.testCase.name);
2052
2125
  this.debug("Config:", this["config"]);
2053
2126
  this.resetState();
2054
- this.debug("State reset, calling super.execute()...");
2055
- const result = await super.execute(endpoint, request, auth, onProgress, onRawEvent);
2056
- this.debug("super.execute() returned with", result.trajectory.length, "steps");
2057
- this.debug("========== execute() COMPLETED ==========");
2058
- 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
+ }
2059
2176
  }
2060
2177
  /**
2061
2178
  * Health check - verify claude command exists
@@ -2309,6 +2426,12 @@ function createServerCleanup(result, isCI) {
2309
2426
  }
2310
2427
 
2311
2428
  // cli/utils/apiClient.ts
2429
+ var ServerError = class extends Error {
2430
+ constructor(message) {
2431
+ super(message);
2432
+ this.name = "ServerError";
2433
+ }
2434
+ };
2312
2435
  var ApiClient = class {
2313
2436
  constructor(baseUrl) {
2314
2437
  this.baseUrl = baseUrl;
@@ -2424,7 +2547,7 @@ var ApiClient = class {
2424
2547
  } else if (event.type === "completed" || event.type === "cancelled") {
2425
2548
  finalRun = event.run;
2426
2549
  } else if (event.type === "error") {
2427
- throw new Error(event.error);
2550
+ throw new ServerError(event.error);
2428
2551
  }
2429
2552
  } catch (e) {
2430
2553
  if (e instanceof SyntaxError) continue;
@@ -2434,6 +2557,9 @@ var ApiClient = class {
2434
2557
  }
2435
2558
  }
2436
2559
  } catch (streamError) {
2560
+ if (streamError instanceof ServerError) {
2561
+ throw streamError;
2562
+ }
2437
2563
  if (runId) {
2438
2564
  console.warn(`[ApiClient] SSE stream disconnected: ${streamError instanceof Error ? streamError.message : streamError}`);
2439
2565
  console.warn(`[ApiClient] Falling back to polling for run ${runId}...`);
@@ -2671,7 +2797,7 @@ var ApiClient = class {
2671
2797
  }
2672
2798
  const response = await this.listTestCasesWithMeta();
2673
2799
  return response.data.find(
2674
- (tc) => tc.name.toLowerCase() === identifier.toLowerCase()
2800
+ (tc) => tc.name?.toLowerCase() === identifier.toLowerCase()
2675
2801
  ) || null;
2676
2802
  }
2677
2803
  /**
@@ -2719,7 +2845,7 @@ var ApiClient = class {
2719
2845
  if (event.type === "completed") {
2720
2846
  result = event.report;
2721
2847
  } else if (event.type === "error") {
2722
- throw new Error(event.error);
2848
+ throw new ServerError(event.error);
2723
2849
  }
2724
2850
  } catch (e) {
2725
2851
  if (e instanceof SyntaxError) continue;
@@ -2769,12 +2895,53 @@ var ApiClient = class {
2769
2895
  }
2770
2896
  return res.json();
2771
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
+ }
2772
2920
  };
2773
2921
 
2774
- // 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
+ }
2775
2933
  function formatJson(data) {
2776
2934
  return JSON.stringify(data, null, 2);
2777
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
2778
2945
  function displayStorageWarnings(meta) {
2779
2946
  if (!meta.storageConfigured) {
2780
2947
  console.log(chalk.yellow("\n \u26A0 Storage not configured"));
@@ -2795,25 +2962,24 @@ async function listAgents(format, config) {
2795
2962
  console.log(formatJson(agents));
2796
2963
  return;
2797
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
+ }
2798
2976
  const table = new Table({
2799
- head: [
2800
- chalk.cyan("Key"),
2801
- chalk.cyan("Name"),
2802
- chalk.cyan("Connector"),
2803
- chalk.cyan("Models"),
2804
- chalk.cyan("Endpoint")
2805
- ],
2806
- colWidths: [15, 20, 15, 25, 40],
2977
+ head: headers.map((h) => chalk.cyan(h)),
2978
+ colWidths: [15, 20, 15, 50],
2807
2979
  wordWrap: true
2808
2980
  });
2809
- for (const agent of agents) {
2810
- table.push([
2811
- agent.key,
2812
- agent.name,
2813
- agent.connectorType || "agui-streaming",
2814
- agent.models.slice(0, 3).join(", ") + (agent.models.length > 3 ? "..." : ""),
2815
- agent.endpoint.substring(0, 37) + (agent.endpoint.length > 37 ? "..." : "")
2816
- ]);
2981
+ for (const row of rows) {
2982
+ table.push(row);
2817
2983
  }
2818
2984
  console.log(chalk.bold("\nAvailable Agents:\n"));
2819
2985
  console.log(table.toString());
@@ -2835,31 +3001,35 @@ async function listTestCases(format, config) {
2835
3001
  try {
2836
3002
  const client = new ApiClient(serverResult.baseUrl);
2837
3003
  const response = await client.listTestCasesWithMeta();
2838
- displayStorageWarnings(response.meta);
3004
+ if (format !== "markdown") {
3005
+ displayStorageWarnings(response.meta);
3006
+ }
2839
3007
  if (format === "json") {
2840
3008
  console.log(formatJson(response));
2841
3009
  return;
2842
3010
  }
2843
- const table = new Table({
2844
- head: [
2845
- chalk.cyan("ID"),
2846
- chalk.cyan("Name"),
2847
- chalk.cyan("Labels"),
2848
- chalk.cyan("Version"),
2849
- chalk.cyan("Source")
2850
- ],
2851
- colWidths: [25, 28, 28, 10, 10],
2852
- wordWrap: true
2853
- });
2854
- for (const tc of response.data) {
3011
+ const headers = ["ID", "Name", "Labels", "Version", "Source"];
3012
+ const rows = response.data.map((tc) => {
2855
3013
  const isDemo = tc.id.startsWith("demo-");
2856
- table.push([
3014
+ return [
2857
3015
  tc.id,
2858
3016
  tc.name,
2859
3017
  tc.labels?.slice(0, 3).join(", ") || "",
2860
3018
  `v${tc.currentVersion || 1}`,
2861
- isDemo ? chalk.gray("Sample") : chalk.green("Stored")
2862
- ]);
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);
2863
3033
  }
2864
3034
  console.log(chalk.bold("\nAvailable Test Cases:\n"));
2865
3035
  console.log(table.toString());
@@ -2888,31 +3058,35 @@ async function listBenchmarks(format, config) {
2888
3058
  try {
2889
3059
  const client = new ApiClient(serverResult.baseUrl);
2890
3060
  const response = await client.listBenchmarksWithMeta();
2891
- displayStorageWarnings(response.meta);
3061
+ if (format !== "markdown") {
3062
+ displayStorageWarnings(response.meta);
3063
+ }
2892
3064
  if (format === "json") {
2893
3065
  console.log(formatJson(response));
2894
3066
  return;
2895
3067
  }
2896
- const table = new Table({
2897
- head: [
2898
- chalk.cyan("ID"),
2899
- chalk.cyan("Name"),
2900
- chalk.cyan("Test Cases"),
2901
- chalk.cyan("Created"),
2902
- chalk.cyan("Source")
2903
- ],
2904
- colWidths: [28, 28, 12, 22, 10],
2905
- wordWrap: true
2906
- });
2907
- for (const b of response.data) {
3068
+ const headers = ["ID", "Name", "Test Cases", "Created", "Source"];
3069
+ const rows = response.data.map((b) => {
2908
3070
  const isDemo = b.id.startsWith("demo-");
2909
- table.push([
3071
+ return [
2910
3072
  b.id,
2911
3073
  b.name,
2912
3074
  b.testCaseIds.length.toString(),
2913
3075
  new Date(b.createdAt).toLocaleDateString(),
2914
- isDemo ? chalk.gray("Sample") : chalk.green("Stored")
2915
- ]);
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);
2916
3090
  }
2917
3091
  console.log(chalk.bold("\nAvailable Benchmarks:\n"));
2918
3092
  console.log(table.toString());
@@ -2949,19 +3123,25 @@ function listConnectors(format) {
2949
3123
  console.log(formatJson(connectors));
2950
3124
  return;
2951
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
+ }
2952
3136
  const table = new Table({
2953
- head: [
2954
- chalk.cyan("Type"),
2955
- chalk.cyan("Name"),
2956
- chalk.cyan("Streaming")
2957
- ],
3137
+ head: headers.map((h) => chalk.cyan(h)),
2958
3138
  colWidths: [20, 25, 12]
2959
3139
  });
2960
- for (const c of connectors) {
3140
+ for (const row of rows) {
2961
3141
  table.push([
2962
- c.type,
2963
- c.name,
2964
- c.streaming ? chalk.green("Yes") : chalk.gray("No")
3142
+ row[0],
3143
+ row[1],
3144
+ row[2] === "Yes" ? chalk.green("Yes") : chalk.gray("No")
2965
3145
  ]);
2966
3146
  }
2967
3147
  console.log(chalk.bold("\nRegistered Connectors:\n"));
@@ -2980,23 +3160,24 @@ async function listModels(format, config) {
2980
3160
  console.log(formatJson(models));
2981
3161
  return;
2982
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
+ }
2983
3174
  const table = new Table({
2984
- head: [
2985
- chalk.cyan("Key"),
2986
- chalk.cyan("Display Name"),
2987
- chalk.cyan("Provider"),
2988
- chalk.cyan("Context")
2989
- ],
3175
+ head: headers.map((h) => chalk.cyan(h)),
2990
3176
  colWidths: [25, 30, 12, 12],
2991
3177
  wordWrap: true
2992
3178
  });
2993
- for (const m of models) {
2994
- table.push([
2995
- m.key,
2996
- m.display_name || m.key,
2997
- m.provider || "bedrock",
2998
- m.context_window ? `${Math.round(m.context_window / 1e3)}k` : "-"
2999
- ]);
3179
+ for (const row of rows) {
3180
+ table.push(row);
3000
3181
  }
3001
3182
  console.log(chalk.bold("\nAvailable Models:\n"));
3002
3183
  console.log(table.toString());
@@ -3013,8 +3194,8 @@ async function listModels(format, config) {
3013
3194
  }
3014
3195
  }
3015
3196
  function createListCommand() {
3016
- 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) => {
3017
- 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);
3018
3199
  const config = await loadConfig();
3019
3200
  for (const connector of config.connectors) {
3020
3201
  connectorRegistry.register(connector);
@@ -3058,8 +3239,8 @@ function findAgent(identifier, config) {
3058
3239
  (a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
3059
3240
  );
3060
3241
  }
3061
- function getDefaultModel(agent) {
3062
- return agent.models[0] || "claude-sonnet";
3242
+ function getDefaultModel(config) {
3243
+ return Object.keys(config.models)[0] || "claude-sonnet";
3063
3244
  }
3064
3245
  async function commandExists(command) {
3065
3246
  const { execSync: execSync2 } = await import("child_process");
@@ -3113,26 +3294,36 @@ async function runForAgent(client, testCaseId, agent, modelId, verbose) {
3113
3294
  throw error;
3114
3295
  }
3115
3296
  }
3116
- 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
+ }
3117
3320
  const table = new Table2({
3118
- head: [
3119
- chalk2.cyan("Agent"),
3120
- chalk2.cyan("Status"),
3121
- chalk2.cyan("Accuracy"),
3122
- chalk2.cyan("Steps"),
3123
- chalk2.cyan("Report ID")
3124
- ],
3321
+ head: headers.map((h) => chalk2.cyan(h)),
3125
3322
  colWidths: [20, 12, 12, 10, 30]
3126
3323
  });
3127
3324
  for (const r of results) {
3128
3325
  if (!r.report) {
3129
- table.push([
3130
- r.agent.name,
3131
- chalk2.red("ERROR"),
3132
- "-",
3133
- "-",
3134
- "-"
3135
- ]);
3326
+ table.push([r.agent.name, chalk2.red("ERROR"), "-", "-", "-"]);
3136
3327
  continue;
3137
3328
  }
3138
3329
  const statusStr = r.report.passFailStatus === "passed" ? chalk2.green("PASSED") : r.report.passFailStatus === "failed" ? chalk2.red("FAILED") : chalk2.yellow(r.report.status);
@@ -3148,7 +3339,7 @@ function displayTableResults(results) {
3148
3339
  console.log(table.toString());
3149
3340
  }
3150
3341
  function createRunCommand() {
3151
- 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) => {
3152
3343
  console.log(chalk2.bold("\nAgent Health - Test Case Runner\n"));
3153
3344
  const config = await loadConfig();
3154
3345
  for (const connector of config.connectors) {
@@ -3186,7 +3377,7 @@ function createRunCommand() {
3186
3377
  console.log("");
3187
3378
  const results = [];
3188
3379
  for (const agent of agents) {
3189
- const modelId = options.model || getDefaultModel(agent);
3380
+ const modelId = options.model || getDefaultModel(config);
3190
3381
  const validationError = await validateAgentRequirements(agent);
3191
3382
  if (validationError) {
3192
3383
  console.error(chalk2.red(` Error: ${validationError}`));
@@ -3197,17 +3388,27 @@ function createRunCommand() {
3197
3388
  const report = await runForAgent(client, testCase.id, agent, modelId, options.verbose || false);
3198
3389
  results.push({ agent, report });
3199
3390
  } catch (error) {
3200
- 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
+ }
3201
3401
  results.push({ agent, report: null });
3202
3402
  }
3203
3403
  }
3204
- if (options.output === "json") {
3205
- console.log(JSON.stringify(results.map((r) => ({
3404
+ const outputFormat = parseOutputFormat(options.output);
3405
+ if (outputFormat === "json") {
3406
+ console.log(formatJson(results.map((r) => ({
3206
3407
  agent: { key: r.agent.key, name: r.agent.name },
3207
3408
  report: r.report
3208
- })), null, 2));
3409
+ }))));
3209
3410
  } else {
3210
- displayTableResults(results);
3411
+ displayResults(results, outputFormat);
3211
3412
  }
3212
3413
  } catch (error) {
3213
3414
  console.error(chalk2.red(`
@@ -3354,8 +3555,8 @@ function findAgent2(identifier, config) {
3354
3555
  (a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
3355
3556
  );
3356
3557
  }
3357
- function getDefaultModel2(agent) {
3358
- return agent.models[0] || "claude-sonnet";
3558
+ function getDefaultModel2(config) {
3559
+ return Object.keys(config.models)[0] || "claude-sonnet";
3359
3560
  }
3360
3561
  function isFilePath(value) {
3361
3562
  return value.toLowerCase().endsWith(".json");
@@ -3391,7 +3592,7 @@ async function fetchReportsForRun(api, run) {
3391
3592
  );
3392
3593
  return reportsMap;
3393
3594
  }
3394
- async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3595
+ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose, concurrency) {
3395
3596
  const results = {
3396
3597
  agent,
3397
3598
  passed: 0,
@@ -3406,7 +3607,8 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3406
3607
  {
3407
3608
  name: `CLI Run - ${agent.name}`,
3408
3609
  agentKey: agent.key,
3409
- modelId
3610
+ modelId,
3611
+ ...concurrency && concurrency > 1 ? { concurrency } : {}
3410
3612
  },
3411
3613
  (event) => {
3412
3614
  if (event.type === "started") {
@@ -3415,9 +3617,13 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3415
3617
  const current = event.currentTestCaseIndex + 1;
3416
3618
  const testCaseName = event.currentTestCase?.name || `Test ${current}`;
3417
3619
  spinner.text = `${agent.name}: ${testCaseName} (${current}/${totalTestCases})`;
3418
- if (verbose && event.result) {
3620
+ if (event.result) {
3419
3621
  const status = event.result.status === "completed" ? chalk3.green("\u2713") : chalk3.red("\u2717");
3420
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
+ }
3421
3627
  }
3422
3628
  }
3423
3629
  }
@@ -3444,6 +3650,7 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3444
3650
  }
3445
3651
  } catch (error) {
3446
3652
  const errorMessage = error instanceof Error ? error.message : String(error);
3653
+ const isServerError = error instanceof ServerError;
3447
3654
  if (startedRunId) {
3448
3655
  results.runId = startedRunId;
3449
3656
  try {
@@ -3472,29 +3679,71 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3472
3679
  }
3473
3680
  return results;
3474
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
+ }
3475
3690
  }
3476
3691
  } catch {
3477
3692
  }
3478
3693
  }
3479
- const isStreamError = errorMessage.includes("terminated") || errorMessage.includes("network") || errorMessage.includes("stream") || errorMessage.includes("aborted");
3480
- if (isStreamError && startedRunId) {
3481
- spinner.warn(`${agent.name}: ${chalk3.yellow("Stream disconnected")} - server may still be processing`);
3482
- console.log(chalk3.gray(` Check status: Use the UI to monitor progress`));
3483
- } else {
3694
+ if (isServerError) {
3484
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`));
3485
3720
  }
3486
3721
  }
3487
3722
  return results;
3488
3723
  }
3489
- 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
+ }
3490
3745
  const table = new Table3({
3491
- head: [
3492
- chalk3.cyan("Agent"),
3493
- chalk3.cyan("Passed"),
3494
- chalk3.cyan("Failed"),
3495
- chalk3.cyan("Pass Rate"),
3496
- chalk3.cyan("Run ID")
3497
- ],
3746
+ head: headers.map((h) => chalk3.cyan(h)),
3498
3747
  colWidths: [25, 10, 10, 12, 35]
3499
3748
  });
3500
3749
  for (const results of allResults) {
@@ -3565,7 +3814,7 @@ function createBenchmarkCommand() {
3565
3814
  "Agent key (can be specified multiple times)",
3566
3815
  (val, arr) => [...arr, val],
3567
3816
  []
3568
- ).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 file").option("--format <type>", "Report format for --export: json (default), html, pdf", "json").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) => {
3569
3818
  console.log(chalk3.bold("\nAgent Health - Benchmark Runner\n"));
3570
3819
  const config = await loadConfig();
3571
3820
  const serverConfig = { ...DEFAULT_SERVER_CONFIG, ...config.server };
@@ -3699,6 +3948,9 @@ function createBenchmarkCommand() {
3699
3948
  console.log(chalk3.gray(` - ${a.name} (${a.key})`));
3700
3949
  }
3701
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("");
3702
3954
  process.exit(1);
3703
3955
  }
3704
3956
  agents.push(agent);
@@ -3706,19 +3958,25 @@ function createBenchmarkCommand() {
3706
3958
  console.log(chalk3.gray(` Agents: ${agents.map((a) => a.name).join(", ")}`));
3707
3959
  }
3708
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
+ }
3709
3965
  const allResults = [];
3710
3966
  for (const agent of agents) {
3711
- const modelId = options.model || getDefaultModel2(agent);
3967
+ const modelId = options.model || getDefaultModel2(config);
3712
3968
  const results = await runBenchmarkForAgent(
3713
3969
  api,
3714
3970
  agent,
3715
3971
  modelId,
3716
3972
  benchmark,
3717
- options.verbose || false
3973
+ options.verbose || false,
3974
+ concurrency
3718
3975
  );
3719
3976
  allResults.push(results);
3720
3977
  }
3721
- if (options.output === "json") {
3978
+ const outputFormat = parseOutputFormat(options.output);
3979
+ if (outputFormat === "json") {
3722
3980
  const jsonOutput = allResults.map((r) => ({
3723
3981
  agent: { key: r.agent.key, name: r.agent.name },
3724
3982
  runId: r.run?.id || r.runId,
@@ -3727,9 +3985,9 @@ function createBenchmarkCommand() {
3727
3985
  passRate: benchmark.testCaseIds.length > 0 ? r.passed / benchmark.testCaseIds.length * 100 : 0,
3728
3986
  results: r.run?.results
3729
3987
  }));
3730
- console.log(JSON.stringify(jsonOutput, null, 2));
3988
+ console.log(formatJson(jsonOutput));
3731
3989
  } else {
3732
- displaySummaryTable(allResults, benchmark.testCaseIds.length);
3990
+ displaySummary(allResults, benchmark.testCaseIds.length, outputFormat);
3733
3991
  }
3734
3992
  if (options.export) {
3735
3993
  await exportResults(benchmark, allResults, options.export, options.format, serverResult.baseUrl);
@@ -4045,7 +4303,7 @@ function checkOpenSearchObservability() {
4045
4303
  ]
4046
4304
  };
4047
4305
  }
4048
- function displayResults(results) {
4306
+ function displayResults2(results) {
4049
4307
  console.log(chalk6.bold("\n Configuration Check\n"));
4050
4308
  for (const result of results) {
4051
4309
  const icon = {
@@ -4096,7 +4354,7 @@ function createDoctorCommand() {
4096
4354
  if (options.output === "json") {
4097
4355
  console.log(JSON.stringify(results, null, 2));
4098
4356
  } else {
4099
- displayResults(results);
4357
+ displayResults2(results);
4100
4358
  }
4101
4359
  });
4102
4360
  return command;
@@ -4127,7 +4385,6 @@ export default defineConfig({
4127
4385
  username: process.env.OPENSEARCH_USER || 'admin',
4128
4386
  password: process.env.OPENSEARCH_PASS || 'admin',
4129
4387
  },
4130
- models: ['claude-sonnet'],
4131
4388
  },
4132
4389
 
4133
4390
  // Claude Code CLI agent (optional)
@@ -4144,7 +4401,6 @@ export default defineConfig({
4144
4401
  },
4145
4402
  }),
4146
4403
  endpoint: 'claude', // Command name
4147
- models: ['claude-sonnet-4'],
4148
4404
  },
4149
4405
  */
4150
4406
  ],
@@ -4426,6 +4682,222 @@ function createMigrateCommand() {
4426
4682
  return command;
4427
4683
  }
4428
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
+
4429
4901
  // cli/index.ts
4430
4902
  var __filename3 = fileURLToPath3(import.meta.url);
4431
4903
  var __dirname3 = dirname3(__filename3);
@@ -4439,59 +4911,112 @@ try {
4439
4911
  function loadEnvFile(envPath) {
4440
4912
  const absolutePath = resolve4(process.cwd(), envPath);
4441
4913
  if (!existsSync5(absolutePath)) {
4442
- console.error(chalk9.red(`
4914
+ console.error(chalk10.red(`
4443
4915
  Error: Environment file not found: ${absolutePath}
4444
4916
  `));
4445
4917
  process.exit(1);
4446
4918
  }
4447
4919
  const result = loadDotenv({ path: absolutePath });
4448
4920
  if (result.error) {
4449
- console.error(chalk9.red(`
4921
+ console.error(chalk10.red(`
4450
4922
  Error loading environment file: ${result.error.message}
4451
4923
  `));
4452
4924
  process.exit(1);
4453
4925
  }
4454
- console.log(chalk9.gray(` Loaded environment from: ${envPath}`));
4926
+ console.log(chalk10.gray(` Loaded environment from: ${envPath}`));
4455
4927
  }
4456
4928
  var defaultEnvPath = resolve4(process.cwd(), ".env");
4457
4929
  if (existsSync5(defaultEnvPath)) {
4458
4930
  loadDotenv({ path: defaultEnvPath });
4459
4931
  }
4460
- var program = new Command9();
4461
- 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
+ `);
4462
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");
4463
4987
  program.action(async (options) => {
4464
- console.log(chalk9.cyan.bold(`
4988
+ console.log(chalk10.cyan.bold(`
4465
4989
  Agent Health v${version} - AI Agent Evaluation Framework
4466
4990
  `));
4467
- console.log(chalk9.gray(` Working directory: ${process.cwd()}`));
4468
- console.log(chalk9.gray(` Package directory: ${__dirname3}`));
4991
+ console.log(chalk10.gray(` Working directory: ${process.cwd()}`));
4992
+ console.log(chalk10.gray(` Package directory: ${__dirname3}`));
4469
4993
  if (options.envFile) {
4470
4994
  loadEnvFile(options.envFile);
4471
4995
  } else if (existsSync5(defaultEnvPath)) {
4472
- console.log(chalk9.gray(" Auto-loaded .env from current directory"));
4996
+ console.log(chalk10.gray(" Auto-loaded .env from current directory"));
4473
4997
  }
4474
4998
  const port = parseInt(options.port, 10);
4475
4999
  const spinner = ora5("Starting server...").start();
4476
5000
  try {
4477
5001
  await startServer({ port });
4478
5002
  spinner.succeed("Server started");
4479
- console.log(chalk9.gray("\n Configuration:"));
4480
- console.log(chalk9.gray(` Storage: Sample data (configure OpenSearch for persistence)`));
4481
- console.log(chalk9.gray(` Agent: Select in UI (Demo Agent for mock, real agents require endpoints)`));
4482
- console.log(chalk9.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)
4483
5007
  `));
4484
5008
  const url = `http://localhost:${port}`;
4485
- console.log(chalk9.green(` Server running at ${chalk9.bold(url)}
5009
+ console.log(chalk10.green(` Server running at ${chalk10.bold(url)}
4486
5010
  `));
5011
+ console.log(chalk10.green(` Demo data loaded`));
4487
5012
  if (options.browser !== false) {
4488
- console.log(chalk9.gray(" Opening browser..."));
5013
+ console.log(chalk10.gray(" Opening browser..."));
4489
5014
  await open(url);
4490
5015
  }
4491
- console.log(chalk9.gray(" Press Ctrl+C to stop\n"));
5016
+ console.log(chalk10.gray(" Press Ctrl+C to stop\n"));
4492
5017
  } catch (error) {
4493
5018
  spinner.fail("Failed to start server");
4494
- console.error(chalk9.red(`
5019
+ console.error(chalk10.red(`
4495
5020
  Error: ${error instanceof Error ? error.message : error}
4496
5021
  `));
4497
5022
  process.exit(1);
@@ -4505,8 +5030,9 @@ program.addCommand(createReportCommand());
4505
5030
  program.addCommand(createDoctorCommand());
4506
5031
  program.addCommand(createInitCommand());
4507
5032
  program.addCommand(createMigrateCommand());
5033
+ program.addCommand(createCompareServicesCommand());
4508
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) => {
4509
- console.log(chalk9.cyan.bold(`
5035
+ console.log(chalk10.cyan.bold(`
4510
5036
  Agent Health v${version} - AI Agent Evaluation Framework
4511
5037
  `));
4512
5038
  const port = parseInt(options.port, 10);
@@ -4515,16 +5041,16 @@ program.command("serve").description("Start the Agent Health server (same as def
4515
5041
  await startServer({ port });
4516
5042
  spinner.succeed("Server started");
4517
5043
  const url = `http://localhost:${port}`;
4518
- console.log(chalk9.green(` Server running at ${chalk9.bold(url)}
5044
+ console.log(chalk10.green(` Server running at ${chalk10.bold(url)}
4519
5045
  `));
4520
5046
  if (options.browser !== false) {
4521
- console.log(chalk9.gray(" Opening browser..."));
5047
+ console.log(chalk10.gray(" Opening browser..."));
4522
5048
  await open(url);
4523
5049
  }
4524
- console.log(chalk9.gray(" Press Ctrl+C to stop\n"));
5050
+ console.log(chalk10.gray(" Press Ctrl+C to stop\n"));
4525
5051
  } catch (error) {
4526
5052
  spinner.fail("Failed to start server");
4527
- console.error(chalk9.red(`
5053
+ console.error(chalk10.red(`
4528
5054
  Error: ${error instanceof Error ? error.message : error}
4529
5055
  `));
4530
5056
  process.exit(1);
@@ -4533,15 +5059,15 @@ program.command("serve").description("Start the Agent Health server (same as def
4533
5059
  program.on("command:*", (operands) => {
4534
5060
  const unknownCommand = operands[0];
4535
5061
  const availableCommands = program.commands.map((cmd) => cmd.name());
4536
- console.error(chalk9.red(`
5062
+ console.error(chalk10.red(`
4537
5063
  Error: Unknown command '${unknownCommand}'`));
4538
5064
  console.log("");
4539
- console.log(chalk9.cyan(" Available commands:"));
5065
+ console.log(chalk10.cyan(" Available commands:"));
4540
5066
  for (const cmd of availableCommands) {
4541
- console.log(chalk9.gray(` - ${cmd}`));
5067
+ console.log(chalk10.gray(` - ${cmd}`));
4542
5068
  }
4543
5069
  console.log("");
4544
- console.log(chalk9.gray(` Run ${chalk9.cyan("agent-health --help")} for usage information.
5070
+ console.log(chalk10.gray(` Run ${chalk10.cyan("agent-health --help")} for usage information.
4545
5071
  `));
4546
5072
  process.exitCode = 1;
4547
5073
  });