@opensearch-project/agent-health 0.1.1 → 0.3.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 Command11 } from "commander";
5
+ import chalk11 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";
@@ -28,6 +28,8 @@ function findPackageRoot() {
28
28
  }
29
29
  async function startServer(options) {
30
30
  process.env.VITE_BACKEND_PORT = String(options.port);
31
+ if (options.headless) process.env.AGENT_HEALTH_HEADLESS = "1";
32
+ if (options.apiKey) process.env.AGENT_HEALTH_API_KEY = options.apiKey;
31
33
  const packageRoot = findPackageRoot();
32
34
  const serverPath = join(packageRoot, "server", "dist", "app.js");
33
35
  const { createApp } = await import(serverPath);
@@ -96,9 +98,9 @@ var ENV_CONFIG = {
96
98
  mlcommonsHeaderAwsSessionToken: getEnvVar("MLCOMMONS_HEADER_AWS_SESSION_TOKEN", ""),
97
99
  // Travel Planner multi-agent endpoint (OTel Demo in Docker)
98
100
  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"),
101
+ // OpenAI-compatible (optional - for OpenAI-compatible judge/agent endpoints)
102
+ openaiCompatibleApiKey: getEnvVar("OPENAI_COMPATIBLE_API_KEY", ""),
103
+ openaiCompatibleEndpoint: getEnvVar("OPENAI_COMPATIBLE_ENDPOINT", "http://localhost:4000/v1/chat/completions"),
102
104
  // Claude Code Telemetry (optional - for OTEL traces from Claude Code)
103
105
  claudeCodeTelemetryEnabled: getEnvVar("CLAUDE_CODE_TELEMETRY_ENABLED", "false") === "true",
104
106
  otelExporterEndpoint: getEnvVar("OTEL_EXPORTER_OTLP_ENDPOINT", ""),
@@ -106,39 +108,47 @@ var ENV_CONFIG = {
106
108
  otelExporterProtocol: getEnvVar("OTEL_EXPORTER_OTLP_PROTOCOL", ""),
107
109
  otelExporterHeaders: getEnvVar("OTEL_EXPORTER_OTLP_HEADERS", "")
108
110
  };
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
111
 
136
112
  // lib/constants.ts
113
+ var CONNECTOR_TYPE_INFO = {
114
+ "agui-streaming": {
115
+ label: "AG-UI Streaming",
116
+ description: "AG-UI protocol over SSE. Use for ML-Commons and AG-UI compatible agents.",
117
+ serverOnly: false
118
+ },
119
+ "rest": {
120
+ label: "REST",
121
+ description: "Standard HTTP POST. Agent receives JSON, returns JSON. No streaming.",
122
+ serverOnly: false
123
+ },
124
+ "openai-compatible": {
125
+ label: "OpenAI Compatible",
126
+ description: "OpenAI chat completions format (POST /v1/chat/completions). Works with LiteLLM, Ollama, vLLM.",
127
+ serverOnly: false
128
+ },
129
+ "subprocess": {
130
+ label: "Subprocess",
131
+ description: "Runs a CLI command as a child process. Server-only \u2014 use the CLI or benchmark runner.",
132
+ serverOnly: true
133
+ },
134
+ "claude-code": {
135
+ label: "Claude Code",
136
+ description: "Invokes the Claude Code CLI. Server-only \u2014 use the CLI or benchmark runner.",
137
+ serverOnly: true
138
+ },
139
+ "mock": {
140
+ label: "Mock",
141
+ description: "Built-in demo agent for testing. No real endpoint needed.",
142
+ serverOnly: false
143
+ }
144
+ };
145
+ var VALID_CONNECTOR_TYPES = Object.keys(CONNECTOR_TYPE_INFO);
146
+ var BROWSER_SAFE_CONNECTORS = Object.entries(CONNECTOR_TYPE_INFO).filter(([, info]) => !info.serverOnly).map(([type]) => type);
137
147
  function getClaudeCodeConnectorEnv() {
138
148
  const env = {
139
- AWS_PROFILE: process.env.AWS_PROFILE || "Bedrock",
149
+ AWS_PROFILE: ENV_CONFIG.awsProfile || "Bedrock",
140
150
  CLAUDE_CODE_USE_BEDROCK: "1",
141
- AWS_REGION: process.env.AWS_REGION || "us-west-2",
151
+ AWS_REGION: ENV_CONFIG.awsRegion || "us-west-2",
142
152
  DISABLE_PROMPT_CACHING: "1",
143
153
  DISABLE_ERROR_REPORTING: "1"
144
154
  };
@@ -165,37 +175,15 @@ var DEFAULT_CONFIG = {
165
175
  endpoint: "mock://demo",
166
176
  description: "Mock agent for testing (simulated responses)",
167
177
  connectorType: "mock",
168
- models: ["demo-model"],
169
178
  headers: {},
170
179
  useTraces: false
171
180
  },
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
181
  {
193
182
  key: "claude-code",
194
183
  name: "Claude Code",
195
184
  endpoint: "claude",
196
185
  description: "Claude Code CLI agent (requires claude command installed)",
197
186
  connectorType: "claude-code",
198
- models: ["claude-sonnet-4"],
199
187
  headers: {},
200
188
  useTraces: ENV_CONFIG.claudeCodeTelemetryEnabled && !!ENV_CONFIG.otelExporterEndpoint,
201
189
  connectorConfig: { env: getClaudeCodeConnectorEnv() }
@@ -209,6 +197,48 @@ var DEFAULT_CONFIG = {
209
197
  context_window: 2e5,
210
198
  max_output_tokens: 4096
211
199
  },
200
+ "claude-opus-4.6": {
201
+ model_id: "us.anthropic.claude-opus-4-6-v1",
202
+ display_name: "Claude Opus 4.6",
203
+ provider: "bedrock",
204
+ context_window: 2e5,
205
+ max_output_tokens: 128e3
206
+ },
207
+ "claude-sonnet-4.6": {
208
+ model_id: "us.anthropic.claude-sonnet-4-6",
209
+ display_name: "Claude Sonnet 4.6",
210
+ provider: "bedrock",
211
+ context_window: 2e5,
212
+ max_output_tokens: 64e3
213
+ },
214
+ "claude-haiku-4.5": {
215
+ model_id: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
216
+ display_name: "Claude Haiku 4.5",
217
+ provider: "bedrock",
218
+ context_window: 2e5,
219
+ max_output_tokens: 64e3
220
+ },
221
+ "claude-opus-4.5": {
222
+ model_id: "us.anthropic.claude-opus-4-5-20251101-v1:0",
223
+ display_name: "Claude Opus 4.5",
224
+ provider: "bedrock",
225
+ context_window: 2e5,
226
+ max_output_tokens: 64e3
227
+ },
228
+ "claude-opus-4.1": {
229
+ model_id: "us.anthropic.claude-opus-4-1-20250805-v1:0",
230
+ display_name: "Claude Opus 4.1",
231
+ provider: "bedrock",
232
+ context_window: 2e5,
233
+ max_output_tokens: 32e3
234
+ },
235
+ "claude-opus-4": {
236
+ model_id: "us.anthropic.claude-opus-4-20250514-v1:0",
237
+ display_name: "Claude Opus 4",
238
+ provider: "bedrock",
239
+ context_window: 2e5,
240
+ max_output_tokens: 32e3
241
+ },
212
242
  "claude-sonnet-4.5": {
213
243
  model_id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
214
244
  display_name: "Claude Sonnet 4.5",
@@ -232,10 +262,24 @@ var DEFAULT_CONFIG = {
232
262
  },
233
263
  "gpt-4o": {
234
264
  model_id: "gpt-4o",
235
- display_name: "GPT-4o (via LiteLLM)",
236
- provider: "litellm",
265
+ display_name: "GPT-4o",
266
+ provider: "openai-compatible",
237
267
  context_window: 128e3,
238
268
  max_output_tokens: 4096
269
+ },
270
+ "deepseek-r1:8b": {
271
+ model_id: "deepseek-r1:8b",
272
+ display_name: "DeepSeek R1 8B (Ollama)",
273
+ provider: "openai-compatible",
274
+ context_window: 128e3,
275
+ max_output_tokens: 8192
276
+ },
277
+ "gemma3:12b": {
278
+ model_id: "gemma3:12b",
279
+ display_name: "Gemma 3 12B (Ollama)",
280
+ provider: "openai-compatible",
281
+ context_window: 128e3,
282
+ max_output_tokens: 8192
239
283
  }
240
284
  },
241
285
  defaults: {
@@ -272,7 +316,6 @@ function toAgentConfig(userAgent) {
272
316
  endpoint: userAgent.endpoint,
273
317
  description: userAgent.description,
274
318
  enabled: userAgent.enabled ?? true,
275
- models: userAgent.models,
276
319
  headers: userAgent.headers ?? {},
277
320
  useTraces: userAgent.useTraces ?? false,
278
321
  connectorType: userAgent.connectorType,
@@ -1560,12 +1603,12 @@ var RESTConnector = class extends BaseConnector {
1560
1603
  };
1561
1604
  var restConnector = new RESTConnector();
1562
1605
 
1563
- // services/connectors/litellm/LiteLLMConnector.ts
1564
- var LiteLLMConnector = class extends BaseConnector {
1606
+ // services/connectors/openai-compatible/OpenAICompatibleConnector.ts
1607
+ var OpenAICompatibleConnector = class extends BaseConnector {
1565
1608
  constructor() {
1566
1609
  super(...arguments);
1567
- this.type = "litellm";
1568
- this.name = "LiteLLM / OpenAI-compatible";
1610
+ this.type = "openai-compatible";
1611
+ this.name = "OpenAI-compatible";
1569
1612
  this.supportsStreaming = false;
1570
1613
  }
1571
1614
  /**
@@ -1606,7 +1649,7 @@ var LiteLLMConnector = class extends BaseConnector {
1606
1649
  async execute(endpoint, request, auth, onProgress, onRawEvent) {
1607
1650
  const payload = request.payload || this.buildPayload(request);
1608
1651
  const headers = this.buildAuthHeaders(auth);
1609
- this.debug("Executing LiteLLM request");
1652
+ this.debug("Executing OpenAI-compatible request");
1610
1653
  this.debug("Endpoint:", endpoint);
1611
1654
  this.debug("Model:", payload.model);
1612
1655
  const response = await fetch(endpoint, {
@@ -1619,7 +1662,7 @@ var LiteLLMConnector = class extends BaseConnector {
1619
1662
  });
1620
1663
  if (!response.ok) {
1621
1664
  const errorText = await response.text();
1622
- throw new Error(`LiteLLM request failed: ${response.status} - ${errorText}`);
1665
+ throw new Error(`OpenAI-compatible request failed: ${response.status} - ${errorText}`);
1623
1666
  }
1624
1667
  const data = await response.json();
1625
1668
  onRawEvent?.(data);
@@ -1670,13 +1713,13 @@ var LiteLLMConnector = class extends BaseConnector {
1670
1713
  return steps;
1671
1714
  }
1672
1715
  };
1673
- var litellmConnector = new LiteLLMConnector();
1716
+ var openaiCompatibleConnector = new OpenAICompatibleConnector();
1674
1717
 
1675
1718
  // services/connectors/index.ts
1676
1719
  connectorRegistry.register(aguiStreamingConnector);
1677
1720
  connectorRegistry.register(mockConnector);
1678
1721
  connectorRegistry.register(restConnector);
1679
- connectorRegistry.register(litellmConnector);
1722
+ connectorRegistry.register(openaiCompatibleConnector);
1680
1723
  console.log("[Connectors] Browser-safe connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
1681
1724
 
1682
1725
  // services/connectors/subprocess/SubprocessConnector.ts
@@ -2043,7 +2086,39 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
2043
2086
  this.isInThinking = false;
2044
2087
  }
2045
2088
  /**
2046
- * Override execute to reset state
2089
+ * Build CLI args from ClaudeCodeConnectorConfig
2090
+ */
2091
+ buildConfigArgs(config) {
2092
+ const args = [];
2093
+ if (config.dangerouslySkipPermissions) {
2094
+ args.push("--dangerously-skip-permissions");
2095
+ }
2096
+ if (config.systemPrompt) {
2097
+ args.push("--system-prompt", config.systemPrompt);
2098
+ } else if (config.appendSystemPrompt) {
2099
+ args.push("--append-system-prompt", config.appendSystemPrompt);
2100
+ }
2101
+ if (config.allowedTools?.length) {
2102
+ args.push("--allowed-tools", ...config.allowedTools);
2103
+ }
2104
+ if (config.disallowedTools?.length) {
2105
+ args.push("--disallowed-tools", ...config.disallowedTools);
2106
+ }
2107
+ if (config.mcpConfigPath) {
2108
+ args.push("--mcp-config", config.mcpConfigPath);
2109
+ } else if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
2110
+ args.push("--mcp-config", JSON.stringify({ mcpServers: config.mcpServers }));
2111
+ }
2112
+ if (config.strictMcpConfig) {
2113
+ args.push("--strict-mcp-config");
2114
+ }
2115
+ if (config.additionalArgs) {
2116
+ args.push(...config.additionalArgs);
2117
+ }
2118
+ return args;
2119
+ }
2120
+ /**
2121
+ * Override execute to reset state and apply connectorConfig
2047
2122
  */
2048
2123
  async execute(endpoint, request, auth, onProgress, onRawEvent) {
2049
2124
  this.debug("========== execute() STARTED ==========");
@@ -2051,11 +2126,55 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
2051
2126
  this.debug("Test case:", request.testCase.name);
2052
2127
  this.debug("Config:", this["config"]);
2053
2128
  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;
2129
+ const originalEnv = this.config.env ? structuredClone(this.config.env) : {};
2130
+ const originalArgs = this.config.args ? [...this.config.args] : [];
2131
+ const originalInputMode = this.config.inputMode;
2132
+ const originalTimeout = this.config.timeout;
2133
+ const originalWorkingDir = this.config.workingDir;
2134
+ const ccConfig = request.connectorConfig;
2135
+ if (ccConfig) {
2136
+ this.debug("Applying connectorConfig:", Object.keys(ccConfig));
2137
+ if (ccConfig.env) {
2138
+ this.config.env = { ...this.config.env, ...ccConfig.env };
2139
+ }
2140
+ if (ccConfig.usePromptArg) {
2141
+ this.config.inputMode = "arg";
2142
+ }
2143
+ if (ccConfig.timeout !== void 0) {
2144
+ this.config.timeout = ccConfig.timeout;
2145
+ }
2146
+ if (ccConfig.workingDir) {
2147
+ this.config.workingDir = ccConfig.workingDir;
2148
+ }
2149
+ }
2150
+ if (this.config.env?.CLAUDE_CODE_USE_BEDROCK === "1") {
2151
+ this.config.env = { ...this.config.env, ANTHROPIC_API_KEY: "" };
2152
+ this.debug("Bedrock mode: cleared ANTHROPIC_API_KEY to bypass credit check");
2153
+ }
2154
+ if (request.modelId) {
2155
+ this.config.args = [...this.config.args || [], "--model", request.modelId];
2156
+ this.debug("Model flag added:", request.modelId);
2157
+ }
2158
+ if (ccConfig) {
2159
+ const configArgs = this.buildConfigArgs(ccConfig);
2160
+ if (configArgs.length > 0) {
2161
+ this.config.args = [...this.config.args || [], ...configArgs];
2162
+ this.debug("Config args added:", configArgs);
2163
+ }
2164
+ }
2165
+ try {
2166
+ this.debug("State reset, calling super.execute()...");
2167
+ const result = await super.execute(endpoint, request, auth, onProgress, onRawEvent);
2168
+ this.debug("super.execute() returned with", result.trajectory.length, "steps");
2169
+ this.debug("========== execute() COMPLETED ==========");
2170
+ return result;
2171
+ } finally {
2172
+ this.config.env = originalEnv;
2173
+ this.config.args = originalArgs;
2174
+ this.config.inputMode = originalInputMode;
2175
+ this.config.timeout = originalTimeout;
2176
+ this.config.workingDir = originalWorkingDir;
2177
+ }
2059
2178
  }
2060
2179
  /**
2061
2180
  * Health check - verify claude command exists
@@ -2309,6 +2428,12 @@ function createServerCleanup(result, isCI) {
2309
2428
  }
2310
2429
 
2311
2430
  // cli/utils/apiClient.ts
2431
+ var ServerError = class extends Error {
2432
+ constructor(message) {
2433
+ super(message);
2434
+ this.name = "ServerError";
2435
+ }
2436
+ };
2312
2437
  var ApiClient = class {
2313
2438
  constructor(baseUrl) {
2314
2439
  this.baseUrl = baseUrl;
@@ -2424,7 +2549,7 @@ var ApiClient = class {
2424
2549
  } else if (event.type === "completed" || event.type === "cancelled") {
2425
2550
  finalRun = event.run;
2426
2551
  } else if (event.type === "error") {
2427
- throw new Error(event.error);
2552
+ throw new ServerError(event.error);
2428
2553
  }
2429
2554
  } catch (e) {
2430
2555
  if (e instanceof SyntaxError) continue;
@@ -2434,6 +2559,9 @@ var ApiClient = class {
2434
2559
  }
2435
2560
  }
2436
2561
  } catch (streamError) {
2562
+ if (streamError instanceof ServerError) {
2563
+ throw streamError;
2564
+ }
2437
2565
  if (runId) {
2438
2566
  console.warn(`[ApiClient] SSE stream disconnected: ${streamError instanceof Error ? streamError.message : streamError}`);
2439
2567
  console.warn(`[ApiClient] Falling back to polling for run ${runId}...`);
@@ -2671,7 +2799,7 @@ var ApiClient = class {
2671
2799
  }
2672
2800
  const response = await this.listTestCasesWithMeta();
2673
2801
  return response.data.find(
2674
- (tc) => tc.name.toLowerCase() === identifier.toLowerCase()
2802
+ (tc) => tc.name?.toLowerCase() === identifier.toLowerCase()
2675
2803
  ) || null;
2676
2804
  }
2677
2805
  /**
@@ -2719,7 +2847,7 @@ var ApiClient = class {
2719
2847
  if (event.type === "completed") {
2720
2848
  result = event.report;
2721
2849
  } else if (event.type === "error") {
2722
- throw new Error(event.error);
2850
+ throw new ServerError(event.error);
2723
2851
  }
2724
2852
  } catch (e) {
2725
2853
  if (e instanceof SyntaxError) continue;
@@ -2769,12 +2897,53 @@ var ApiClient = class {
2769
2897
  }
2770
2898
  return res.json();
2771
2899
  }
2900
+ /**
2901
+ * Fetch traces from OpenSearch with optional filters
2902
+ */
2903
+ async fetchTraces(params) {
2904
+ const res = await fetch(`${this.baseUrl}/api/traces`, {
2905
+ method: "POST",
2906
+ headers: { "Content-Type": "application/json" },
2907
+ body: JSON.stringify(params)
2908
+ });
2909
+ if (!res.ok) {
2910
+ const errorBody = await res.text();
2911
+ let errorMessage;
2912
+ try {
2913
+ const parsed = JSON.parse(errorBody);
2914
+ errorMessage = parsed.error || errorBody;
2915
+ } catch {
2916
+ errorMessage = errorBody;
2917
+ }
2918
+ throw new Error(`Failed to fetch traces: ${errorMessage}`);
2919
+ }
2920
+ return res.json();
2921
+ }
2772
2922
  };
2773
2923
 
2774
- // cli/commands/list.ts
2924
+ // cli/utils/formatOutput.ts
2925
+ var OUTPUT_FORMAT_DESCRIPTION = "Output format: table, json, markdown";
2926
+ function formatMarkdownTable(headers, rows) {
2927
+ const separator = headers.map(() => "---");
2928
+ const lines = [
2929
+ `| ${headers.join(" | ")} |`,
2930
+ `| ${separator.join(" | ")} |`,
2931
+ ...rows.map((row) => `| ${row.join(" | ")} |`)
2932
+ ];
2933
+ return lines.join("\n");
2934
+ }
2775
2935
  function formatJson(data) {
2776
2936
  return JSON.stringify(data, null, 2);
2777
2937
  }
2938
+ function parseOutputFormat(format) {
2939
+ const normalized = format.toLowerCase();
2940
+ if (normalized === "table" || normalized === "json" || normalized === "markdown" || normalized === "md") {
2941
+ return normalized === "md" ? "markdown" : normalized;
2942
+ }
2943
+ return "table";
2944
+ }
2945
+
2946
+ // cli/commands/list.ts
2778
2947
  function displayStorageWarnings(meta) {
2779
2948
  if (!meta.storageConfigured) {
2780
2949
  console.log(chalk.yellow("\n \u26A0 Storage not configured"));
@@ -2795,25 +2964,24 @@ async function listAgents(format, config) {
2795
2964
  console.log(formatJson(agents));
2796
2965
  return;
2797
2966
  }
2967
+ const headers = ["Key", "Name", "Connector", "Endpoint"];
2968
+ const rows = agents.map((agent) => [
2969
+ agent.key,
2970
+ agent.name,
2971
+ agent.connectorType || "agui-streaming",
2972
+ agent.endpoint.substring(0, 47) + (agent.endpoint.length > 47 ? "..." : "")
2973
+ ]);
2974
+ if (format === "markdown") {
2975
+ console.log(formatMarkdownTable(headers, rows));
2976
+ return;
2977
+ }
2798
2978
  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],
2979
+ head: headers.map((h) => chalk.cyan(h)),
2980
+ colWidths: [15, 20, 15, 50],
2807
2981
  wordWrap: true
2808
2982
  });
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
- ]);
2983
+ for (const row of rows) {
2984
+ table.push(row);
2817
2985
  }
2818
2986
  console.log(chalk.bold("\nAvailable Agents:\n"));
2819
2987
  console.log(table.toString());
@@ -2835,31 +3003,35 @@ async function listTestCases(format, config) {
2835
3003
  try {
2836
3004
  const client = new ApiClient(serverResult.baseUrl);
2837
3005
  const response = await client.listTestCasesWithMeta();
2838
- displayStorageWarnings(response.meta);
3006
+ if (format !== "markdown") {
3007
+ displayStorageWarnings(response.meta);
3008
+ }
2839
3009
  if (format === "json") {
2840
3010
  console.log(formatJson(response));
2841
3011
  return;
2842
3012
  }
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) {
3013
+ const headers = ["ID", "Name", "Labels", "Version", "Source"];
3014
+ const rows = response.data.map((tc) => {
2855
3015
  const isDemo = tc.id.startsWith("demo-");
2856
- table.push([
3016
+ return [
2857
3017
  tc.id,
2858
3018
  tc.name,
2859
3019
  tc.labels?.slice(0, 3).join(", ") || "",
2860
3020
  `v${tc.currentVersion || 1}`,
2861
- isDemo ? chalk.gray("Sample") : chalk.green("Stored")
2862
- ]);
3021
+ isDemo ? "Sample" : "Stored"
3022
+ ];
3023
+ });
3024
+ if (format === "markdown") {
3025
+ console.log(formatMarkdownTable(headers, rows));
3026
+ return;
3027
+ }
3028
+ const table = new Table({
3029
+ head: headers.map((h) => chalk.cyan(h)),
3030
+ colWidths: [25, 28, 28, 10, 10],
3031
+ wordWrap: true
3032
+ });
3033
+ for (const row of rows) {
3034
+ table.push(row);
2863
3035
  }
2864
3036
  console.log(chalk.bold("\nAvailable Test Cases:\n"));
2865
3037
  console.log(table.toString());
@@ -2888,31 +3060,35 @@ async function listBenchmarks(format, config) {
2888
3060
  try {
2889
3061
  const client = new ApiClient(serverResult.baseUrl);
2890
3062
  const response = await client.listBenchmarksWithMeta();
2891
- displayStorageWarnings(response.meta);
3063
+ if (format !== "markdown") {
3064
+ displayStorageWarnings(response.meta);
3065
+ }
2892
3066
  if (format === "json") {
2893
3067
  console.log(formatJson(response));
2894
3068
  return;
2895
3069
  }
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) {
3070
+ const headers = ["ID", "Name", "Test Cases", "Created", "Source"];
3071
+ const rows = response.data.map((b) => {
2908
3072
  const isDemo = b.id.startsWith("demo-");
2909
- table.push([
3073
+ return [
2910
3074
  b.id,
2911
3075
  b.name,
2912
3076
  b.testCaseIds.length.toString(),
2913
3077
  new Date(b.createdAt).toLocaleDateString(),
2914
- isDemo ? chalk.gray("Sample") : chalk.green("Stored")
2915
- ]);
3078
+ isDemo ? "Sample" : "Stored"
3079
+ ];
3080
+ });
3081
+ if (format === "markdown") {
3082
+ console.log(formatMarkdownTable(headers, rows));
3083
+ return;
3084
+ }
3085
+ const table = new Table({
3086
+ head: headers.map((h) => chalk.cyan(h)),
3087
+ colWidths: [28, 28, 12, 22, 10],
3088
+ wordWrap: true
3089
+ });
3090
+ for (const row of rows) {
3091
+ table.push(row);
2916
3092
  }
2917
3093
  console.log(chalk.bold("\nAvailable Benchmarks:\n"));
2918
3094
  console.log(table.toString());
@@ -2949,19 +3125,25 @@ function listConnectors(format) {
2949
3125
  console.log(formatJson(connectors));
2950
3126
  return;
2951
3127
  }
3128
+ const headers = ["Type", "Name", "Streaming"];
3129
+ const rows = connectors.map((c) => [
3130
+ c.type,
3131
+ c.name,
3132
+ c.streaming ? "Yes" : "No"
3133
+ ]);
3134
+ if (format === "markdown") {
3135
+ console.log(formatMarkdownTable(headers, rows));
3136
+ return;
3137
+ }
2952
3138
  const table = new Table({
2953
- head: [
2954
- chalk.cyan("Type"),
2955
- chalk.cyan("Name"),
2956
- chalk.cyan("Streaming")
2957
- ],
3139
+ head: headers.map((h) => chalk.cyan(h)),
2958
3140
  colWidths: [20, 25, 12]
2959
3141
  });
2960
- for (const c of connectors) {
3142
+ for (const row of rows) {
2961
3143
  table.push([
2962
- c.type,
2963
- c.name,
2964
- c.streaming ? chalk.green("Yes") : chalk.gray("No")
3144
+ row[0],
3145
+ row[1],
3146
+ row[2] === "Yes" ? chalk.green("Yes") : chalk.gray("No")
2965
3147
  ]);
2966
3148
  }
2967
3149
  console.log(chalk.bold("\nRegistered Connectors:\n"));
@@ -2980,23 +3162,24 @@ async function listModels(format, config) {
2980
3162
  console.log(formatJson(models));
2981
3163
  return;
2982
3164
  }
3165
+ const headers = ["Key", "Display Name", "Provider", "Context"];
3166
+ const rows = models.map((m) => [
3167
+ m.key,
3168
+ m.display_name || m.key,
3169
+ m.provider || "bedrock",
3170
+ m.context_window ? `${Math.round(m.context_window / 1e3)}k` : "-"
3171
+ ]);
3172
+ if (format === "markdown") {
3173
+ console.log(formatMarkdownTable(headers, rows));
3174
+ return;
3175
+ }
2983
3176
  const table = new Table({
2984
- head: [
2985
- chalk.cyan("Key"),
2986
- chalk.cyan("Display Name"),
2987
- chalk.cyan("Provider"),
2988
- chalk.cyan("Context")
2989
- ],
3177
+ head: headers.map((h) => chalk.cyan(h)),
2990
3178
  colWidths: [25, 30, 12, 12],
2991
3179
  wordWrap: true
2992
3180
  });
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
- ]);
3181
+ for (const row of rows) {
3182
+ table.push(row);
3000
3183
  }
3001
3184
  console.log(chalk.bold("\nAvailable Models:\n"));
3002
3185
  console.log(table.toString());
@@ -3013,8 +3196,8 @@ async function listModels(format, config) {
3013
3196
  }
3014
3197
  }
3015
3198
  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;
3199
+ 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) => {
3200
+ const format = parseOutputFormat(options.output);
3018
3201
  const config = await loadConfig();
3019
3202
  for (const connector of config.connectors) {
3020
3203
  connectorRegistry.register(connector);
@@ -3058,8 +3241,8 @@ function findAgent(identifier, config) {
3058
3241
  (a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
3059
3242
  );
3060
3243
  }
3061
- function getDefaultModel(agent) {
3062
- return agent.models[0] || "claude-sonnet";
3244
+ function getDefaultModel(config) {
3245
+ return Object.keys(config.models)[0] || "claude-sonnet";
3063
3246
  }
3064
3247
  async function commandExists(command) {
3065
3248
  const { execSync: execSync2 } = await import("child_process");
@@ -3113,26 +3296,36 @@ async function runForAgent(client, testCaseId, agent, modelId, verbose) {
3113
3296
  throw error;
3114
3297
  }
3115
3298
  }
3116
- function displayTableResults(results) {
3299
+ function buildResultRows(results) {
3300
+ return results.map((r) => {
3301
+ if (!r.report) {
3302
+ return [r.agent.name, "ERROR", "-", "-", "-"];
3303
+ }
3304
+ const status = r.report.passFailStatus === "passed" ? "PASSED" : r.report.passFailStatus === "failed" ? "FAILED" : r.report.status;
3305
+ return [
3306
+ r.agent.name,
3307
+ status,
3308
+ r.report.metrics?.accuracy ? `${Math.round(r.report.metrics.accuracy)}%` : "-",
3309
+ r.report.trajectorySteps.toString(),
3310
+ r.report.id?.substring(0, 27) + "..." || "-"
3311
+ ];
3312
+ });
3313
+ }
3314
+ function displayResults(results, format) {
3315
+ const headers = ["Agent", "Status", "Accuracy", "Steps", "Report ID"];
3316
+ const rows = buildResultRows(results);
3317
+ if (format === "markdown") {
3318
+ console.log("\n");
3319
+ console.log(formatMarkdownTable(headers, rows));
3320
+ return;
3321
+ }
3117
3322
  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
- ],
3323
+ head: headers.map((h) => chalk2.cyan(h)),
3125
3324
  colWidths: [20, 12, 12, 10, 30]
3126
3325
  });
3127
3326
  for (const r of results) {
3128
3327
  if (!r.report) {
3129
- table.push([
3130
- r.agent.name,
3131
- chalk2.red("ERROR"),
3132
- "-",
3133
- "-",
3134
- "-"
3135
- ]);
3328
+ table.push([r.agent.name, chalk2.red("ERROR"), "-", "-", "-"]);
3136
3329
  continue;
3137
3330
  }
3138
3331
  const statusStr = r.report.passFailStatus === "passed" ? chalk2.green("PASSED") : r.report.passFailStatus === "failed" ? chalk2.red("FAILED") : chalk2.yellow(r.report.status);
@@ -3148,7 +3341,7 @@ function displayTableResults(results) {
3148
3341
  console.log(table.toString());
3149
3342
  }
3150
3343
  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) => {
3344
+ 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
3345
  console.log(chalk2.bold("\nAgent Health - Test Case Runner\n"));
3153
3346
  const config = await loadConfig();
3154
3347
  for (const connector of config.connectors) {
@@ -3186,7 +3379,7 @@ function createRunCommand() {
3186
3379
  console.log("");
3187
3380
  const results = [];
3188
3381
  for (const agent of agents) {
3189
- const modelId = options.model || getDefaultModel(agent);
3382
+ const modelId = options.model || getDefaultModel(config);
3190
3383
  const validationError = await validateAgentRequirements(agent);
3191
3384
  if (validationError) {
3192
3385
  console.error(chalk2.red(` Error: ${validationError}`));
@@ -3197,17 +3390,27 @@ function createRunCommand() {
3197
3390
  const report = await runForAgent(client, testCase.id, agent, modelId, options.verbose || false);
3198
3391
  results.push({ agent, report });
3199
3392
  } catch (error) {
3200
- console.error(chalk2.red(` Error running ${agent.name}: ${error instanceof Error ? error.message : error}`));
3393
+ const errorMsg = error instanceof Error ? error.message : String(error);
3394
+ console.error(chalk2.red(` Error running ${agent.name}: ${errorMsg}`));
3395
+ const lowerError = errorMsg.toLowerCase();
3396
+ if (lowerError.includes("401") || lowerError.includes("403") || lowerError.includes("unauthorized") || lowerError.includes("forbidden") || lowerError.includes("token") || lowerError.includes("auth")) {
3397
+ console.log(chalk2.gray(` Hint: Authentication issue. Check agent-health.config.ts (headers, hooks.beforeRequest, or credentials).`));
3398
+ } else if (lowerError.includes("econnrefused") || lowerError.includes("enotfound") || lowerError.includes("connect")) {
3399
+ console.log(chalk2.gray(` Hint: Cannot reach agent endpoint. Run: npx @opensearch-project/agent-health doctor`));
3400
+ } else if (lowerError.includes("hook") || lowerError.includes("beforerequest")) {
3401
+ console.log(chalk2.gray(` Hint: The beforeRequest hook in agent-health.config.ts threw an error.`));
3402
+ }
3201
3403
  results.push({ agent, report: null });
3202
3404
  }
3203
3405
  }
3204
- if (options.output === "json") {
3205
- console.log(JSON.stringify(results.map((r) => ({
3406
+ const outputFormat = parseOutputFormat(options.output);
3407
+ if (outputFormat === "json") {
3408
+ console.log(formatJson(results.map((r) => ({
3206
3409
  agent: { key: r.agent.key, name: r.agent.name },
3207
3410
  report: r.report
3208
- })), null, 2));
3411
+ }))));
3209
3412
  } else {
3210
- displayTableResults(results);
3413
+ displayResults(results, outputFormat);
3211
3414
  }
3212
3415
  } catch (error) {
3213
3416
  console.error(chalk2.red(`
@@ -3354,8 +3557,8 @@ function findAgent2(identifier, config) {
3354
3557
  (a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
3355
3558
  );
3356
3559
  }
3357
- function getDefaultModel2(agent) {
3358
- return agent.models[0] || "claude-sonnet";
3560
+ function getDefaultModel2(config) {
3561
+ return Object.keys(config.models)[0] || "claude-sonnet";
3359
3562
  }
3360
3563
  function isFilePath(value) {
3361
3564
  return value.toLowerCase().endsWith(".json");
@@ -3391,7 +3594,7 @@ async function fetchReportsForRun(api, run) {
3391
3594
  );
3392
3595
  return reportsMap;
3393
3596
  }
3394
- async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3597
+ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose, concurrency) {
3395
3598
  const results = {
3396
3599
  agent,
3397
3600
  passed: 0,
@@ -3406,7 +3609,8 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3406
3609
  {
3407
3610
  name: `CLI Run - ${agent.name}`,
3408
3611
  agentKey: agent.key,
3409
- modelId
3612
+ modelId,
3613
+ ...concurrency && concurrency > 1 ? { concurrency } : {}
3410
3614
  },
3411
3615
  (event) => {
3412
3616
  if (event.type === "started") {
@@ -3415,9 +3619,13 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3415
3619
  const current = event.currentTestCaseIndex + 1;
3416
3620
  const testCaseName = event.currentTestCase?.name || `Test ${current}`;
3417
3621
  spinner.text = `${agent.name}: ${testCaseName} (${current}/${totalTestCases})`;
3418
- if (verbose && event.result) {
3622
+ if (event.result) {
3419
3623
  const status = event.result.status === "completed" ? chalk3.green("\u2713") : chalk3.red("\u2717");
3420
3624
  spinner.text = `${agent.name}: ${testCaseName} ${status} (${current}/${totalTestCases})`;
3625
+ if (verbose && event.result.status === "failed" && event.result.error) {
3626
+ spinner.info(`${agent.name}: ${testCaseName} ${chalk3.red("\u2717")} - ${event.result.error}`);
3627
+ spinner.start(`${agent.name}: (${current}/${totalTestCases})`);
3628
+ }
3421
3629
  }
3422
3630
  }
3423
3631
  }
@@ -3444,6 +3652,7 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3444
3652
  }
3445
3653
  } catch (error) {
3446
3654
  const errorMessage = error instanceof Error ? error.message : String(error);
3655
+ const isServerError = error instanceof ServerError;
3447
3656
  if (startedRunId) {
3448
3657
  results.runId = startedRunId;
3449
3658
  try {
@@ -3472,29 +3681,71 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
3472
3681
  }
3473
3682
  return results;
3474
3683
  }
3684
+ if (run.status === "failed") {
3685
+ const runError = run.error || errorMessage;
3686
+ spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${runError}`);
3687
+ if (stats.passed > 0 || stats.failed > 0) {
3688
+ console.log(chalk3.gray(` Partial results: ${stats.passed} passed, ${stats.failed} failed out of ${stats.total}`));
3689
+ }
3690
+ return results;
3691
+ }
3475
3692
  }
3476
3693
  } catch {
3477
3694
  }
3478
3695
  }
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 {
3696
+ if (isServerError) {
3484
3697
  spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${errorMessage}`);
3698
+ } else {
3699
+ const isStreamError = errorMessage.includes("terminated") || errorMessage.includes("network") || errorMessage.includes("stream") || errorMessage.includes("aborted");
3700
+ if (isStreamError && startedRunId) {
3701
+ spinner.warn(`${agent.name}: ${chalk3.yellow("Stream disconnected")} - server may still be processing`);
3702
+ console.log(chalk3.gray(` Check status: Use the UI to monitor progress`));
3703
+ } else {
3704
+ spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${errorMessage}`);
3705
+ }
3706
+ }
3707
+ const lowerError = errorMessage.toLowerCase();
3708
+ if (lowerError.includes("401") || lowerError.includes("403") || lowerError.includes("unauthorized") || lowerError.includes("forbidden") || lowerError.includes("token") || lowerError.includes("auth")) {
3709
+ console.log(chalk3.gray(` Hint: This looks like an authentication issue. Check your agent-health.config.ts`));
3710
+ console.log(chalk3.gray(` (headers, hooks.beforeRequest, or credentials) and re-run.`));
3711
+ } else if (lowerError.includes("econnrefused") || lowerError.includes("enotfound") || lowerError.includes("connect")) {
3712
+ console.log(chalk3.gray(` Hint: Could not connect to the agent endpoint. Verify the endpoint in agent-health.config.ts`));
3713
+ console.log(chalk3.gray(` is reachable: npx @opensearch-project/agent-health doctor`));
3714
+ } else if (lowerError.includes("not found") || lowerError.includes("agent not found")) {
3715
+ console.log(chalk3.gray(` Hint: Agent key not found. List available agents: npx @opensearch-project/agent-health list agents`));
3716
+ } else if (lowerError.includes("hook") || lowerError.includes("beforerequest")) {
3717
+ console.log(chalk3.gray(` Hint: The beforeRequest hook in agent-health.config.ts threw an error.`));
3718
+ console.log(chalk3.gray(` Check the hook logic and any external services it calls.`));
3719
+ }
3720
+ if (errorMessage !== "terminated") {
3721
+ console.log(chalk3.gray(` Debug: Run with DEBUG=true for verbose server logs`));
3485
3722
  }
3486
3723
  }
3487
3724
  return results;
3488
3725
  }
3489
- function displaySummaryTable(allResults, totalTestCases) {
3726
+ function buildSummaryRows(allResults, totalTestCases) {
3727
+ return allResults.map((results) => {
3728
+ const passRate = totalTestCases > 0 ? results.passed / totalTestCases * 100 : 0;
3729
+ return [
3730
+ results.agent.name,
3731
+ results.passed.toString(),
3732
+ results.failed.toString(),
3733
+ `${passRate.toFixed(0)}%`,
3734
+ results.run?.id || results.runId || "N/A"
3735
+ ];
3736
+ });
3737
+ }
3738
+ function displaySummary(allResults, totalTestCases, format) {
3739
+ const headers = ["Agent", "Passed", "Failed", "Pass Rate", "Run ID"];
3740
+ const rows = buildSummaryRows(allResults, totalTestCases);
3741
+ if (format === "markdown") {
3742
+ console.log("\n");
3743
+ console.log("## Benchmark Summary\n");
3744
+ console.log(formatMarkdownTable(headers, rows));
3745
+ return;
3746
+ }
3490
3747
  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
- ],
3748
+ head: headers.map((h) => chalk3.cyan(h)),
3498
3749
  colWidths: [25, 10, 10, 12, 35]
3499
3750
  });
3500
3751
  for (const results of allResults) {
@@ -3565,7 +3816,7 @@ function createBenchmarkCommand() {
3565
3816
  "Agent key (can be specified multiple times)",
3566
3817
  (val, arr) => [...arr, val],
3567
3818
  []
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) => {
3819
+ ).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
3820
  console.log(chalk3.bold("\nAgent Health - Benchmark Runner\n"));
3570
3821
  const config = await loadConfig();
3571
3822
  const serverConfig = { ...DEFAULT_SERVER_CONFIG, ...config.server };
@@ -3699,6 +3950,9 @@ function createBenchmarkCommand() {
3699
3950
  console.log(chalk3.gray(` - ${a.name} (${a.key})`));
3700
3951
  }
3701
3952
  console.log("");
3953
+ console.log(chalk3.gray(" To add a custom agent, configure it in agent-health.config.ts"));
3954
+ console.log(chalk3.gray(" Generate one with: npx @opensearch-project/agent-health init"));
3955
+ console.log("");
3702
3956
  process.exit(1);
3703
3957
  }
3704
3958
  agents.push(agent);
@@ -3706,19 +3960,25 @@ function createBenchmarkCommand() {
3706
3960
  console.log(chalk3.gray(` Agents: ${agents.map((a) => a.name).join(", ")}`));
3707
3961
  }
3708
3962
  console.log("");
3963
+ const concurrency = Math.max(1, Math.min(20, parseInt(options.concurrency, 10) || 1));
3964
+ if (concurrency > 1) {
3965
+ console.log(chalk3.gray(` Concurrency: ${concurrency}`));
3966
+ }
3709
3967
  const allResults = [];
3710
3968
  for (const agent of agents) {
3711
- const modelId = options.model || getDefaultModel2(agent);
3969
+ const modelId = options.model || getDefaultModel2(config);
3712
3970
  const results = await runBenchmarkForAgent(
3713
3971
  api,
3714
3972
  agent,
3715
3973
  modelId,
3716
3974
  benchmark,
3717
- options.verbose || false
3975
+ options.verbose || false,
3976
+ concurrency
3718
3977
  );
3719
3978
  allResults.push(results);
3720
3979
  }
3721
- if (options.output === "json") {
3980
+ const outputFormat = parseOutputFormat(options.output);
3981
+ if (outputFormat === "json") {
3722
3982
  const jsonOutput = allResults.map((r) => ({
3723
3983
  agent: { key: r.agent.key, name: r.agent.name },
3724
3984
  runId: r.run?.id || r.runId,
@@ -3727,9 +3987,9 @@ function createBenchmarkCommand() {
3727
3987
  passRate: benchmark.testCaseIds.length > 0 ? r.passed / benchmark.testCaseIds.length * 100 : 0,
3728
3988
  results: r.run?.results
3729
3989
  }));
3730
- console.log(JSON.stringify(jsonOutput, null, 2));
3990
+ console.log(formatJson(jsonOutput));
3731
3991
  } else {
3732
- displaySummaryTable(allResults, benchmark.testCaseIds.length);
3992
+ displaySummary(allResults, benchmark.testCaseIds.length, outputFormat);
3733
3993
  }
3734
3994
  if (options.export) {
3735
3995
  await exportResults(benchmark, allResults, options.export, options.format, serverResult.baseUrl);
@@ -4045,7 +4305,7 @@ function checkOpenSearchObservability() {
4045
4305
  ]
4046
4306
  };
4047
4307
  }
4048
- function displayResults(results) {
4308
+ function displayResults2(results) {
4049
4309
  console.log(chalk6.bold("\n Configuration Check\n"));
4050
4310
  for (const result of results) {
4051
4311
  const icon = {
@@ -4096,7 +4356,7 @@ function createDoctorCommand() {
4096
4356
  if (options.output === "json") {
4097
4357
  console.log(JSON.stringify(results, null, 2));
4098
4358
  } else {
4099
- displayResults(results);
4359
+ displayResults2(results);
4100
4360
  }
4101
4361
  });
4102
4362
  return command;
@@ -4127,7 +4387,6 @@ export default defineConfig({
4127
4387
  username: process.env.OPENSEARCH_USER || 'admin',
4128
4388
  password: process.env.OPENSEARCH_PASS || 'admin',
4129
4389
  },
4130
- models: ['claude-sonnet'],
4131
4390
  },
4132
4391
 
4133
4392
  // Claude Code CLI agent (optional)
@@ -4144,7 +4403,6 @@ export default defineConfig({
4144
4403
  },
4145
4404
  }),
4146
4405
  endpoint: 'claude', // Command name
4147
- models: ['claude-sonnet-4'],
4148
4406
  },
4149
4407
  */
4150
4408
  ],
@@ -4426,6 +4684,338 @@ function createMigrateCommand() {
4426
4684
  return command;
4427
4685
  }
4428
4686
 
4687
+ // cli/commands/compare-services.ts
4688
+ import { Command as Command9 } from "commander";
4689
+ import chalk9 from "chalk";
4690
+ function analyzeErrorPatterns(spans) {
4691
+ const errorSpans = spans.filter((s) => s.status === "ERROR");
4692
+ if (errorSpans.length === 0) {
4693
+ return { errorSpans, patterns: [], avgDurationMs: 0 };
4694
+ }
4695
+ const patternMap = /* @__PURE__ */ new Map();
4696
+ let totalDuration = 0;
4697
+ for (const span of errorSpans) {
4698
+ const errorType = extractErrorType(span);
4699
+ const spanName = span.name || "Unknown";
4700
+ const duration = span.duration || 0;
4701
+ const errorMsg = extractErrorMessage(span);
4702
+ totalDuration += duration;
4703
+ if (!patternMap.has(errorType)) {
4704
+ patternMap.set(errorType, {
4705
+ count: 0,
4706
+ spanNames: /* @__PURE__ */ new Set(),
4707
+ durations: [],
4708
+ messages: /* @__PURE__ */ new Set()
4709
+ });
4710
+ }
4711
+ const pattern = patternMap.get(errorType);
4712
+ pattern.count++;
4713
+ pattern.spanNames.add(spanName);
4714
+ pattern.durations.push(duration);
4715
+ if (errorMsg) pattern.messages.add(errorMsg);
4716
+ }
4717
+ const patterns = Array.from(patternMap.entries()).map(([errorType, data]) => ({
4718
+ errorType,
4719
+ count: data.count,
4720
+ spanNames: Array.from(data.spanNames),
4721
+ avgDurationMs: data.durations.reduce((a, b) => a + b, 0) / data.durations.length,
4722
+ exampleMessages: Array.from(data.messages).slice(0, 3)
4723
+ // Top 3 examples
4724
+ })).sort((a, b) => b.count - a.count);
4725
+ const avgDurationMs = totalDuration / errorSpans.length;
4726
+ return { errorSpans, patterns, avgDurationMs };
4727
+ }
4728
+ function extractErrorType(span) {
4729
+ const attrs = span.attributes || {};
4730
+ if (attrs["error.type"]) return attrs["error.type"];
4731
+ if (attrs["exception.type"]) return attrs["exception.type"];
4732
+ if (attrs["http.status_code"] >= 400) return `HTTP ${attrs["http.status_code"]}`;
4733
+ const name = span.name || "";
4734
+ if (name.includes("timeout")) return "Timeout";
4735
+ if (name.includes("connection")) return "Connection Error";
4736
+ if (name.includes("auth")) return "Authentication Error";
4737
+ return "Unknown Error";
4738
+ }
4739
+ function extractErrorMessage(span) {
4740
+ const attrs = span.attributes || {};
4741
+ if (attrs["error.message"]) return attrs["error.message"];
4742
+ if (attrs["exception.message"]) return attrs["exception.message"];
4743
+ if (span.events && Array.isArray(span.events)) {
4744
+ for (const event of span.events) {
4745
+ if (event.name === "exception" && event.attributes?.["exception.message"]) {
4746
+ return event.attributes["exception.message"];
4747
+ }
4748
+ }
4749
+ }
4750
+ return null;
4751
+ }
4752
+ async function analyzeServiceErrors(client, serviceName, startTime, endTime, limit = 1e3) {
4753
+ console.log(chalk9.gray(`
4754
+ Fetching traces for service: ${serviceName}...`));
4755
+ const response = await client.fetchTraces({
4756
+ serviceName,
4757
+ startTime,
4758
+ endTime,
4759
+ size: limit
4760
+ });
4761
+ const spans = response.spans || [];
4762
+ console.log(chalk9.gray(` Found ${spans.length} spans`));
4763
+ const traceMap = /* @__PURE__ */ new Map();
4764
+ for (const span of spans) {
4765
+ if (!traceMap.has(span.traceId)) {
4766
+ traceMap.set(span.traceId, []);
4767
+ }
4768
+ traceMap.get(span.traceId).push(span);
4769
+ }
4770
+ const totalTraces = traceMap.size;
4771
+ let tracesWithErrors = 0;
4772
+ for (const traceSpans of traceMap.values()) {
4773
+ if (traceSpans.some((s) => s.status === "ERROR")) {
4774
+ tracesWithErrors++;
4775
+ }
4776
+ }
4777
+ const errorRate = totalTraces > 0 ? tracesWithErrors / totalTraces * 100 : 0;
4778
+ const { errorSpans, patterns, avgDurationMs } = analyzeErrorPatterns(spans);
4779
+ return {
4780
+ serviceName,
4781
+ totalTraces,
4782
+ tracesWithErrors,
4783
+ errorRate,
4784
+ totalErrorSpans: errorSpans.length,
4785
+ errorPatterns: patterns,
4786
+ avgErrorDurationMs: avgDurationMs
4787
+ };
4788
+ }
4789
+ function printServiceAnalysis(analysis) {
4790
+ console.log(chalk9.bold.cyan(`
4791
+ ${"=".repeat(60)}`));
4792
+ console.log(chalk9.bold.cyan(`Service: ${analysis.serviceName}`));
4793
+ console.log(chalk9.bold.cyan("=".repeat(60)));
4794
+ console.log(chalk9.white(`Total Traces: ${analysis.totalTraces}`));
4795
+ console.log(chalk9.white(`Traces with Errors: ${analysis.tracesWithErrors}`));
4796
+ const errorRateColor = analysis.errorRate > 10 ? chalk9.red : analysis.errorRate > 5 ? chalk9.yellow : chalk9.green;
4797
+ console.log(errorRateColor(`Error Rate: ${analysis.errorRate.toFixed(2)}%`));
4798
+ console.log(chalk9.white(`Total Error Spans: ${analysis.totalErrorSpans}`));
4799
+ console.log(chalk9.white(`Avg Error Span Duration: ${analysis.avgErrorDurationMs.toFixed(2)}ms`));
4800
+ if (analysis.errorPatterns.length > 0) {
4801
+ console.log(chalk9.bold.white("\nError Patterns:"));
4802
+ for (const pattern of analysis.errorPatterns) {
4803
+ console.log(chalk9.yellow(`
4804
+ \u2022 ${pattern.errorType}`));
4805
+ console.log(chalk9.gray(` Count: ${pattern.count}`));
4806
+ console.log(chalk9.gray(` Avg Duration: ${pattern.avgDurationMs.toFixed(2)}ms`));
4807
+ console.log(chalk9.gray(` Affected Spans: ${pattern.spanNames.join(", ")}`));
4808
+ if (pattern.exampleMessages.length > 0) {
4809
+ console.log(chalk9.gray(` Example Messages:`));
4810
+ pattern.exampleMessages.forEach((msg) => {
4811
+ console.log(chalk9.gray(` - ${msg.substring(0, 80)}${msg.length > 80 ? "..." : ""}`));
4812
+ });
4813
+ }
4814
+ }
4815
+ } else {
4816
+ console.log(chalk9.green("\n\u2713 No error patterns detected"));
4817
+ }
4818
+ }
4819
+ function printComparison(service1, service2) {
4820
+ console.log(chalk9.bold.magenta(`
4821
+ ${"=".repeat(60)}`));
4822
+ console.log(chalk9.bold.magenta("COMPARISON SUMMARY"));
4823
+ console.log(chalk9.bold.magenta("=".repeat(60)));
4824
+ const errorRateDiff = service1.errorRate - service2.errorRate;
4825
+ const diffColor = Math.abs(errorRateDiff) < 1 ? chalk9.white : errorRateDiff > 0 ? chalk9.red : chalk9.green;
4826
+ const diffSymbol = errorRateDiff > 0 ? "\u2191" : errorRateDiff < 0 ? "\u2193" : "=";
4827
+ console.log(chalk9.bold.white("\nError Rate:"));
4828
+ console.log(` ${service1.serviceName}: ${service1.errorRate.toFixed(2)}%`);
4829
+ console.log(` ${service2.serviceName}: ${service2.errorRate.toFixed(2)}%`);
4830
+ console.log(diffColor(` Difference: ${diffSymbol} ${Math.abs(errorRateDiff).toFixed(2)}%`));
4831
+ console.log(chalk9.bold.white("\nUnique Error Patterns:"));
4832
+ const patterns1 = new Set(service1.errorPatterns.map((p) => p.errorType));
4833
+ const patterns2 = new Set(service2.errorPatterns.map((p) => p.errorType));
4834
+ const onlyIn1 = Array.from(patterns1).filter((p) => !patterns2.has(p));
4835
+ const onlyIn2 = Array.from(patterns2).filter((p) => !patterns1.has(p));
4836
+ const inBoth = Array.from(patterns1).filter((p) => patterns2.has(p));
4837
+ if (onlyIn1.length > 0) {
4838
+ console.log(chalk9.cyan(`
4839
+ Only in ${service1.serviceName}:`));
4840
+ onlyIn1.forEach((p) => console.log(chalk9.gray(` \u2022 ${p}`)));
4841
+ }
4842
+ if (onlyIn2.length > 0) {
4843
+ console.log(chalk9.cyan(`
4844
+ Only in ${service2.serviceName}:`));
4845
+ onlyIn2.forEach((p) => console.log(chalk9.gray(` \u2022 ${p}`)));
4846
+ }
4847
+ if (inBoth.length > 0) {
4848
+ console.log(chalk9.cyan(`
4849
+ Common error patterns:`));
4850
+ inBoth.forEach((p) => {
4851
+ const count1 = service1.errorPatterns.find((x) => x.errorType === p)?.count || 0;
4852
+ const count2 = service2.errorPatterns.find((x) => x.errorType === p)?.count || 0;
4853
+ console.log(chalk9.gray(` \u2022 ${p}: ${count1} vs ${count2}`));
4854
+ });
4855
+ }
4856
+ console.log(chalk9.bold.yellow("\nRecommendations:"));
4857
+ if (service1.errorRate > service2.errorRate * 1.5) {
4858
+ console.log(chalk9.yellow(` \u26A0 ${service1.serviceName} has significantly higher error rate - investigate urgently`));
4859
+ } else if (service2.errorRate > service1.errorRate * 1.5) {
4860
+ console.log(chalk9.yellow(` \u26A0 ${service2.serviceName} has significantly higher error rate - investigate urgently`));
4861
+ } else {
4862
+ console.log(chalk9.green(` \u2713 Error rates are comparable`));
4863
+ }
4864
+ if (onlyIn1.length > 2) {
4865
+ console.log(chalk9.yellow(` \u26A0 ${service1.serviceName} has ${onlyIn1.length} unique error types - review configuration`));
4866
+ }
4867
+ if (onlyIn2.length > 2) {
4868
+ console.log(chalk9.yellow(` \u26A0 ${service2.serviceName} has ${onlyIn2.length} unique error types - review configuration`));
4869
+ }
4870
+ }
4871
+ function createCompareServicesCommand() {
4872
+ const cmd = new Command9("compare-services");
4873
+ 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) => {
4874
+ try {
4875
+ const config = await loadConfig();
4876
+ const serverResult = await ensureServer(config.server);
4877
+ const client = new ApiClient(serverResult.baseUrl);
4878
+ const serviceNames = options.services.split(",").map((s) => s.trim());
4879
+ if (serviceNames.length !== 2) {
4880
+ console.error(chalk9.red("Error: Please provide exactly two service names"));
4881
+ process.exit(1);
4882
+ }
4883
+ const [service1Name, service2Name] = serviceNames;
4884
+ const limit = parseInt(options.limit, 10);
4885
+ console.log(chalk9.bold.cyan("\nComparing Error Patterns Between Services"));
4886
+ console.log(chalk9.gray(`Service 1: ${service1Name}`));
4887
+ console.log(chalk9.gray(`Service 2: ${service2Name}`));
4888
+ if (options.start) console.log(chalk9.gray(`Time Range: ${options.start} to ${options.end || "now"}`));
4889
+ const analysis1 = await analyzeServiceErrors(client, service1Name, options.start, options.end, limit);
4890
+ const analysis2 = await analyzeServiceErrors(client, service2Name, options.start, options.end, limit);
4891
+ printServiceAnalysis(analysis1);
4892
+ printServiceAnalysis(analysis2);
4893
+ printComparison(analysis1, analysis2);
4894
+ console.log();
4895
+ } catch (error) {
4896
+ console.error(chalk9.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
4897
+ process.exit(1);
4898
+ }
4899
+ });
4900
+ return cmd;
4901
+ }
4902
+
4903
+ // cli/commands/remote.ts
4904
+ import { Command as Command10 } from "commander";
4905
+ import chalk10 from "chalk";
4906
+ import fs2 from "fs";
4907
+ import path2 from "path";
4908
+ var CONFIG_FILENAME2 = "agent-health.config.json";
4909
+ function getConfigPath() {
4910
+ return path2.join(process.cwd(), CONFIG_FILENAME2);
4911
+ }
4912
+ function readConfig() {
4913
+ const filePath = getConfigPath();
4914
+ if (!fs2.existsSync(filePath)) return {};
4915
+ try {
4916
+ return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
4917
+ } catch {
4918
+ return {};
4919
+ }
4920
+ }
4921
+ function writeConfig(config) {
4922
+ fs2.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
4923
+ }
4924
+ function getRemoteServers(config) {
4925
+ return Array.isArray(config.remoteServers) ? config.remoteServers : [];
4926
+ }
4927
+ function createRemoteCommand() {
4928
+ const remote = new Command10("remote").description("Manage remote agent-health server connections");
4929
+ remote.command("add").description("Add a remote server").requiredOption("--name <name>", "Display name for the server").requiredOption("--url <url>", "Server URL (e.g. http://10.0.1.50:4001)").option("--api-key <key>", "API key for authentication").action((options) => {
4930
+ const config = readConfig();
4931
+ const servers = getRemoteServers(config);
4932
+ if (servers.some((s) => s.name === options.name)) {
4933
+ console.error(chalk10.red(`
4934
+ Error: Server "${options.name}" already exists. Use 'remote remove' first.
4935
+ `));
4936
+ process.exit(1);
4937
+ }
4938
+ const server = { name: options.name, url: options.url.replace(/\/$/, "") };
4939
+ if (options.apiKey) server.apiKey = options.apiKey;
4940
+ servers.push(server);
4941
+ config.remoteServers = servers;
4942
+ writeConfig(config);
4943
+ console.log(chalk10.green(`
4944
+ Added remote server: ${options.name} (${options.url})
4945
+ `));
4946
+ });
4947
+ remote.command("remove").description("Remove a remote server").argument("<name>", "Server name to remove").action((name) => {
4948
+ const config = readConfig();
4949
+ const servers = getRemoteServers(config);
4950
+ const idx = servers.findIndex((s) => s.name === name);
4951
+ if (idx === -1) {
4952
+ console.error(chalk10.red(`
4953
+ Error: Server "${name}" not found.
4954
+ `));
4955
+ process.exit(1);
4956
+ }
4957
+ servers.splice(idx, 1);
4958
+ config.remoteServers = servers;
4959
+ writeConfig(config);
4960
+ console.log(chalk10.green(`
4961
+ Removed remote server: ${name}
4962
+ `));
4963
+ });
4964
+ remote.command("list").description("List configured remote servers").action(() => {
4965
+ const config = readConfig();
4966
+ const servers = getRemoteServers(config);
4967
+ if (servers.length === 0) {
4968
+ console.log(chalk10.gray("\n No remote servers configured.\n"));
4969
+ console.log(chalk10.gray(" Add one with: agent-health remote add --name <name> --url <url>\n"));
4970
+ return;
4971
+ }
4972
+ console.log(chalk10.cyan(`
4973
+ Remote Servers (${servers.length}):
4974
+ `));
4975
+ for (const s of servers) {
4976
+ const auth = s.apiKey ? chalk10.green(" [auth]") : chalk10.gray(" [no auth]");
4977
+ console.log(` ${chalk10.bold(s.name)} ${s.url}${auth}`);
4978
+ }
4979
+ console.log("");
4980
+ });
4981
+ remote.command("test").description("Test connectivity to all remote servers").action(async () => {
4982
+ const config = readConfig();
4983
+ const servers = getRemoteServers(config);
4984
+ if (servers.length === 0) {
4985
+ console.log(chalk10.gray("\n No remote servers configured.\n"));
4986
+ return;
4987
+ }
4988
+ console.log(chalk10.cyan(`
4989
+ Testing ${servers.length} remote server(s)...
4990
+ `));
4991
+ for (const s of servers) {
4992
+ try {
4993
+ const headers = {};
4994
+ if (s.apiKey) headers["Authorization"] = `Bearer ${s.apiKey}`;
4995
+ const controller = new AbortController();
4996
+ const timer = setTimeout(() => controller.abort(), 5e3);
4997
+ const response = await fetch(`${s.url}/api/coding-agents/available`, {
4998
+ headers,
4999
+ signal: controller.signal
5000
+ });
5001
+ clearTimeout(timer);
5002
+ if (response.ok) {
5003
+ const data = await response.json();
5004
+ const agentCount = data.agents?.length ?? 0;
5005
+ console.log(chalk10.green(` \u2713 ${s.name} \u2014 OK (${agentCount} agents detected)`));
5006
+ } else {
5007
+ console.log(chalk10.red(` \u2717 ${s.name} \u2014 HTTP ${response.status} ${response.statusText}`));
5008
+ }
5009
+ } catch (error) {
5010
+ const msg = error instanceof Error ? error.message : String(error);
5011
+ console.log(chalk10.red(` \u2717 ${s.name} \u2014 ${msg}`));
5012
+ }
5013
+ }
5014
+ console.log("");
5015
+ });
5016
+ return remote;
5017
+ }
5018
+
4429
5019
  // cli/index.ts
4430
5020
  var __filename3 = fileURLToPath3(import.meta.url);
4431
5021
  var __dirname3 = dirname3(__filename3);
@@ -4439,59 +5029,126 @@ try {
4439
5029
  function loadEnvFile(envPath) {
4440
5030
  const absolutePath = resolve4(process.cwd(), envPath);
4441
5031
  if (!existsSync5(absolutePath)) {
4442
- console.error(chalk9.red(`
5032
+ console.error(chalk11.red(`
4443
5033
  Error: Environment file not found: ${absolutePath}
4444
5034
  `));
4445
5035
  process.exit(1);
4446
5036
  }
4447
5037
  const result = loadDotenv({ path: absolutePath });
4448
5038
  if (result.error) {
4449
- console.error(chalk9.red(`
5039
+ console.error(chalk11.red(`
4450
5040
  Error loading environment file: ${result.error.message}
4451
5041
  `));
4452
5042
  process.exit(1);
4453
5043
  }
4454
- console.log(chalk9.gray(` Loaded environment from: ${envPath}`));
5044
+ console.log(chalk11.gray(` Loaded environment from: ${envPath}`));
4455
5045
  }
4456
5046
  var defaultEnvPath = resolve4(process.cwd(), ".env");
4457
5047
  if (existsSync5(defaultEnvPath)) {
4458
5048
  loadDotenv({ path: defaultEnvPath });
4459
5049
  }
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();
4462
- 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");
5050
+ var program = new Command11();
5051
+ program.name("agent-health").description("Agent Health Evaluation Framework - Evaluate and monitor AI agent performance").version(version).enablePositionalOptions().passThroughOptions().configureHelp({
5052
+ sortSubcommands: false,
5053
+ // Hide default command list — replaced by grouped custom help below
5054
+ subcommandTerm: () => "",
5055
+ formatHelp: (cmd, helper) => {
5056
+ const termWidth = helper.padWidth(cmd, helper);
5057
+ const helpWidth = helper.helpWidth || 80;
5058
+ const output = [];
5059
+ const desc = helper.commandDescription(cmd);
5060
+ if (desc) {
5061
+ output.push(desc, "");
5062
+ }
5063
+ output.push(`${chalk11.cyan.bold("Usage:")} ${helper.commandUsage(cmd)}`, "");
5064
+ const optionList = helper.visibleOptions(cmd).map((opt) => {
5065
+ const term = helper.optionTerm(opt);
5066
+ const desc2 = helper.optionDescription(opt);
5067
+ return ` ${term.padEnd(termWidth)} ${desc2}`;
5068
+ }).join("\n");
5069
+ if (optionList) {
5070
+ output.push(`${chalk11.cyan.bold("Options:")}`, optionList, "");
5071
+ }
5072
+ return output.join("\n");
5073
+ }
5074
+ });
5075
+ program.addHelpText("after", `
5076
+ ${chalk11.cyan.bold("Getting Started:")}
5077
+ ${chalk11.yellow("agent-health")} Launch the web UI and evaluation server
5078
+ ${chalk11.yellow("agent-health init")} Generate an agent-health.config.ts file
5079
+ ${chalk11.yellow("agent-health doctor")} Verify your setup (AWS creds, OpenSearch, agents)
5080
+
5081
+ ${chalk11.cyan.bold("Running Evaluations:")}
5082
+ ${chalk11.yellow("agent-health run")} ${chalk11.gray("-t <case> -a <agent>")} Run a single test case against an agent
5083
+ ${chalk11.yellow("agent-health benchmark")} ${chalk11.gray("-f <file>")} Run a full benchmark from a test cases JSON file
5084
+ ${chalk11.yellow("agent-health benchmark")} ${chalk11.gray("-b <id>")} Re-run an existing benchmark
5085
+
5086
+ ${chalk11.cyan.bold("Viewing Results:")}
5087
+ ${chalk11.yellow("agent-health list")} ${chalk11.gray("agents|benchmarks|...")} List agents, connectors, test cases, or benchmarks
5088
+ ${chalk11.yellow("agent-health report")} ${chalk11.gray("-b <benchmark>")} Generate an HTML/PDF/JSON report
5089
+ ${chalk11.yellow("agent-health export")} ${chalk11.gray("-b <benchmark>")} Export test cases as re-importable JSON
5090
+ ${chalk11.yellow("agent-health compare-services")} ${chalk11.gray("-s A B")} Compare error patterns between services
5091
+
5092
+ ${chalk11.cyan.bold("Remote Servers:")}
5093
+ ${chalk11.yellow("agent-health remote add")} ${chalk11.gray("--name <n> --url <u>")} Add a remote server
5094
+ ${chalk11.yellow("agent-health remote list")} List configured remote servers
5095
+ ${chalk11.yellow("agent-health remote test")} Test connectivity to all remotes
5096
+
5097
+ ${chalk11.cyan.bold("Maintenance:")}
5098
+ ${chalk11.yellow("agent-health migrate")} Migrate legacy benchmark data to current format
5099
+ ${chalk11.yellow("agent-health serve")} Start the server (same as default, explicit command)
5100
+
5101
+ ${chalk11.cyan.bold("Examples:")}
5102
+ ${chalk11.gray("$")} npx @opensearch-project/agent-health
5103
+ ${chalk11.gray("$")} npx @opensearch-project/agent-health --port 8080 --no-browser
5104
+ ${chalk11.gray("$")} npx @opensearch-project/agent-health run -t "RCA for 500 errors" -a langgraph
5105
+ ${chalk11.gray("$")} npx @opensearch-project/agent-health benchmark -f ./test-cases.json -a my-agent
5106
+ ${chalk11.gray("$")} npx @opensearch-project/agent-health list agents
5107
+ ${chalk11.gray("$")} npx @opensearch-project/agent-health report -b bench-123 -f pdf -o report.pdf
5108
+ ${chalk11.gray("$")} npx @opensearch-project/agent-health serve --headless --api-key sk-secret
5109
+ `);
5110
+ 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").option("--headless", "Run API server only (no frontend, no browser)").option("--api-key <key>", "Require API key for coding-agents endpoints");
4463
5111
  program.action(async (options) => {
4464
- console.log(chalk9.cyan.bold(`
5112
+ console.log(chalk11.cyan.bold(`
4465
5113
  Agent Health v${version} - AI Agent Evaluation Framework
4466
5114
  `));
4467
- console.log(chalk9.gray(` Working directory: ${process.cwd()}`));
4468
- console.log(chalk9.gray(` Package directory: ${__dirname3}`));
5115
+ console.log(chalk11.gray(` Working directory: ${process.cwd()}`));
5116
+ console.log(chalk11.gray(` Package directory: ${__dirname3}`));
4469
5117
  if (options.envFile) {
4470
5118
  loadEnvFile(options.envFile);
4471
5119
  } else if (existsSync5(defaultEnvPath)) {
4472
- console.log(chalk9.gray(" Auto-loaded .env from current directory"));
5120
+ console.log(chalk11.gray(" Auto-loaded .env from current directory"));
4473
5121
  }
4474
5122
  const port = parseInt(options.port, 10);
4475
- const spinner = ora5("Starting server...").start();
5123
+ const headless = options.headless || false;
5124
+ const spinner = ora5(headless ? "Starting headless API server..." : "Starting server...").start();
4476
5125
  try {
4477
- await startServer({ port });
4478
- 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)
5126
+ await startServer({ port, headless, apiKey: options.apiKey });
5127
+ spinner.succeed(headless ? "Headless API server started" : "Server started");
5128
+ if (headless) {
5129
+ console.log(chalk11.green(`
5130
+ API server running on http://0.0.0.0:${port}`));
5131
+ if (options.apiKey) console.log(chalk11.gray(" API key authentication enabled"));
5132
+ console.log(chalk11.gray(" Mode: headless (API only, no frontend)\n"));
5133
+ } else {
5134
+ console.log(chalk11.gray("\n Configuration:"));
5135
+ console.log(chalk11.gray(` Storage: Sample data (configure OpenSearch for persistence)`));
5136
+ console.log(chalk11.gray(` Agent: Select in UI (Demo Agent for mock, real agents require endpoints)`));
5137
+ console.log(chalk11.gray(` Judge: Select in UI (Demo Judge for mock, Bedrock requires AWS creds)
4483
5138
  `));
4484
- const url = `http://localhost:${port}`;
4485
- console.log(chalk9.green(` Server running at ${chalk9.bold(url)}
5139
+ const url = `http://localhost:${port}`;
5140
+ console.log(chalk11.green(` Server running at ${chalk11.bold(url)}
4486
5141
  `));
4487
- if (options.browser !== false) {
4488
- console.log(chalk9.gray(" Opening browser..."));
4489
- await open(url);
5142
+ console.log(chalk11.green(` Demo data loaded`));
5143
+ if (options.browser !== false) {
5144
+ console.log(chalk11.gray(" Opening browser..."));
5145
+ await open(url);
5146
+ }
4490
5147
  }
4491
- console.log(chalk9.gray(" Press Ctrl+C to stop\n"));
5148
+ console.log(chalk11.gray(" Press Ctrl+C to stop\n"));
4492
5149
  } catch (error) {
4493
5150
  spinner.fail("Failed to start server");
4494
- console.error(chalk9.red(`
5151
+ console.error(chalk11.red(`
4495
5152
  Error: ${error instanceof Error ? error.message : error}
4496
5153
  `));
4497
5154
  process.exit(1);
@@ -4505,26 +5162,35 @@ program.addCommand(createReportCommand());
4505
5162
  program.addCommand(createDoctorCommand());
4506
5163
  program.addCommand(createInitCommand());
4507
5164
  program.addCommand(createMigrateCommand());
4508
- 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(`
5165
+ program.addCommand(createCompareServicesCommand());
5166
+ program.addCommand(createRemoteCommand());
5167
+ 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").option("--headless", "Run API server only (no frontend, no browser)").option("--api-key <key>", "Require API key for coding-agents endpoints").action(async (options) => {
5168
+ console.log(chalk11.cyan.bold(`
4510
5169
  Agent Health v${version} - AI Agent Evaluation Framework
4511
5170
  `));
4512
5171
  const port = parseInt(options.port, 10);
4513
- const spinner = ora5("Starting server...").start();
5172
+ const headless = options.headless || false;
5173
+ const spinner = ora5(headless ? "Starting headless API server..." : "Starting server...").start();
4514
5174
  try {
4515
- await startServer({ port });
4516
- spinner.succeed("Server started");
5175
+ await startServer({ port, headless, apiKey: options.apiKey });
5176
+ spinner.succeed(headless ? "Headless API server started" : "Server started");
4517
5177
  const url = `http://localhost:${port}`;
4518
- console.log(chalk9.green(` Server running at ${chalk9.bold(url)}
5178
+ if (headless) {
5179
+ console.log(chalk11.green(` API server running on http://0.0.0.0:${port}`));
5180
+ if (options.apiKey) console.log(chalk11.gray(" API key authentication enabled"));
5181
+ console.log(chalk11.gray(" Mode: headless (API only, no frontend)\n"));
5182
+ } else {
5183
+ console.log(chalk11.green(` Server running at ${chalk11.bold(url)}
4519
5184
  `));
4520
- if (options.browser !== false) {
4521
- console.log(chalk9.gray(" Opening browser..."));
4522
- await open(url);
5185
+ if (options.browser !== false) {
5186
+ console.log(chalk11.gray(" Opening browser..."));
5187
+ await open(url);
5188
+ }
4523
5189
  }
4524
- console.log(chalk9.gray(" Press Ctrl+C to stop\n"));
5190
+ console.log(chalk11.gray(" Press Ctrl+C to stop\n"));
4525
5191
  } catch (error) {
4526
5192
  spinner.fail("Failed to start server");
4527
- console.error(chalk9.red(`
5193
+ console.error(chalk11.red(`
4528
5194
  Error: ${error instanceof Error ? error.message : error}
4529
5195
  `));
4530
5196
  process.exit(1);
@@ -4533,15 +5199,15 @@ program.command("serve").description("Start the Agent Health server (same as def
4533
5199
  program.on("command:*", (operands) => {
4534
5200
  const unknownCommand = operands[0];
4535
5201
  const availableCommands = program.commands.map((cmd) => cmd.name());
4536
- console.error(chalk9.red(`
5202
+ console.error(chalk11.red(`
4537
5203
  Error: Unknown command '${unknownCommand}'`));
4538
5204
  console.log("");
4539
- console.log(chalk9.cyan(" Available commands:"));
5205
+ console.log(chalk11.cyan(" Available commands:"));
4540
5206
  for (const cmd of availableCommands) {
4541
- console.log(chalk9.gray(` - ${cmd}`));
5207
+ console.log(chalk11.gray(` - ${cmd}`));
4542
5208
  }
4543
5209
  console.log("");
4544
- console.log(chalk9.gray(` Run ${chalk9.cyan("agent-health --help")} for usage information.
5210
+ console.log(chalk11.gray(` Run ${chalk11.cyan("agent-health --help")} for usage information.
4545
5211
  `));
4546
5212
  process.exitCode = 1;
4547
5213
  });