@opensearch-project/agent-health 0.2.0 → 0.4.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,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // cli/index.ts
4
- import { Command as Command10 } from "commander";
5
- import chalk10 from "chalk";
6
- import { fileURLToPath as fileURLToPath3 } from "url";
7
- import { dirname as dirname3, join as join4, resolve as resolve4 } from "path";
8
- import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
4
+ import { Command as Command14 } from "commander";
5
+ import chalk14 from "chalk";
6
+ import { fileURLToPath as fileURLToPath5 } from "url";
7
+ import { dirname as dirname5, join as join7, resolve as resolve4 } from "path";
8
+ import { readFileSync as readFileSync5, existsSync as existsSync7 } from "fs";
9
9
  import { config as loadDotenv } from "dotenv";
10
10
  import open from "open";
11
11
  import ora5 from "ora";
@@ -26,17 +26,37 @@ function findPackageRoot() {
26
26
  }
27
27
  return join(__dirname, "..");
28
28
  }
29
+ var MAX_PORT_ATTEMPTS = 10;
29
30
  async function startServer(options) {
30
31
  process.env.VITE_BACKEND_PORT = String(options.port);
32
+ if (options.headless) process.env.AGENT_HEALTH_HEADLESS = "1";
33
+ if (options.apiKey) process.env.AGENT_HEALTH_API_KEY = options.apiKey;
31
34
  const packageRoot = findPackageRoot();
32
35
  const serverPath = join(packageRoot, "server", "dist", "app.js");
33
36
  const { createApp } = await import(serverPath);
34
37
  const app = await createApp();
35
- return new Promise((resolve5) => {
36
- app.listen(options.port, "0.0.0.0", () => {
37
- resolve5();
38
+ const tryListen = (port) => {
39
+ return new Promise((resolve5, reject) => {
40
+ const server = app.listen(port, "0.0.0.0");
41
+ server.on("listening", () => {
42
+ resolve5(port);
43
+ });
44
+ server.on("error", (err) => {
45
+ server.close();
46
+ if (err.code === "EADDRINUSE" && port <= options.port + MAX_PORT_ATTEMPTS) {
47
+ console.log(` Port ${port} is in use, trying ${port + 1}...`);
48
+ resolve5(tryListen(port + 1));
49
+ } else {
50
+ reject(err);
51
+ }
52
+ });
38
53
  });
39
- });
54
+ };
55
+ const actualPort = await tryListen(options.port);
56
+ if (actualPort !== options.port) {
57
+ process.env.VITE_BACKEND_PORT = String(actualPort);
58
+ }
59
+ return actualPort;
40
60
  }
41
61
 
42
62
  // cli/commands/list.ts
@@ -99,6 +119,8 @@ var ENV_CONFIG = {
99
119
  // OpenAI-compatible (optional - for OpenAI-compatible judge/agent endpoints)
100
120
  openaiCompatibleApiKey: getEnvVar("OPENAI_COMPATIBLE_API_KEY", ""),
101
121
  openaiCompatibleEndpoint: getEnvVar("OPENAI_COMPATIBLE_ENDPOINT", "http://localhost:4000/v1/chat/completions"),
122
+ // Observio Sample Agent endpoint
123
+ observioEndpoint: getEnvVar("OBSERVIO_ENDPOINT", "http://localhost:3001/run-agent"),
102
124
  // Claude Code Telemetry (optional - for OTEL traces from Claude Code)
103
125
  claudeCodeTelemetryEnabled: getEnvVar("CLAUDE_CODE_TELEMETRY_ENABLED", "false") === "true",
104
126
  otelExporterEndpoint: getEnvVar("OTEL_EXPORTER_OTLP_ENDPOINT", ""),
@@ -134,6 +156,16 @@ var CONNECTOR_TYPE_INFO = {
134
156
  description: "Invokes the Claude Code CLI. Server-only \u2014 use the CLI or benchmark runner.",
135
157
  serverOnly: true
136
158
  },
159
+ "strands": {
160
+ label: "Amazon Strands",
161
+ description: "Amazon Strands agent framework via Bedrock Agent Runtime API. Server-only \u2014 requires AWS SDK.",
162
+ serverOnly: true
163
+ },
164
+ "langgraph": {
165
+ label: "LangGraph (REST)",
166
+ description: "LangGraph agent via direct REST API. Use for non-AG-UI LangGraph instances.",
167
+ serverOnly: false
168
+ },
137
169
  "mock": {
138
170
  label: "Mock",
139
171
  description: "Built-in demo agent for testing. No real endpoint needed.",
@@ -176,6 +208,15 @@ var DEFAULT_CONFIG = {
176
208
  headers: {},
177
209
  useTraces: false
178
210
  },
211
+ {
212
+ key: "observio",
213
+ name: "Observio Sample Agent",
214
+ endpoint: ENV_CONFIG.observioEndpoint || "http://localhost:3001/run-agent",
215
+ description: "Observio sample agent \u2014 ReAct pattern with LangGraph and Bedrock. Start with: cd observio-sample-agent && npm run start:ag-ui",
216
+ connectorType: "agui-streaming",
217
+ headers: {},
218
+ useTraces: true
219
+ },
179
220
  {
180
221
  key: "claude-code",
181
222
  name: "Claude Code",
@@ -185,6 +226,30 @@ var DEFAULT_CONFIG = {
185
226
  headers: {},
186
227
  useTraces: ENV_CONFIG.claudeCodeTelemetryEnabled && !!ENV_CONFIG.otelExporterEndpoint,
187
228
  connectorConfig: { env: getClaudeCodeConnectorEnv() }
229
+ },
230
+ {
231
+ key: "strands",
232
+ name: "Amazon Strands",
233
+ endpoint: "${STRANDS_AGENT_ID}",
234
+ description: "Amazon Strands agent framework (Bedrock Agent Runtime)",
235
+ connectorType: "strands",
236
+ headers: {},
237
+ useTraces: false,
238
+ connectorConfig: {
239
+ agentAliasId: "${STRANDS_ALIAS_ID:-TSTALIASID}",
240
+ region: "${AWS_REGION:-us-east-1}"
241
+ },
242
+ enabled: false
243
+ },
244
+ {
245
+ key: "langgraph-rest",
246
+ name: "LangGraph (REST)",
247
+ endpoint: "${LANGGRAPH_API_ENDPOINT:-http://localhost:8000}",
248
+ description: "LangGraph agent via direct REST API",
249
+ connectorType: "langgraph",
250
+ headers: {},
251
+ useTraces: false,
252
+ enabled: false
188
253
  }
189
254
  ],
190
255
  models: {
@@ -373,6 +438,7 @@ function mergeConfigs(userConfig, defaultConfig) {
373
438
  ...DEFAULT_SERVER_CONFIG,
374
439
  ...userConfig.server
375
440
  };
441
+ const telemetry = userConfig.telemetry ?? {};
376
442
  return {
377
443
  server,
378
444
  agents,
@@ -380,7 +446,8 @@ function mergeConfigs(userConfig, defaultConfig) {
380
446
  connectors,
381
447
  testCases,
382
448
  reporters,
383
- judge
449
+ judge,
450
+ telemetry
384
451
  };
385
452
  }
386
453
  async function loadUserConfig(configPath) {
@@ -1713,11 +1780,150 @@ var OpenAICompatibleConnector = class extends BaseConnector {
1713
1780
  };
1714
1781
  var openaiCompatibleConnector = new OpenAICompatibleConnector();
1715
1782
 
1783
+ // services/connectors/langgraph/LangGraphConnector.ts
1784
+ var LangGraphConnector = class extends BaseConnector {
1785
+ constructor() {
1786
+ super(...arguments);
1787
+ this.type = "langgraph";
1788
+ this.name = "LangGraph (REST)";
1789
+ this.supportsStreaming = false;
1790
+ }
1791
+ buildPayload(request) {
1792
+ const config = request.connectorConfig || {};
1793
+ return {
1794
+ input: {
1795
+ messages: [
1796
+ {
1797
+ role: "user",
1798
+ content: request.testCase.initialPrompt
1799
+ }
1800
+ ]
1801
+ },
1802
+ config: {
1803
+ configurable: {
1804
+ ...request.modelId && { model: request.modelId },
1805
+ ...config.configurable
1806
+ }
1807
+ }
1808
+ };
1809
+ }
1810
+ async execute(endpoint, request, auth, onProgress, onRawEvent) {
1811
+ const payload = request.payload || this.buildPayload(request);
1812
+ const headers = this.buildAuthHeaders(auth);
1813
+ const config = request.connectorConfig || {};
1814
+ const graphId = config.graphId || "agent";
1815
+ const baseUrl = endpoint.replace(/\/+$/, "");
1816
+ const threadId = config.threadId || request.threadId;
1817
+ const invokeUrl = threadId ? `${baseUrl}/threads/${threadId}/runs/wait` : `${baseUrl}/assistants/${graphId}/invoke`;
1818
+ this.debug("Executing LangGraph request");
1819
+ this.debug("URL:", invokeUrl);
1820
+ const response = await fetch(invokeUrl, {
1821
+ method: "POST",
1822
+ headers: {
1823
+ "Content-Type": "application/json",
1824
+ ...headers
1825
+ },
1826
+ body: JSON.stringify(payload)
1827
+ });
1828
+ if (!response.ok) {
1829
+ const errorText = await response.text();
1830
+ throw new Error(`LangGraph request failed: ${response.status} - ${errorText}`);
1831
+ }
1832
+ const data = await response.json();
1833
+ onRawEvent?.(data);
1834
+ const trajectory = this.parseResponse(data);
1835
+ trajectory.forEach((step) => onProgress?.(step));
1836
+ return {
1837
+ trajectory,
1838
+ runId: data.run_id || data.thread_id || threadId || null,
1839
+ rawEvents: [data],
1840
+ metadata: {
1841
+ graphId,
1842
+ threadId: data.thread_id || threadId
1843
+ }
1844
+ };
1845
+ }
1846
+ parseResponse(data) {
1847
+ const steps = [];
1848
+ const messages = data.output?.messages || data.values?.messages || data.messages || [];
1849
+ for (const msg of messages) {
1850
+ const role = msg.type || msg.role;
1851
+ const content = typeof msg.content === "string" ? msg.content : Array.isArray(msg.content) ? msg.content.map((c) => c.text || JSON.stringify(c)).join("\n") : JSON.stringify(msg.content);
1852
+ if (role === "ai" || role === "assistant") {
1853
+ if (msg.tool_calls && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
1854
+ for (const toolCall of msg.tool_calls) {
1855
+ steps.push(this.createStep("action", `Calling ${toolCall.name}...`, {
1856
+ toolName: toolCall.name,
1857
+ toolArgs: toolCall.args
1858
+ }));
1859
+ }
1860
+ }
1861
+ if (content && !msg.tool_calls?.length) {
1862
+ steps.push(this.createStep("assistant", content));
1863
+ }
1864
+ } else if (role === "tool") {
1865
+ steps.push(this.createStep("tool_result", content, {
1866
+ toolName: msg.name,
1867
+ status: "SUCCESS"
1868
+ }));
1869
+ }
1870
+ }
1871
+ const lastAssistant = [...steps].reverse().find((s) => s.type === "assistant");
1872
+ if (lastAssistant) {
1873
+ lastAssistant.type = "response";
1874
+ }
1875
+ if (data.intermediate_steps && Array.isArray(data.intermediate_steps)) {
1876
+ for (const [action, observation] of data.intermediate_steps) {
1877
+ if (action?.tool) {
1878
+ steps.push(this.createStep("action", `Calling ${action.tool}...`, {
1879
+ toolName: action.tool,
1880
+ toolArgs: action.tool_input
1881
+ }));
1882
+ }
1883
+ if (observation !== void 0) {
1884
+ const obsContent = typeof observation === "string" ? observation : JSON.stringify(observation);
1885
+ steps.push(this.createStep("tool_result", obsContent, {
1886
+ status: "SUCCESS"
1887
+ }));
1888
+ }
1889
+ }
1890
+ }
1891
+ if (data.output && typeof data.output === "string" && steps.length === 0) {
1892
+ steps.push(this.createStep("response", data.output));
1893
+ }
1894
+ if (steps.length === 0 && data) {
1895
+ steps.push(this.createStep("response", JSON.stringify(data, null, 2)));
1896
+ }
1897
+ return steps;
1898
+ }
1899
+ async healthCheck(endpoint, auth) {
1900
+ try {
1901
+ const headers = this.buildAuthHeaders(auth);
1902
+ const baseUrl = endpoint.replace(/\/+$/, "");
1903
+ const response = await fetch(`${baseUrl}/ok`, {
1904
+ method: "GET",
1905
+ headers
1906
+ });
1907
+ return response.ok;
1908
+ } catch {
1909
+ try {
1910
+ const headers = this.buildAuthHeaders(auth);
1911
+ const response = await fetch(endpoint, { method: "GET", headers });
1912
+ return response.ok;
1913
+ } catch {
1914
+ return false;
1915
+ }
1916
+ }
1917
+ }
1918
+ };
1919
+ var langgraphConnector = new LangGraphConnector();
1920
+
1716
1921
  // services/connectors/index.ts
1717
1922
  connectorRegistry.register(aguiStreamingConnector);
1718
1923
  connectorRegistry.register(mockConnector);
1719
1924
  connectorRegistry.register(restConnector);
1720
1925
  connectorRegistry.register(openaiCompatibleConnector);
1926
+ connectorRegistry.register(langgraphConnector);
1721
1927
  console.log("[Connectors] Browser-safe connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
1722
1928
 
1723
1929
  // services/connectors/subprocess/SubprocessConnector.ts
@@ -1761,6 +1967,7 @@ Question: ${prompt}`;
1761
1967
  const command = endpoint || this.config.command;
1762
1968
  const args = this.config.args || [];
1763
1969
  const input = request.payload || this.buildPayload(request);
1970
+ const runId = `subprocess-${Date.now()}`;
1764
1971
  this.debug("Command:", command);
1765
1972
  this.debug("Args:", args);
1766
1973
  this.debug("Input mode:", this.config.inputMode);
@@ -1768,10 +1975,12 @@ Question: ${prompt}`;
1768
1975
  this.debug("Timeout:", this.config.timeout);
1769
1976
  this.debug("Input (first 500 chars):", input.substring(0, 500));
1770
1977
  this.debug("Working dir:", this.config.workingDir || process.cwd());
1978
+ this.debug("Run ID:", runId);
1771
1979
  const env = {
1772
1980
  ...process.env,
1773
1981
  ...this.buildAuthEnv(auth),
1774
- ...this.config.env
1982
+ ...this.config.env,
1983
+ AGENT_EVAL_RUN_ID: runId
1775
1984
  };
1776
1985
  return new Promise((resolve5, reject) => {
1777
1986
  const trajectory = [];
@@ -1830,6 +2039,17 @@ Question: ${prompt}`;
1830
2039
  this.error(`Process exited with code ${code}`);
1831
2040
  this.error("stderr:", stderr);
1832
2041
  }
2042
+ if (this.config.outputParser === "streaming") {
2043
+ this.onBeforeStreamEnd(trajectory, onProgress);
2044
+ if (code !== 0 && trajectory.length === 0) {
2045
+ const errorContent = stderr.trim() ? `Error: Process exited with code ${code}. ${stderr.trim()}` : `Error: Process exited with code ${code}`;
2046
+ const errorStep = this.createStep("tool_result", errorContent, {
2047
+ status: "FAILURE" /* FAILURE */
2048
+ });
2049
+ trajectory.push(errorStep);
2050
+ onProgress?.(errorStep);
2051
+ }
2052
+ }
1833
2053
  const finalTrajectory = this.config.outputParser === "streaming" ? trajectory : this.parseResponse({ stdout, stderr, exitCode: code });
1834
2054
  if (this.config.outputParser !== "streaming") {
1835
2055
  finalTrajectory.forEach((step) => onProgress?.(step));
@@ -1837,7 +2057,7 @@ Question: ${prompt}`;
1837
2057
  this.debug("Resolving with trajectory of", finalTrajectory.length, "steps");
1838
2058
  resolve5({
1839
2059
  trajectory: finalTrajectory,
1840
- runId: `subprocess-${Date.now()}`,
2060
+ runId,
1841
2061
  rawEvents: rawOutput,
1842
2062
  metadata: {
1843
2063
  command,
@@ -1868,6 +2088,12 @@ Question: ${prompt}`;
1868
2088
  });
1869
2089
  this.debug("========== execute() COMPLETED ==========");
1870
2090
  }
2091
+ /**
2092
+ * Hook called before returning the streaming trajectory on process close.
2093
+ * Subclasses can override to flush internal buffers.
2094
+ */
2095
+ onBeforeStreamEnd(_trajectory, _onProgress) {
2096
+ }
1871
2097
  /**
1872
2098
  * Parse streaming output and emit steps in real-time
1873
2099
  */
@@ -1962,6 +2188,7 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
1962
2188
  this.name = "Claude Code CLI";
1963
2189
  this.outputBuffer = "";
1964
2190
  this.thinkingBuffer = "";
2191
+ this.textBuffer = "";
1965
2192
  this.isInThinking = false;
1966
2193
  }
1967
2194
  /**
@@ -2031,11 +2258,17 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
2031
2258
  if (event.delta?.type === "thinking_delta" && event.delta.thinking) {
2032
2259
  this.thinkingBuffer += event.delta.thinking;
2033
2260
  } else if (event.delta?.type === "text_delta" && event.delta.text) {
2034
- steps.push(this.createStep("assistant", event.delta.text));
2261
+ this.textBuffer += event.delta.text;
2262
+ }
2263
+ } else if (event.type === "content_block_stop") {
2264
+ if (this.thinkingBuffer) {
2265
+ steps.push(this.createStep("thinking", this.thinkingBuffer));
2266
+ this.thinkingBuffer = "";
2267
+ }
2268
+ if (this.textBuffer) {
2269
+ steps.push(this.createStep("assistant", this.textBuffer));
2270
+ this.textBuffer = "";
2035
2271
  }
2036
- } else if (event.type === "content_block_stop" && this.thinkingBuffer) {
2037
- steps.push(this.createStep("thinking", this.thinkingBuffer));
2038
- this.thinkingBuffer = "";
2039
2272
  } else if (event.type === "result" && event.result) {
2040
2273
  steps.push(this.createStep(
2041
2274
  "response",
@@ -2081,8 +2314,41 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
2081
2314
  resetState() {
2082
2315
  this.outputBuffer = "";
2083
2316
  this.thinkingBuffer = "";
2317
+ this.textBuffer = "";
2084
2318
  this.isInThinking = false;
2085
2319
  }
2320
+ /**
2321
+ * Flush remaining buffers when the subprocess stream ends.
2322
+ */
2323
+ onBeforeStreamEnd(trajectory, onProgress) {
2324
+ if (this.outputBuffer.trim()) {
2325
+ try {
2326
+ const event = JSON.parse(this.outputBuffer.trim());
2327
+ const steps = this.parseJsonEvent(event);
2328
+ for (const step of steps) {
2329
+ trajectory.push(step);
2330
+ onProgress?.(step);
2331
+ }
2332
+ } catch {
2333
+ const step = this.createStep("assistant", this.outputBuffer.trim());
2334
+ trajectory.push(step);
2335
+ onProgress?.(step);
2336
+ }
2337
+ this.outputBuffer = "";
2338
+ }
2339
+ if (this.thinkingBuffer) {
2340
+ const step = this.createStep("thinking", this.thinkingBuffer);
2341
+ trajectory.push(step);
2342
+ onProgress?.(step);
2343
+ this.thinkingBuffer = "";
2344
+ }
2345
+ if (this.textBuffer) {
2346
+ const step = this.createStep("response", this.textBuffer);
2347
+ trajectory.push(step);
2348
+ onProgress?.(step);
2349
+ this.textBuffer = "";
2350
+ }
2351
+ }
2086
2352
  /**
2087
2353
  * Build CLI args from ClaudeCodeConnectorConfig
2088
2354
  */
@@ -2183,9 +2449,214 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
2183
2449
  };
2184
2450
  var claudeCodeConnector = new ClaudeCodeConnector();
2185
2451
 
2452
+ // services/connectors/strands/StrandsConnector.ts
2453
+ var StrandsConnector = class extends BaseConnector {
2454
+ constructor() {
2455
+ super(...arguments);
2456
+ this.type = "strands";
2457
+ this.name = "Amazon Strands";
2458
+ this.supportsStreaming = true;
2459
+ }
2460
+ buildPayload(request) {
2461
+ const config = request.connectorConfig || {};
2462
+ return {
2463
+ agentId: "",
2464
+ // Set from endpoint in execute()
2465
+ agentAliasId: config.agentAliasId || "TSTALIASID",
2466
+ sessionId: config.sessionId || `eval-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2467
+ inputText: request.testCase.initialPrompt,
2468
+ enableTrace: config.enableTrace !== false
2469
+ };
2470
+ }
2471
+ async execute(endpoint, request, auth, onProgress, onRawEvent) {
2472
+ const payload = request.payload || this.buildPayload(request);
2473
+ payload.agentId = endpoint;
2474
+ const config = request.connectorConfig || {};
2475
+ const region = config.region || auth.awsRegion || process.env.AWS_REGION || "us-east-1";
2476
+ this.debug("Executing Strands agent");
2477
+ this.debug("Agent ID:", endpoint);
2478
+ this.debug("Alias:", payload.agentAliasId);
2479
+ this.debug("Region:", region);
2480
+ const { BedrockAgentRuntimeClient, InvokeAgentCommand } = await import("@aws-sdk/client-bedrock-agent-runtime");
2481
+ const clientConfig = { region };
2482
+ if (auth.type === "aws-sigv4" && auth.awsAccessKeyId && auth.awsSecretAccessKey) {
2483
+ clientConfig.credentials = {
2484
+ accessKeyId: auth.awsAccessKeyId,
2485
+ secretAccessKey: auth.awsSecretAccessKey,
2486
+ ...auth.awsSessionToken && { sessionToken: auth.awsSessionToken }
2487
+ };
2488
+ }
2489
+ const client = new BedrockAgentRuntimeClient(clientConfig);
2490
+ const command = new InvokeAgentCommand({
2491
+ agentId: payload.agentId,
2492
+ agentAliasId: payload.agentAliasId,
2493
+ sessionId: payload.sessionId,
2494
+ inputText: payload.inputText,
2495
+ enableTrace: payload.enableTrace
2496
+ });
2497
+ const response = await client.send(command);
2498
+ const trajectory = [];
2499
+ let finalOutput = "";
2500
+ if (response.completion) {
2501
+ for await (const event of response.completion) {
2502
+ onRawEvent?.(event);
2503
+ if (event.chunk?.bytes) {
2504
+ const text = new TextDecoder().decode(event.chunk.bytes);
2505
+ finalOutput += text;
2506
+ }
2507
+ if (event.trace?.trace) {
2508
+ const traceSteps = this.parseTraceEvent(event.trace.trace);
2509
+ for (const step of traceSteps) {
2510
+ trajectory.push(step);
2511
+ onProgress?.(step);
2512
+ }
2513
+ }
2514
+ }
2515
+ }
2516
+ const hasResponseFromTrace = trajectory.some((s) => s.type === "response");
2517
+ if (finalOutput && !hasResponseFromTrace) {
2518
+ const responseStep = this.createStep("response", finalOutput);
2519
+ trajectory.push(responseStep);
2520
+ onProgress?.(responseStep);
2521
+ }
2522
+ return {
2523
+ trajectory,
2524
+ runId: payload.sessionId,
2525
+ metadata: {
2526
+ agentId: endpoint,
2527
+ agentAliasId: payload.agentAliasId,
2528
+ sessionId: payload.sessionId,
2529
+ region
2530
+ }
2531
+ };
2532
+ }
2533
+ parseResponse(rawResponse) {
2534
+ if (rawResponse?.trace?.trace) {
2535
+ return this.parseTraceEvent(rawResponse.trace.trace);
2536
+ }
2537
+ if (typeof rawResponse === "string") {
2538
+ return [this.createStep("response", rawResponse)];
2539
+ }
2540
+ return [];
2541
+ }
2542
+ /**
2543
+ * Parse a Bedrock Agent trace event into TrajectorySteps
2544
+ */
2545
+ parseTraceEvent(trace) {
2546
+ const steps = [];
2547
+ if (trace.preProcessingTrace) {
2548
+ const pre = trace.preProcessingTrace;
2549
+ if (pre.modelInvocationOutput?.parsedResponse?.isValid !== void 0) {
2550
+ steps.push(this.createStep(
2551
+ "thinking",
2552
+ `Pre-processing: Input ${pre.modelInvocationOutput.parsedResponse.isValid ? "valid" : "invalid"}` + (pre.modelInvocationOutput.parsedResponse.rationale ? ` \u2014 ${pre.modelInvocationOutput.parsedResponse.rationale}` : "")
2553
+ ));
2554
+ }
2555
+ }
2556
+ if (trace.orchestrationTrace) {
2557
+ const orch = trace.orchestrationTrace;
2558
+ if (orch.rationale?.text) {
2559
+ steps.push(this.createStep("thinking", orch.rationale.text));
2560
+ }
2561
+ if (orch.modelInvocationInput?.text) {
2562
+ steps.push(this.createStep("thinking", `Model input: ${this.truncate(orch.modelInvocationInput.text, 500)}`));
2563
+ }
2564
+ if (orch.invocationInput) {
2565
+ const inv = orch.invocationInput;
2566
+ if (inv.actionGroupInvocationInput) {
2567
+ const action = inv.actionGroupInvocationInput;
2568
+ steps.push(this.createStep("action", `Calling ${action.actionGroupName || "action"}::${action.apiPath || action.function || "invoke"}`, {
2569
+ toolName: `${action.actionGroupName || "action"}::${action.apiPath || action.function || "invoke"}`,
2570
+ toolArgs: action.parameters ? Object.fromEntries(
2571
+ action.parameters.map((p) => [p.name, p.value])
2572
+ ) : void 0
2573
+ }));
2574
+ }
2575
+ if (inv.knowledgeBaseLookupInput) {
2576
+ steps.push(this.createStep("action", `Knowledge base lookup: ${inv.knowledgeBaseLookupInput.text}`, {
2577
+ toolName: "knowledge_base_lookup",
2578
+ toolArgs: { query: inv.knowledgeBaseLookupInput.text }
2579
+ }));
2580
+ }
2581
+ }
2582
+ if (orch.observation) {
2583
+ const obs = orch.observation;
2584
+ if (obs.actionGroupInvocationOutput?.text) {
2585
+ steps.push(this.createStep("tool_result", obs.actionGroupInvocationOutput.text, {
2586
+ status: "SUCCESS"
2587
+ }));
2588
+ }
2589
+ if (obs.knowledgeBaseLookupOutput?.retrievedReferences) {
2590
+ const refs = obs.knowledgeBaseLookupOutput.retrievedReferences;
2591
+ steps.push(this.createStep(
2592
+ "tool_result",
2593
+ `Retrieved ${refs.length} reference(s) from knowledge base`,
2594
+ {
2595
+ status: "SUCCESS",
2596
+ toolOutput: refs.map((r) => ({
2597
+ content: this.truncate(r.content?.text, 200),
2598
+ location: r.location
2599
+ }))
2600
+ }
2601
+ ));
2602
+ }
2603
+ if (obs.finalResponse?.text) {
2604
+ steps.push(this.createStep("response", obs.finalResponse.text));
2605
+ }
2606
+ }
2607
+ }
2608
+ if (trace.postProcessingTrace) {
2609
+ const post = trace.postProcessingTrace;
2610
+ if (post.modelInvocationOutput?.parsedResponse?.text) {
2611
+ steps.push(this.createStep(
2612
+ "thinking",
2613
+ `Post-processing: ${this.truncate(post.modelInvocationOutput.parsedResponse.text, 300)}`
2614
+ ));
2615
+ }
2616
+ }
2617
+ if (trace.failureTrace) {
2618
+ steps.push(this.createStep(
2619
+ "response",
2620
+ `Agent error: ${trace.failureTrace.failureReason || "Unknown failure"}`
2621
+ ));
2622
+ }
2623
+ return steps;
2624
+ }
2625
+ /**
2626
+ * Health check: verify the agent exists via GetAgent
2627
+ */
2628
+ async healthCheck(endpoint, auth) {
2629
+ try {
2630
+ const config = auth;
2631
+ const region = config.awsRegion || process.env.AWS_REGION || "us-east-1";
2632
+ const { BedrockAgentClient, GetAgentCommand } = await import("@aws-sdk/client-bedrock-agent");
2633
+ const clientConfig = { region };
2634
+ if (auth.type === "aws-sigv4" && auth.awsAccessKeyId && auth.awsSecretAccessKey) {
2635
+ clientConfig.credentials = {
2636
+ accessKeyId: auth.awsAccessKeyId,
2637
+ secretAccessKey: auth.awsSecretAccessKey,
2638
+ ...auth.awsSessionToken && { sessionToken: auth.awsSessionToken }
2639
+ };
2640
+ }
2641
+ const client = new BedrockAgentClient(clientConfig);
2642
+ const result = await client.send(new GetAgentCommand({ agentId: endpoint }));
2643
+ return result.agent?.agentStatus === "PREPARED";
2644
+ } catch (error) {
2645
+ this.error("Health check failed:", error);
2646
+ return false;
2647
+ }
2648
+ }
2649
+ truncate(text, maxLen) {
2650
+ if (!text) return "";
2651
+ return text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
2652
+ }
2653
+ };
2654
+ var strandsConnector = new StrandsConnector();
2655
+
2186
2656
  // services/connectors/server.ts
2187
2657
  connectorRegistry.register(subprocessConnector);
2188
2658
  connectorRegistry.register(claudeCodeConnector);
2659
+ connectorRegistry.register(strandsConnector);
2189
2660
  console.log("[Connectors] Server connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
2190
2661
 
2191
2662
  // cli/utils/serverLifecycle.ts
@@ -2327,20 +2798,35 @@ async function startServer2(port, timeout) {
2327
2798
  });
2328
2799
  let stderrOutput = "";
2329
2800
  let stdoutOutput = "";
2801
+ let resolvePortDetection;
2802
+ const portDetected = new Promise((resolve5) => {
2803
+ resolvePortDetection = resolve5;
2804
+ });
2330
2805
  child.stderr?.on("data", (data) => {
2331
2806
  stderrOutput += data.toString();
2332
2807
  });
2333
2808
  child.stdout?.on("data", (data) => {
2334
- stdoutOutput += data.toString();
2809
+ const chunk = data.toString();
2810
+ stdoutOutput += chunk;
2811
+ const match = chunk.match(/Backend Server running on http:\/\/0\.0\.0\.0:(\d+)/);
2812
+ if (match) {
2813
+ resolvePortDetection(parseInt(match[1], 10));
2814
+ }
2335
2815
  });
2336
2816
  let earlyExit = false;
2337
2817
  let exitCode = null;
2338
2818
  child.on("exit", (code) => {
2339
2819
  earlyExit = true;
2340
2820
  exitCode = code;
2821
+ resolvePortDetection(port);
2341
2822
  });
2342
2823
  child.unref();
2343
- const ready = await waitForServer(port, timeout);
2824
+ const portDetectionTimeout = Math.min(timeout, 1e4);
2825
+ const actualPort = await Promise.race([
2826
+ portDetected,
2827
+ new Promise((resolve5) => setTimeout(() => resolve5(port), portDetectionTimeout))
2828
+ ]);
2829
+ const ready = await waitForServer(actualPort, timeout);
2344
2830
  if (!ready) {
2345
2831
  try {
2346
2832
  child.kill();
@@ -2366,7 +2852,7 @@ ${stdoutOutput}`);
2366
2852
  }
2367
2853
  throw new Error(`Server failed to start within ${timeout}ms on port ${port}`);
2368
2854
  }
2369
- return child;
2855
+ return { child, actualPort };
2370
2856
  }
2371
2857
  function stopServer(process2) {
2372
2858
  try {
@@ -2410,11 +2896,14 @@ async function ensureServer(config) {
2410
2896
  );
2411
2897
  }
2412
2898
  }
2413
- const serverProcess = await startServer2(port, startTimeout);
2899
+ const { child, actualPort } = await startServer2(port, startTimeout);
2900
+ if (actualPort !== port) {
2901
+ console.log(`[ServerLifecycle] Port ${port} was in use, server started on port ${actualPort}`);
2902
+ }
2414
2903
  return {
2415
2904
  wasStarted: true,
2416
- baseUrl,
2417
- process: serverProcess
2905
+ baseUrl: `http://localhost:${actualPort}`,
2906
+ process: child
2418
2907
  };
2419
2908
  }
2420
2909
  function createServerCleanup(result, isCI) {
@@ -3243,10 +3732,10 @@ function getDefaultModel(config) {
3243
3732
  return Object.keys(config.models)[0] || "claude-sonnet";
3244
3733
  }
3245
3734
  async function commandExists(command) {
3246
- const { execSync: execSync2 } = await import("child_process");
3735
+ const { execSync: execSync3 } = await import("child_process");
3247
3736
  const checkCommand = process.platform === "win32" ? `where ${command}` : `which ${command}`;
3248
3737
  try {
3249
- execSync2(checkCommand, { stdio: "ignore" });
3738
+ execSync3(checkCommand, { stdio: "ignore" });
3250
3739
  return true;
3251
3740
  } catch {
3252
3741
  return false;
@@ -4183,7 +4672,7 @@ function checkEnvFile() {
4183
4672
  details: ["Env vars can be set in shell, CI/CD, or via --env-file"]
4184
4673
  };
4185
4674
  }
4186
- function checkAWSCredentials() {
4675
+ async function checkAWSCredentials() {
4187
4676
  const profile = process.env.AWS_PROFILE;
4188
4677
  const accessKey = process.env.AWS_ACCESS_KEY_ID;
4189
4678
  const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
@@ -4197,28 +4686,58 @@ function checkAWSCredentials() {
4197
4686
  if (region) {
4198
4687
  details.push(`AWS_REGION: ${region}`);
4199
4688
  }
4200
- if (profile || accessKey) {
4689
+ if (!profile && !accessKey) {
4690
+ return {
4691
+ name: "AWS Credentials",
4692
+ status: "warning",
4693
+ message: "No AWS credentials detected",
4694
+ details: [
4695
+ "Set AWS_PROFILE or AWS_ACCESS_KEY_ID for Bedrock judge",
4696
+ "Claude Code connector also requires AWS credentials"
4697
+ ]
4698
+ };
4699
+ }
4700
+ try {
4701
+ const { fromNodeProviderChain } = await import("@aws-sdk/credential-providers");
4702
+ const provider = fromNodeProviderChain({
4703
+ ...profile && { profile }
4704
+ });
4705
+ const creds = await provider();
4706
+ if (creds.expiration && creds.expiration.getTime() < Date.now()) {
4707
+ return {
4708
+ name: "AWS Credentials",
4709
+ status: "error",
4710
+ message: "AWS credentials are expired",
4711
+ details: [
4712
+ ...details,
4713
+ `Expired: ${creds.expiration.toISOString()}`,
4714
+ profile ? `Run: aws sso login --profile ${profile}` : "Refresh your AWS credentials"
4715
+ ]
4716
+ };
4717
+ }
4201
4718
  return {
4202
4719
  name: "AWS Credentials",
4203
4720
  status: "ok",
4204
- message: profile ? `Profile: ${profile}` : "Using access key",
4721
+ message: profile ? `Profile: ${profile} (validated)` : "Using access key (validated)",
4205
4722
  details: details.length > 0 ? details : void 0
4206
4723
  };
4724
+ } catch (err) {
4725
+ return {
4726
+ name: "AWS Credentials",
4727
+ status: "error",
4728
+ message: `AWS credentials invalid: ${err.message}`,
4729
+ details: [
4730
+ ...details,
4731
+ "Run: aws sts get-caller-identity to debug",
4732
+ profile ? `Run: aws sso login --profile ${profile}` : "Check AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY"
4733
+ ]
4734
+ };
4207
4735
  }
4208
- return {
4209
- name: "AWS Credentials",
4210
- status: "warning",
4211
- message: "No AWS credentials detected",
4212
- details: [
4213
- "Set AWS_PROFILE or AWS_ACCESS_KEY_ID for Bedrock judge",
4214
- "Claude Code connector also requires AWS credentials"
4215
- ]
4216
- };
4217
4736
  }
4218
4737
  async function checkClaudeCodeCLI() {
4219
- const { execSync: execSync2 } = await import("child_process");
4738
+ const { execSync: execSync3 } = await import("child_process");
4220
4739
  try {
4221
- execSync2("which claude", { stdio: "pipe" });
4740
+ execSync3("which claude", { stdio: "pipe" });
4222
4741
  return {
4223
4742
  name: "Claude Code CLI",
4224
4743
  status: "ok",
@@ -4303,6 +4822,45 @@ function checkOpenSearchObservability() {
4303
4822
  ]
4304
4823
  };
4305
4824
  }
4825
+ async function checkTracesConnectivity() {
4826
+ const port = process.env.PORT || "4001";
4827
+ try {
4828
+ const response = await fetch(`http://localhost:${port}/api/traces/health`, {
4829
+ signal: AbortSignal.timeout(5e3)
4830
+ });
4831
+ const data = await response.json();
4832
+ if (data.status === "ok") {
4833
+ return {
4834
+ name: "Traces Connectivity",
4835
+ status: "ok",
4836
+ message: "OpenSearch traces index is accessible"
4837
+ };
4838
+ }
4839
+ if (data.status === "sample_only") {
4840
+ return {
4841
+ name: "Traces Connectivity",
4842
+ status: "ok",
4843
+ message: "Sample data only (no observability backend)"
4844
+ };
4845
+ }
4846
+ const details = [];
4847
+ if (data.error) details.push(data.error);
4848
+ if (data.suggestion) details.push(data.suggestion);
4849
+ return {
4850
+ name: "Traces Connectivity",
4851
+ status: "error",
4852
+ message: data.errorCategory === "auth" ? "Authentication failed \u2014 credentials may be expired" : `Connection error: ${data.error || "unknown"}`,
4853
+ details: details.length > 0 ? details : void 0
4854
+ };
4855
+ } catch {
4856
+ return {
4857
+ name: "Traces Connectivity",
4858
+ status: "ok",
4859
+ message: "Server not running (skipped)",
4860
+ details: ["Start the server first: npm run dev:server"]
4861
+ };
4862
+ }
4863
+ }
4306
4864
  function displayResults2(results) {
4307
4865
  console.log(chalk6.bold("\n Configuration Check\n"));
4308
4866
  for (const result of results) {
@@ -4345,12 +4903,13 @@ function createDoctorCommand() {
4345
4903
  }
4346
4904
  results.push(checkConfigFile());
4347
4905
  results.push(checkEnvFile());
4348
- results.push(checkAWSCredentials());
4906
+ results.push(await checkAWSCredentials());
4349
4907
  results.push(await checkClaudeCodeCLI());
4350
4908
  results.push(checkAgents(config));
4351
4909
  results.push(checkConnectors());
4352
4910
  results.push(checkOpenSearchStorage());
4353
4911
  results.push(checkOpenSearchObservability());
4912
+ results.push(await checkTracesConnectivity());
4354
4913
  if (options.output === "json") {
4355
4914
  console.log(JSON.stringify(results, null, 2));
4356
4915
  } else {
@@ -4898,38 +5457,768 @@ function createCompareServicesCommand() {
4898
5457
  return cmd;
4899
5458
  }
4900
5459
 
4901
- // cli/index.ts
5460
+ // cli/commands/remote.ts
5461
+ import { Command as Command10 } from "commander";
5462
+ import chalk10 from "chalk";
5463
+ import fs2 from "fs";
5464
+ import path2 from "path";
5465
+ var CONFIG_FILENAME2 = "agent-health.config.json";
5466
+ function getConfigPath() {
5467
+ return path2.join(process.cwd(), CONFIG_FILENAME2);
5468
+ }
5469
+ function readConfig() {
5470
+ const filePath = getConfigPath();
5471
+ if (!fs2.existsSync(filePath)) return {};
5472
+ try {
5473
+ return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
5474
+ } catch {
5475
+ return {};
5476
+ }
5477
+ }
5478
+ function writeConfig(config) {
5479
+ fs2.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
5480
+ }
5481
+ function getRemoteServers(config) {
5482
+ return Array.isArray(config.remoteServers) ? config.remoteServers : [];
5483
+ }
5484
+ function createRemoteCommand() {
5485
+ const remote = new Command10("remote").description("Manage remote agent-health server connections");
5486
+ 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) => {
5487
+ const config = readConfig();
5488
+ const servers = getRemoteServers(config);
5489
+ if (servers.some((s) => s.name === options.name)) {
5490
+ console.error(chalk10.red(`
5491
+ Error: Server "${options.name}" already exists. Use 'remote remove' first.
5492
+ `));
5493
+ process.exit(1);
5494
+ }
5495
+ const server = { name: options.name, url: options.url.replace(/\/$/, "") };
5496
+ if (options.apiKey) server.apiKey = options.apiKey;
5497
+ servers.push(server);
5498
+ config.remoteServers = servers;
5499
+ writeConfig(config);
5500
+ console.log(chalk10.green(`
5501
+ Added remote server: ${options.name} (${options.url})
5502
+ `));
5503
+ });
5504
+ remote.command("remove").description("Remove a remote server").argument("<name>", "Server name to remove").action((name) => {
5505
+ const config = readConfig();
5506
+ const servers = getRemoteServers(config);
5507
+ const idx = servers.findIndex((s) => s.name === name);
5508
+ if (idx === -1) {
5509
+ console.error(chalk10.red(`
5510
+ Error: Server "${name}" not found.
5511
+ `));
5512
+ process.exit(1);
5513
+ }
5514
+ servers.splice(idx, 1);
5515
+ config.remoteServers = servers;
5516
+ writeConfig(config);
5517
+ console.log(chalk10.green(`
5518
+ Removed remote server: ${name}
5519
+ `));
5520
+ });
5521
+ remote.command("list").description("List configured remote servers").action(() => {
5522
+ const config = readConfig();
5523
+ const servers = getRemoteServers(config);
5524
+ if (servers.length === 0) {
5525
+ console.log(chalk10.gray("\n No remote servers configured.\n"));
5526
+ console.log(chalk10.gray(" Add one with: agent-health remote add --name <name> --url <url>\n"));
5527
+ return;
5528
+ }
5529
+ console.log(chalk10.cyan(`
5530
+ Remote Servers (${servers.length}):
5531
+ `));
5532
+ for (const s of servers) {
5533
+ const auth = s.apiKey ? chalk10.green(" [auth]") : chalk10.gray(" [no auth]");
5534
+ console.log(` ${chalk10.bold(s.name)} ${s.url}${auth}`);
5535
+ }
5536
+ console.log("");
5537
+ });
5538
+ remote.command("test").description("Test connectivity to all remote servers").action(async () => {
5539
+ const config = readConfig();
5540
+ const servers = getRemoteServers(config);
5541
+ if (servers.length === 0) {
5542
+ console.log(chalk10.gray("\n No remote servers configured.\n"));
5543
+ return;
5544
+ }
5545
+ console.log(chalk10.cyan(`
5546
+ Testing ${servers.length} remote server(s)...
5547
+ `));
5548
+ for (const s of servers) {
5549
+ try {
5550
+ const headers = {};
5551
+ if (s.apiKey) headers["Authorization"] = `Bearer ${s.apiKey}`;
5552
+ const controller = new AbortController();
5553
+ const timer = setTimeout(() => controller.abort(), 5e3);
5554
+ const response = await fetch(`${s.url}/api/coding-agents/available`, {
5555
+ headers,
5556
+ signal: controller.signal
5557
+ });
5558
+ clearTimeout(timer);
5559
+ if (response.ok) {
5560
+ const data = await response.json();
5561
+ const agentCount = data.agents?.length ?? 0;
5562
+ console.log(chalk10.green(` \u2713 ${s.name} \u2014 OK (${agentCount} agents detected)`));
5563
+ } else {
5564
+ console.log(chalk10.red(` \u2717 ${s.name} \u2014 HTTP ${response.status} ${response.statusText}`));
5565
+ }
5566
+ } catch (error) {
5567
+ const msg = error instanceof Error ? error.message : String(error);
5568
+ console.log(chalk10.red(` \u2717 ${s.name} \u2014 ${msg}`));
5569
+ }
5570
+ }
5571
+ console.log("");
5572
+ });
5573
+ return remote;
5574
+ }
5575
+
5576
+ // cli/commands/configure.ts
5577
+ import { Command as Command11 } from "commander";
5578
+ import chalk11 from "chalk";
5579
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync5 } from "fs";
5580
+ import { join as join4 } from "path";
5581
+ import { execSync as execSync2, spawnSync } from "child_process";
5582
+ var CONFIG_FILENAME3 = "agent-health.config.json";
5583
+ function readConfig2() {
5584
+ const filePath = join4(process.cwd(), CONFIG_FILENAME3);
5585
+ if (!existsSync5(filePath)) return {};
5586
+ const raw = readFileSync3(filePath, "utf-8");
5587
+ try {
5588
+ const parsed = JSON.parse(raw);
5589
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5590
+ throw new Error(`Config file must contain a JSON object: ${filePath}`);
5591
+ }
5592
+ return parsed;
5593
+ } catch (err) {
5594
+ if (err instanceof SyntaxError) {
5595
+ throw new Error(`Failed to parse ${filePath}: ${err.message}. Fix the JSON syntax or delete the file to start fresh.`);
5596
+ }
5597
+ throw err;
5598
+ }
5599
+ }
5600
+ function writeConfig2(config) {
5601
+ const filePath = join4(process.cwd(), CONFIG_FILENAME3);
5602
+ writeFileSync5(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
5603
+ }
5604
+ function validateInput(value, label) {
5605
+ if (/[;&|`$(){}[\]<>!#'"\\\n\r]/.test(value)) {
5606
+ throw new Error(`Invalid ${label}: contains disallowed characters`);
5607
+ }
5608
+ }
5609
+ function getStackOutputs(stackName, region, profile) {
5610
+ validateInput(stackName, "stack name");
5611
+ if (region) validateInput(region, "region");
5612
+ if (profile) validateInput(profile, "profile");
5613
+ const args = ["cloudformation", "describe-stacks", "--stack-name", stackName, "--output", "json"];
5614
+ if (region) args.push("--region", region);
5615
+ if (profile) args.push("--profile", profile);
5616
+ try {
5617
+ const result = spawnSync("aws", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5618
+ if (result.status !== 0) {
5619
+ const stderr = (result.stderr || "").trim();
5620
+ if (stderr.includes("Unable to locate credentials")) {
5621
+ throw new Error("AWS credentials not configured. Run `aws configure` or set AWS_PROFILE.");
5622
+ }
5623
+ throw new Error(stderr || `AWS CLI exited with code ${result.status}`);
5624
+ }
5625
+ const parsed = JSON.parse(result.stdout);
5626
+ const stacks = parsed.Stacks || [];
5627
+ if (stacks.length === 0) {
5628
+ throw new Error(`Stack '${stackName}' not found`);
5629
+ }
5630
+ const stack = stacks[0];
5631
+ if (!stack.Outputs || stack.Outputs.length === 0) {
5632
+ throw new Error(`Stack '${stackName}' has no outputs (status: ${stack.StackStatus})`);
5633
+ }
5634
+ return stack.Outputs;
5635
+ } catch (err) {
5636
+ if (err instanceof Error && (err.message.includes("not found") || err.message.includes("credentials") || err.message.includes("Invalid"))) {
5637
+ throw err;
5638
+ }
5639
+ throw new Error(`Failed to describe stack '${stackName}': ${err instanceof Error ? err.message : err}`);
5640
+ }
5641
+ }
5642
+ function createConfigureCommand() {
5643
+ const cmd = new Command11("configure").description("Configure Agent Health from infrastructure outputs").option("--from-stack <stackName>", "Import observability config from a CloudFormation stack").option("--region <region>", "AWS region for the CloudFormation stack").option("--profile <profile>", "AWS CLI profile to use").option("--dry-run", "Show what would be written without making changes").action(async (options) => {
5644
+ if (options.fromStack) {
5645
+ await configureFromStack(options.fromStack, options.region, options.profile, options.dryRun);
5646
+ } else {
5647
+ console.log(chalk11.yellow("\n No configuration source specified.\n"));
5648
+ console.log(chalk11.gray(" Usage:"));
5649
+ console.log(chalk11.gray(" agent-health configure --from-stack <stack-name>"));
5650
+ console.log(chalk11.gray(" agent-health configure --from-stack AgentHealthObservability --region us-west-2"));
5651
+ console.log(chalk11.gray(" agent-health configure --from-stack AgentHealthObservability --dry-run\n"));
5652
+ }
5653
+ });
5654
+ return cmd;
5655
+ }
5656
+ async function configureFromStack(stackName, region, profile, dryRun) {
5657
+ console.log(chalk11.cyan(`
5658
+ Importing configuration from CloudFormation stack: ${chalk11.bold(stackName)}
5659
+ `));
5660
+ try {
5661
+ execSync2("aws --version", { stdio: ["pipe", "pipe", "pipe"] });
5662
+ } catch {
5663
+ console.error(chalk11.red(" AWS CLI is not installed. Install it from https://aws.amazon.com/cli/\n"));
5664
+ process.exit(1);
5665
+ }
5666
+ let outputs;
5667
+ try {
5668
+ outputs = getStackOutputs(stackName, region, profile);
5669
+ } catch (err) {
5670
+ console.error(chalk11.red(` ${err instanceof Error ? err.message : err}
5671
+ `));
5672
+ process.exit(1);
5673
+ }
5674
+ const outputMap = new Map(outputs.map((o) => [o.OutputKey, o.OutputValue]));
5675
+ const endpoint = outputMap.get("OpenSearchEndpoint");
5676
+ const osisEndpoint = outputMap.get("OSISTraceIngestEndpoint") || outputMap.get("OSISIngestEndpoint");
5677
+ const stackRegion = outputMap.get("Region") || region;
5678
+ const ingestionRoleArn = outputMap.get("IngestionRoleArn");
5679
+ if (!endpoint) {
5680
+ console.error(chalk11.red(" Stack does not have an OpenSearchEndpoint output."));
5681
+ console.error(chalk11.gray(" Available outputs: " + outputs.map((o) => o.OutputKey).join(", ") + "\n"));
5682
+ process.exit(1);
5683
+ }
5684
+ const observabilityConfig = {
5685
+ endpoint,
5686
+ authType: "sigv4",
5687
+ awsRegion: stackRegion,
5688
+ awsService: "es",
5689
+ tlsSkipVerify: false
5690
+ };
5691
+ console.log(chalk11.green(" Stack outputs found:"));
5692
+ console.log(chalk11.gray(` OpenSearch Endpoint: ${endpoint}`));
5693
+ if (osisEndpoint) {
5694
+ console.log(chalk11.gray(` OSIS Ingest Endpoint: ${osisEndpoint}`));
5695
+ }
5696
+ if (stackRegion) {
5697
+ console.log(chalk11.gray(` Region: ${stackRegion}`));
5698
+ }
5699
+ if (ingestionRoleArn) {
5700
+ console.log(chalk11.gray(` Ingestion Role: ${ingestionRoleArn}`));
5701
+ }
5702
+ console.log();
5703
+ if (dryRun) {
5704
+ console.log(chalk11.yellow(" Dry run \u2014 would write this to agent-health.config.json:\n"));
5705
+ console.log(chalk11.gray(JSON.stringify({ observability: observabilityConfig }, null, 2)));
5706
+ console.log();
5707
+ return;
5708
+ }
5709
+ const config = readConfig2();
5710
+ if (config.observability) {
5711
+ console.log(chalk11.yellow(" Existing observability config found \u2014 overwriting.\n"));
5712
+ }
5713
+ config.observability = observabilityConfig;
5714
+ writeConfig2(config);
5715
+ console.log(chalk11.green(` \u2713 Observability config written to ${CONFIG_FILENAME3}
5716
+ `));
5717
+ if (osisEndpoint) {
5718
+ console.log(chalk11.cyan(" Next step: Configure your agent to send traces to:"));
5719
+ console.log(chalk11.bold(` OTEL_EXPORTER_OTLP_ENDPOINT=${osisEndpoint}
5720
+ `));
5721
+ }
5722
+ if (ingestionRoleArn) {
5723
+ console.log(chalk11.gray(` Your agents should assume this role for SigV4 auth:`));
5724
+ console.log(chalk11.gray(` ${ingestionRoleArn}
5725
+ `));
5726
+ }
5727
+ console.log(chalk11.green(" Done! Start Agent Health with: npx @opensearch-project/agent-health\n"));
5728
+ }
5729
+
5730
+ // cli/commands/kill.ts
5731
+ import { Command as Command12 } from "commander";
5732
+ import chalk12 from "chalk";
5733
+
5734
+ // server/services/observioAgent.ts
5735
+ import { join as join5, dirname as dirname3 } from "path";
5736
+ import { fileURLToPath as fileURLToPath3 } from "url";
5737
+ import net2 from "net";
4902
5738
  var __filename3 = fileURLToPath3(import.meta.url);
4903
5739
  var __dirname3 = dirname3(__filename3);
4904
- var packageJsonPath2 = join4(__dirname3, "..", "..", "package.json");
5740
+ var OBSERVIO_PORT = 3001;
5741
+ var observioChild = null;
5742
+ function isPortFree(port) {
5743
+ return new Promise((resolve5) => {
5744
+ let resolved = false;
5745
+ const done = (value) => {
5746
+ if (!resolved) {
5747
+ resolved = true;
5748
+ socket.destroy();
5749
+ resolve5(value);
5750
+ }
5751
+ };
5752
+ const socket = new net2.Socket();
5753
+ socket.setTimeout(1e3);
5754
+ socket.once("connect", () => done(false));
5755
+ socket.once("timeout", () => done(true));
5756
+ socket.once("error", () => done(true));
5757
+ socket.connect(port, "localhost");
5758
+ });
5759
+ }
5760
+ async function killObservioAgent(port = OBSERVIO_PORT) {
5761
+ if (observioChild && !observioChild.killed) {
5762
+ try {
5763
+ observioChild.kill("SIGTERM");
5764
+ for (let i = 0; i < 10; i++) {
5765
+ await new Promise((r) => setTimeout(r, 500));
5766
+ if (observioChild === null || observioChild.killed) break;
5767
+ }
5768
+ if (observioChild && !observioChild.killed) {
5769
+ observioChild.kill("SIGKILL");
5770
+ }
5771
+ observioChild = null;
5772
+ } catch {
5773
+ }
5774
+ for (let i = 0; i < 10; i++) {
5775
+ await new Promise((r) => setTimeout(r, 500));
5776
+ if (await isPortFree(port)) return true;
5777
+ }
5778
+ }
5779
+ const free = await isPortFree(port);
5780
+ if (free) return false;
5781
+ console.warn(` [observio] Port ${port} is in use but not by a tracked process. Use 'lsof -i :${port}' to investigate.`);
5782
+ return false;
5783
+ }
5784
+
5785
+ // cli/commands/kill.ts
5786
+ function createKillCommand() {
5787
+ const command = new Command12("kill").description("Kill a running agent process").argument("<target>", "What to kill: sample-agent").action(async (target) => {
5788
+ switch (target) {
5789
+ case "sample-agent": {
5790
+ const free = await isPortFree(OBSERVIO_PORT);
5791
+ if (free) {
5792
+ console.log(chalk12.yellow(` No process found on port ${OBSERVIO_PORT}`));
5793
+ return;
5794
+ }
5795
+ const killed = await killObservioAgent();
5796
+ if (killed) {
5797
+ console.log(chalk12.green(" \u2713 Sample agent stopped"));
5798
+ } else {
5799
+ console.log(chalk12.red(" \u2717 Failed to stop sample agent"));
5800
+ process.exitCode = 1;
5801
+ }
5802
+ break;
5803
+ }
5804
+ default:
5805
+ console.error(chalk12.red(` Unknown target: ${target}`));
5806
+ console.log(` Available targets: ${chalk12.cyan("sample-agent")}`);
5807
+ process.exitCode = 1;
5808
+ }
5809
+ });
5810
+ return command;
5811
+ }
5812
+
5813
+ // cli/commands/setup-telemetry.ts
5814
+ import { Command as Command13 } from "commander";
5815
+ import chalk13 from "chalk";
5816
+ import { existsSync as existsSync6, readFileSync as readFileSync4, appendFileSync, writeFileSync as writeFileSync6 } from "fs";
5817
+ import { join as join6, dirname as dirname4 } from "path";
5818
+ import { spawnSync as spawnSync2 } from "child_process";
5819
+ import { homedir } from "os";
5820
+ import { fileURLToPath as fileURLToPath4 } from "url";
5821
+ var __dirname4 = dirname4(fileURLToPath4(import.meta.url));
5822
+ function getCfnTemplatePath() {
5823
+ const candidates = [
5824
+ join6(__dirname4, "..", "..", "deployment", "cloudformation", "agent-health-observability.yaml"),
5825
+ join6(__dirname4, "..", "deployment", "cloudformation", "agent-health-observability.yaml")
5826
+ ];
5827
+ for (const p of candidates) {
5828
+ if (existsSync6(p)) return p;
5829
+ }
5830
+ return candidates[0];
5831
+ }
5832
+ var RC_BLOCK_START = "# --- Agent Health: Claude Code Telemetry ---";
5833
+ var RC_BLOCK_END = "# --- End Agent Health Telemetry ---";
5834
+ function validateInput2(value, label) {
5835
+ if (/[;&|`$(){}[\]<>!#'"\\\n\r]/.test(value)) {
5836
+ throw new Error(`Invalid ${label}: contains disallowed characters`);
5837
+ }
5838
+ }
5839
+ function detectRcFile() {
5840
+ const shellEnv = process.env.SHELL || "";
5841
+ const home = homedir();
5842
+ if (shellEnv.includes("zsh")) {
5843
+ return { shell: "zsh", rcPath: join6(home, ".zshrc") };
5844
+ }
5845
+ if (shellEnv.includes("fish")) {
5846
+ return { shell: "fish", rcPath: join6(home, ".config", "fish", "config.fish") };
5847
+ }
5848
+ const bashrc = join6(home, ".bashrc");
5849
+ const profile = join6(home, ".bash_profile");
5850
+ return { shell: "bash", rcPath: existsSync6(bashrc) ? bashrc : profile };
5851
+ }
5852
+ function rcFileHasTelemetryBlock(rcPath) {
5853
+ if (!existsSync6(rcPath)) return false;
5854
+ const content = readFileSync4(rcPath, "utf-8");
5855
+ return content.includes(RC_BLOCK_START);
5856
+ }
5857
+ function getStackOutputs2(stackName, region, profile) {
5858
+ validateInput2(stackName, "stack name");
5859
+ if (region) validateInput2(region, "region");
5860
+ if (profile) validateInput2(profile, "profile");
5861
+ const args = ["cloudformation", "describe-stacks", "--stack-name", stackName, "--output", "json"];
5862
+ if (region) args.push("--region", region);
5863
+ if (profile) args.push("--profile", profile);
5864
+ const result = spawnSync2("aws", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5865
+ if (result.status !== 0) {
5866
+ const stderr = (result.stderr || "").trim();
5867
+ if (stderr.includes("Unable to locate credentials")) {
5868
+ throw new Error("AWS credentials not configured. Run `aws configure` or set AWS_PROFILE.");
5869
+ }
5870
+ if (stderr.includes("does not exist")) {
5871
+ const templatePath = getCfnTemplatePath();
5872
+ throw new Error(`Stack '${stackName}' not found. Deploy it first:
5873
+ npx @goyamegh/agent-health setup-telemetry --deploy
5874
+ Or manually:
5875
+ aws cloudformation deploy --template-file ${templatePath} --stack-name ${stackName} --capabilities CAPABILITY_NAMED_IAM`);
5876
+ }
5877
+ throw new Error(stderr || `AWS CLI exited with code ${result.status}`);
5878
+ }
5879
+ const parsed = JSON.parse(result.stdout);
5880
+ const stacks = parsed.Stacks || [];
5881
+ if (stacks.length === 0) {
5882
+ throw new Error(`Stack '${stackName}' not found`);
5883
+ }
5884
+ const outputs = stacks[0].Outputs || [];
5885
+ const outputMap = new Map(outputs.map((o) => [o.OutputKey, o.OutputValue]));
5886
+ const otlpEndpoint = outputMap.get("OTLPProxyApiEndpoint") || outputMap.get("OTLPIngestEndpoint");
5887
+ if (!otlpEndpoint) {
5888
+ const available = outputs.map((o) => o.OutputKey).join(", ");
5889
+ throw new Error(`Stack '${stackName}' has no OTLP endpoint output.
5890
+ Available outputs: ${available}
5891
+ Make sure the stack includes the API Gateway OTLP proxy.`);
5892
+ }
5893
+ const opensearchEndpoint = outputMap.get("OpenSearchEndpoint") || outputMap.get("DomainEndpoint");
5894
+ const stackRegion = outputMap.get("Region") || region;
5895
+ return { otlpEndpoint, opensearchEndpoint, region: stackRegion };
5896
+ }
5897
+ var CONFIG_FILENAME4 = "agent-health.config.json";
5898
+ function writeServerConfig(opensearchEndpoint, region) {
5899
+ const fullEndpoint = opensearchEndpoint.startsWith("https://") ? opensearchEndpoint : `https://${opensearchEndpoint}`;
5900
+ const observability = {
5901
+ endpoint: fullEndpoint,
5902
+ authType: "sigv4",
5903
+ awsRegion: region,
5904
+ awsService: "es"
5905
+ };
5906
+ const paths = [
5907
+ join6(process.cwd(), CONFIG_FILENAME4),
5908
+ join6(homedir(), CONFIG_FILENAME4)
5909
+ ];
5910
+ const uniquePaths = [...new Set(paths)];
5911
+ const written = [];
5912
+ for (const configPath of uniquePaths) {
5913
+ let config = {};
5914
+ if (existsSync6(configPath)) {
5915
+ try {
5916
+ config = JSON.parse(readFileSync4(configPath, "utf-8"));
5917
+ } catch {
5918
+ }
5919
+ }
5920
+ config.observability = observability;
5921
+ writeFileSync6(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
5922
+ written.push(configPath);
5923
+ }
5924
+ return written;
5925
+ }
5926
+ function isClaudeInstalled() {
5927
+ const result = spawnSync2("which", ["claude"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5928
+ return result.status === 0;
5929
+ }
5930
+ function isAwsCliInstalled() {
5931
+ const result = spawnSync2("aws", ["--version"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5932
+ return result.status === 0;
5933
+ }
5934
+ async function testEndpoint(endpoint) {
5935
+ try {
5936
+ const url = endpoint.replace(/\/+$/, "") + "/v1/traces";
5937
+ const controller = new AbortController();
5938
+ const timeout = setTimeout(() => controller.abort(), 1e4);
5939
+ const res = await fetch(url, {
5940
+ method: "POST",
5941
+ headers: { "Content-Type": "application/x-protobuf" },
5942
+ body: new Uint8Array(0),
5943
+ signal: controller.signal
5944
+ });
5945
+ clearTimeout(timeout);
5946
+ if (res.ok || res.status === 400) {
5947
+ return { ok: true, message: `Endpoint reachable (HTTP ${res.status})` };
5948
+ }
5949
+ if (res.status === 403) {
5950
+ return { ok: true, message: "Endpoint reachable (HTTP 403 \u2014 IAM auth required, which is expected)" };
5951
+ }
5952
+ return { ok: false, message: `Endpoint returned HTTP ${res.status}` };
5953
+ } catch (err) {
5954
+ const msg = err instanceof Error ? err.message : String(err);
5955
+ if (msg.includes("abort")) {
5956
+ return { ok: false, message: "Endpoint timed out after 10s" };
5957
+ }
5958
+ return { ok: false, message: `Connection failed: ${msg}` };
5959
+ }
5960
+ }
5961
+ function buildRcBlock(endpoint) {
5962
+ const lines = [
5963
+ RC_BLOCK_START,
5964
+ `export CLAUDE_CODE_ENABLE_TELEMETRY=1`,
5965
+ `export OTEL_METRICS_EXPORTER=otlp`,
5966
+ `export OTEL_LOGS_EXPORTER=otlp`,
5967
+ `export OTEL_TRACES_EXPORTER=otlp`,
5968
+ `export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`,
5969
+ `export OTEL_EXPORTER_OTLP_ENDPOINT=${endpoint}`,
5970
+ "",
5971
+ `# cc-otel: Launch Claude Code with telemetry enabled`,
5972
+ `alias cc-otel="export AWS_PROFILE=Bedrock && export CLAUDE_CODE_USE_BEDROCK=1 && export DISABLE_PROMPT_CACHING=1 && export DISABLE_ERROR_REPORTING=1 && export DISABLE_TELEMETRY=0 && export AWS_REGION=us-east-1 && export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 && export CLAUDE_CODE_ENABLE_TELEMETRY=1 && export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 && export OTEL_METRICS_EXPORTER=otlp && export OTEL_LOGS_EXPORTER=otlp && export OTEL_TRACES_EXPORTER=otlp && export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf && export OTEL_EXPORTER_OTLP_ENDPOINT=${endpoint} && claude"`,
5973
+ RC_BLOCK_END
5974
+ ];
5975
+ return "\n" + lines.join("\n") + "\n";
5976
+ }
5977
+ function createSetupTelemetryCommand() {
5978
+ const command = new Command13("setup-telemetry").description("Configure Claude Code to send telemetry to Agent Health").option("--stack <name>", "CloudFormation stack name", "AgentHealthObservability").option("--region <region>", "AWS region for the CloudFormation stack").option("--profile <profile>", "AWS CLI profile to use").option("--endpoint <url>", "OTLP endpoint URL (skip stack lookup)").option("--dry-run", "Show what would be written without making changes").option("--skip-rc", "Print env vars without writing to shell rc file").option("--status", "Check current telemetry configuration status").option("--deploy", "Deploy the CloudFormation stack before configuring telemetry").option("--force", "Replace existing telemetry block in shell rc file").action(async (options) => {
5979
+ console.log(chalk13.cyan.bold("\n Agent Health \u2014 Claude Code Telemetry Setup\n"));
5980
+ if (options.status) {
5981
+ await showStatus();
5982
+ return;
5983
+ }
5984
+ console.log(chalk13.bold(" Checking prerequisites...\n"));
5985
+ if (!isClaudeInstalled()) {
5986
+ console.log(chalk13.yellow(" \u26A0 Claude Code CLI not found"));
5987
+ console.log(chalk13.gray(" Install: npm install -g @anthropic-ai/claude-code\n"));
5988
+ } else {
5989
+ console.log(chalk13.green(" \u2713 Claude Code CLI installed"));
5990
+ }
5991
+ if (options.deploy && !options.endpoint) {
5992
+ if (!isAwsCliInstalled()) {
5993
+ console.error(chalk13.red(" \u2717 AWS CLI not found. Install it first or use --endpoint <url>.\n"));
5994
+ process.exit(1);
5995
+ }
5996
+ console.log(chalk13.green(" \u2713 AWS CLI installed"));
5997
+ const templatePath = getCfnTemplatePath();
5998
+ if (!existsSync6(templatePath)) {
5999
+ console.error(chalk13.red(`
6000
+ \u2717 CFN template not found at ${templatePath}`));
6001
+ console.error(chalk13.gray(" This can happen if running from source. Try: npx @goyamegh/agent-health setup-telemetry --deploy\n"));
6002
+ process.exit(1);
6003
+ }
6004
+ console.log(chalk13.gray(`
6005
+ Deploying stack ${chalk13.bold(options.stack)}...`));
6006
+ console.log(chalk13.gray(` Template: ${templatePath}`));
6007
+ console.log(chalk13.gray(" This may take 10-15 minutes on first deploy.\n"));
6008
+ const deployArgs = [
6009
+ "cloudformation",
6010
+ "deploy",
6011
+ "--template-file",
6012
+ templatePath,
6013
+ "--stack-name",
6014
+ options.stack,
6015
+ "--capabilities",
6016
+ "CAPABILITY_NAMED_IAM",
6017
+ "--no-fail-on-empty-changeset"
6018
+ ];
6019
+ if (options.region) deployArgs.push("--region", options.region);
6020
+ if (options.profile) deployArgs.push("--profile", options.profile);
6021
+ const deployResult = spawnSync2("aws", deployArgs, {
6022
+ encoding: "utf-8",
6023
+ stdio: ["pipe", "pipe", "pipe"],
6024
+ timeout: 20 * 60 * 1e3
6025
+ // 20 min timeout for CFN deploy
6026
+ });
6027
+ if (deployResult.status !== 0) {
6028
+ const stderr = (deployResult.stderr || "").trim();
6029
+ console.error(chalk13.red(`
6030
+ \u2717 Stack deployment failed:
6031
+ ${stderr}
6032
+ `));
6033
+ process.exit(1);
6034
+ }
6035
+ console.log(chalk13.green(` \u2713 Stack ${options.stack} deployed successfully`));
6036
+ }
6037
+ let endpoint;
6038
+ let stackOutputs = null;
6039
+ if (options.endpoint) {
6040
+ endpoint = options.endpoint;
6041
+ console.log(chalk13.green(` \u2713 Using provided endpoint: ${endpoint}`));
6042
+ } else {
6043
+ if (!isAwsCliInstalled()) {
6044
+ console.error(chalk13.red(" \u2717 AWS CLI not found. Install it or use --endpoint <url> to skip stack lookup.\n"));
6045
+ process.exit(1);
6046
+ }
6047
+ if (!options.deploy) console.log(chalk13.green(" \u2713 AWS CLI installed"));
6048
+ console.log(chalk13.gray(`
6049
+ Reading stack outputs from ${chalk13.bold(options.stack)}...`));
6050
+ try {
6051
+ stackOutputs = getStackOutputs2(options.stack, options.region, options.profile);
6052
+ endpoint = stackOutputs.otlpEndpoint;
6053
+ console.log(chalk13.green(` \u2713 OTLP endpoint: ${endpoint}`));
6054
+ if (stackOutputs.opensearchEndpoint) {
6055
+ console.log(chalk13.green(` \u2713 OpenSearch endpoint: ${stackOutputs.opensearchEndpoint}`));
6056
+ }
6057
+ } catch (err) {
6058
+ console.error(chalk13.red(`
6059
+ \u2717 ${err instanceof Error ? err.message : err}
6060
+ `));
6061
+ process.exit(1);
6062
+ }
6063
+ }
6064
+ console.log(chalk13.gray("\n Testing OTLP endpoint connectivity..."));
6065
+ const connectivity = await testEndpoint(endpoint);
6066
+ if (connectivity.ok) {
6067
+ console.log(chalk13.green(` \u2713 ${connectivity.message}`));
6068
+ } else {
6069
+ console.log(chalk13.yellow(` \u26A0 ${connectivity.message}`));
6070
+ console.log(chalk13.gray(" Telemetry may not work until the endpoint is reachable."));
6071
+ }
6072
+ if (stackOutputs?.opensearchEndpoint) {
6073
+ const effectiveRegion = stackOutputs.region || options.region;
6074
+ if (effectiveRegion) {
6075
+ if (options.dryRun) {
6076
+ console.log(chalk13.yellow("\n Dry run \u2014 would write server config:"));
6077
+ console.log(chalk13.gray(` OpenSearch endpoint: ${stackOutputs.opensearchEndpoint}`));
6078
+ console.log(chalk13.gray(` Auth: SigV4, Region: ${effectiveRegion}
6079
+ `));
6080
+ } else {
6081
+ const configPaths = writeServerConfig(stackOutputs.opensearchEndpoint, effectiveRegion);
6082
+ for (const p of configPaths) {
6083
+ console.log(chalk13.green(` \u2713 Server config written to ${p}`));
6084
+ }
6085
+ console.log(chalk13.gray(` Agent Health server will read traces from this OpenSearch domain.`));
6086
+ }
6087
+ } else {
6088
+ console.log(chalk13.yellow("\n \u26A0 Could not determine region for server config. Set --region explicitly."));
6089
+ }
6090
+ }
6091
+ const { shell, rcPath } = detectRcFile();
6092
+ console.log(chalk13.gray(`
6093
+ Detected shell: ${shell} \u2192 ${rcPath}`));
6094
+ if (options.dryRun) {
6095
+ console.log(chalk13.yellow("\n Dry run \u2014 would append to " + rcPath + ":\n"));
6096
+ console.log(chalk13.gray(buildRcBlock(endpoint)));
6097
+ console.log(chalk13.yellow(" No changes made.\n"));
6098
+ return;
6099
+ }
6100
+ if (options.skipRc) {
6101
+ console.log(chalk13.yellow("\n Add these to your shell profile:\n"));
6102
+ console.log(chalk13.gray(buildRcBlock(endpoint)));
6103
+ return;
6104
+ }
6105
+ if (rcFileHasTelemetryBlock(rcPath)) {
6106
+ if (options.force) {
6107
+ const content = readFileSync4(rcPath, "utf-8");
6108
+ const regex = new RegExp(`${RC_BLOCK_START}[\\s\\S]*?${RC_BLOCK_END}\\n?`, "g");
6109
+ const cleaned = content.replace(regex, "");
6110
+ writeFileSync6(rcPath, cleaned + buildRcBlock(endpoint), "utf-8");
6111
+ console.log(chalk13.green(`
6112
+ \u2713 Telemetry env vars updated in ${rcPath}`));
6113
+ } else {
6114
+ console.log(chalk13.yellow(`
6115
+ \u26A0 Telemetry block already exists in ${rcPath}`));
6116
+ console.log(chalk13.gray(` Use --force to replace it, or manually remove the block between "${RC_BLOCK_START}" and "${RC_BLOCK_END}".
6117
+ `));
6118
+ }
6119
+ } else {
6120
+ appendFileSync(rcPath, buildRcBlock(endpoint));
6121
+ console.log(chalk13.green(`
6122
+ \u2713 Telemetry env vars written to ${rcPath}`));
6123
+ }
6124
+ console.log(chalk13.cyan.bold("\n Next steps:\n"));
6125
+ console.log(chalk13.gray(` 1. Reload your shell: ${chalk13.white(`source ${rcPath}`)}`));
6126
+ console.log(chalk13.gray(` 2. Start Claude Code: ${chalk13.white("cc-otel")} (launches Claude with telemetry)`));
6127
+ console.log(chalk13.gray(` 3. View traces: ${chalk13.white("http://localhost:4001/coding-agents")}`));
6128
+ console.log(chalk13.gray(`
6129
+ The ${chalk13.white("cc-otel")} alias combines Bedrock auth + OTel telemetry + Claude launch.
6130
+ `));
6131
+ });
6132
+ return command;
6133
+ }
6134
+ async function showStatus() {
6135
+ console.log(chalk13.bold(" Current Telemetry Status\n"));
6136
+ const checks = [
6137
+ { name: "Telemetry enabled", envVar: "CLAUDE_CODE_ENABLE_TELEMETRY", expected: "1" },
6138
+ { name: "Traces exporter", envVar: "OTEL_TRACES_EXPORTER", expected: "otlp" },
6139
+ { name: "Logs exporter", envVar: "OTEL_LOGS_EXPORTER", expected: "otlp" },
6140
+ { name: "Metrics exporter", envVar: "OTEL_METRICS_EXPORTER", expected: "otlp" },
6141
+ { name: "OTLP protocol", envVar: "OTEL_EXPORTER_OTLP_PROTOCOL" },
6142
+ { name: "OTLP endpoint", envVar: "OTEL_EXPORTER_OTLP_ENDPOINT" }
6143
+ ];
6144
+ let allOk = true;
6145
+ for (const check of checks) {
6146
+ const value = process.env[check.envVar];
6147
+ if (!value) {
6148
+ console.log(chalk13.yellow(` \u26A0 ${check.name}: ${chalk13.gray("not set")} (${check.envVar})`));
6149
+ allOk = false;
6150
+ } else if (check.expected && value !== check.expected) {
6151
+ console.log(chalk13.yellow(` \u26A0 ${check.name}: ${value} (expected ${check.expected})`));
6152
+ allOk = false;
6153
+ } else {
6154
+ console.log(chalk13.green(` \u2713 ${check.name}: ${value}`));
6155
+ }
6156
+ }
6157
+ console.log("");
6158
+ if (isClaudeInstalled()) {
6159
+ console.log(chalk13.green(" \u2713 Claude Code CLI installed"));
6160
+ } else {
6161
+ console.log(chalk13.yellow(" \u26A0 Claude Code CLI not found"));
6162
+ allOk = false;
6163
+ }
6164
+ const { rcPath } = detectRcFile();
6165
+ if (rcFileHasTelemetryBlock(rcPath)) {
6166
+ console.log(chalk13.green(` \u2713 Telemetry block in ${rcPath}`));
6167
+ } else {
6168
+ console.log(chalk13.yellow(` \u26A0 No telemetry block in ${rcPath}`));
6169
+ allOk = false;
6170
+ }
6171
+ const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
6172
+ if (endpoint) {
6173
+ console.log(chalk13.gray("\n Testing endpoint..."));
6174
+ const result = await testEndpoint(endpoint);
6175
+ if (result.ok) {
6176
+ console.log(chalk13.green(` \u2713 ${result.message}`));
6177
+ } else {
6178
+ console.log(chalk13.yellow(` \u26A0 ${result.message}`));
6179
+ allOk = false;
6180
+ }
6181
+ }
6182
+ console.log("");
6183
+ if (allOk) {
6184
+ console.log(chalk13.green(" All checks passed! Telemetry is configured.\n"));
6185
+ } else {
6186
+ console.log(chalk13.yellow(" Some checks failed. Run `agent-health setup-telemetry` to fix.\n"));
6187
+ }
6188
+ }
6189
+
6190
+ // cli/index.ts
6191
+ var __filename4 = fileURLToPath5(import.meta.url);
6192
+ var __dirname5 = dirname5(__filename4);
6193
+ var packageJsonPath2 = join7(__dirname5, "..", "..", "package.json");
4905
6194
  var version = "0.1.0";
4906
6195
  try {
4907
- const packageJson = JSON.parse(readFileSync3(packageJsonPath2, "utf-8"));
6196
+ const packageJson = JSON.parse(readFileSync5(packageJsonPath2, "utf-8"));
4908
6197
  version = packageJson.version;
4909
6198
  } catch {
4910
6199
  }
4911
6200
  function loadEnvFile(envPath) {
4912
6201
  const absolutePath = resolve4(process.cwd(), envPath);
4913
- if (!existsSync5(absolutePath)) {
4914
- console.error(chalk10.red(`
6202
+ if (!existsSync7(absolutePath)) {
6203
+ console.error(chalk14.red(`
4915
6204
  Error: Environment file not found: ${absolutePath}
4916
6205
  `));
4917
6206
  process.exit(1);
4918
6207
  }
4919
6208
  const result = loadDotenv({ path: absolutePath });
4920
6209
  if (result.error) {
4921
- console.error(chalk10.red(`
6210
+ console.error(chalk14.red(`
4922
6211
  Error loading environment file: ${result.error.message}
4923
6212
  `));
4924
6213
  process.exit(1);
4925
6214
  }
4926
- console.log(chalk10.gray(` Loaded environment from: ${envPath}`));
6215
+ console.log(chalk14.gray(` Loaded environment from: ${envPath}`));
4927
6216
  }
4928
6217
  var defaultEnvPath = resolve4(process.cwd(), ".env");
4929
- if (existsSync5(defaultEnvPath)) {
6218
+ if (existsSync7(defaultEnvPath)) {
4930
6219
  loadDotenv({ path: defaultEnvPath });
4931
6220
  }
4932
- var program = new Command10();
6221
+ var program = new Command14();
4933
6222
  program.name("agent-health").description("Agent Health Evaluation Framework - Evaluate and monitor AI agent performance").version(version).enablePositionalOptions().passThroughOptions().configureHelp({
4934
6223
  sortSubcommands: false,
4935
6224
  // Hide default command list — replaced by grouped custom help below
@@ -4942,81 +6231,105 @@ program.name("agent-health").description("Agent Health Evaluation Framework - Ev
4942
6231
  if (desc) {
4943
6232
  output.push(desc, "");
4944
6233
  }
4945
- output.push(`${chalk10.cyan.bold("Usage:")} ${helper.commandUsage(cmd)}`, "");
6234
+ output.push(`${chalk14.cyan.bold("Usage:")} ${helper.commandUsage(cmd)}`, "");
4946
6235
  const optionList = helper.visibleOptions(cmd).map((opt) => {
4947
6236
  const term = helper.optionTerm(opt);
4948
6237
  const desc2 = helper.optionDescription(opt);
4949
6238
  return ` ${term.padEnd(termWidth)} ${desc2}`;
4950
6239
  }).join("\n");
4951
6240
  if (optionList) {
4952
- output.push(`${chalk10.cyan.bold("Options:")}`, optionList, "");
6241
+ output.push(`${chalk14.cyan.bold("Options:")}`, optionList, "");
4953
6242
  }
4954
6243
  return output.join("\n");
4955
6244
  }
4956
6245
  });
4957
6246
  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)
6247
+ ${chalk14.cyan.bold("Getting Started:")}
6248
+ ${chalk14.yellow("agent-health")} Launch the web UI and evaluation server
6249
+ ${chalk14.yellow("agent-health init")} Generate an agent-health.config.ts file
6250
+ ${chalk14.yellow("agent-health doctor")} Verify your setup (AWS creds, OpenSearch, agents)
6251
+
6252
+ ${chalk14.cyan.bold("Running Evaluations:")}
6253
+ ${chalk14.yellow("agent-health run")} ${chalk14.gray("-t <case> -a <agent>")} Run a single test case against an agent
6254
+ ${chalk14.yellow("agent-health benchmark")} ${chalk14.gray("-f <file>")} Run a full benchmark from a test cases JSON file
6255
+ ${chalk14.yellow("agent-health benchmark")} ${chalk14.gray("-b <id>")} Re-run an existing benchmark
4962
6256
 
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
6257
+ ${chalk14.cyan.bold("Viewing Results:")}
6258
+ ${chalk14.yellow("agent-health list")} ${chalk14.gray("agents|benchmarks|...")} List agents, connectors, test cases, or benchmarks
6259
+ ${chalk14.yellow("agent-health report")} ${chalk14.gray("-b <benchmark>")} Generate an HTML/PDF/JSON report
6260
+ ${chalk14.yellow("agent-health export")} ${chalk14.gray("-b <benchmark>")} Export test cases as re-importable JSON
6261
+ ${chalk14.yellow("agent-health compare-services")} ${chalk14.gray("-s A B")} Compare error patterns between services
4967
6262
 
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
6263
+ ${chalk14.cyan.bold("Remote Servers:")}
6264
+ ${chalk14.yellow("agent-health remote add")} ${chalk14.gray("--name <n> --url <u>")} Add a remote server
6265
+ ${chalk14.yellow("agent-health remote list")} List configured remote servers
6266
+ ${chalk14.yellow("agent-health remote test")} Test connectivity to all remotes
4973
6267
 
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)
6268
+ ${chalk14.cyan.bold("Infrastructure:")}
6269
+ ${chalk14.yellow("agent-health configure")} ${chalk14.gray("--from-stack <name>")} Import config from a CloudFormation stack
6270
+ ${chalk14.yellow("agent-health setup-telemetry")} Configure Claude Code \u2192 Agent Health telemetry
6271
+ ${chalk14.yellow("agent-health setup-telemetry")} ${chalk14.gray("--status")} Check current telemetry status
4977
6272
 
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
6273
+ ${chalk14.cyan.bold("Maintenance:")}
6274
+ ${chalk14.yellow("agent-health migrate")} Migrate legacy benchmark data to current format
6275
+ ${chalk14.yellow("agent-health kill")} ${chalk14.gray("sample-agent")} Stop a running sample agent by name
6276
+ ${chalk14.yellow("agent-health serve")} Start the server (same as default, explicit command)
6277
+
6278
+ ${chalk14.cyan.bold("Examples:")}
6279
+ ${chalk14.gray("$")} npx @opensearch-project/agent-health
6280
+ ${chalk14.gray("$")} npx @opensearch-project/agent-health --port 8080 --no-browser
6281
+ ${chalk14.gray("$")} npx @opensearch-project/agent-health run -t "RCA for 500 errors" -a langgraph
6282
+ ${chalk14.gray("$")} npx @opensearch-project/agent-health benchmark -f ./test-cases.json -a my-agent
6283
+ ${chalk14.gray("$")} npx @opensearch-project/agent-health list agents
6284
+ ${chalk14.gray("$")} npx @opensearch-project/agent-health report -b bench-123 -f pdf -o report.pdf
6285
+ ${chalk14.gray("$")} npx @opensearch-project/agent-health serve --headless --api-key sk-secret
4985
6286
  `);
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");
6287
+ 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");
4987
6288
  program.action(async (options) => {
4988
- console.log(chalk10.cyan.bold(`
6289
+ console.log(chalk14.cyan.bold(`
4989
6290
  Agent Health v${version} - AI Agent Evaluation Framework
4990
6291
  `));
4991
- console.log(chalk10.gray(` Working directory: ${process.cwd()}`));
4992
- console.log(chalk10.gray(` Package directory: ${__dirname3}`));
6292
+ console.log(chalk14.gray(` Working directory: ${process.cwd()}`));
6293
+ console.log(chalk14.gray(` Package directory: ${__dirname5}`));
4993
6294
  if (options.envFile) {
4994
6295
  loadEnvFile(options.envFile);
4995
- } else if (existsSync5(defaultEnvPath)) {
4996
- console.log(chalk10.gray(" Auto-loaded .env from current directory"));
6296
+ } else if (existsSync7(defaultEnvPath)) {
6297
+ console.log(chalk14.gray(" Auto-loaded .env from current directory"));
4997
6298
  }
4998
6299
  const port = parseInt(options.port, 10);
4999
- const spinner = ora5("Starting server...").start();
6300
+ const headless = options.headless || false;
6301
+ const spinner = ora5(headless ? "Starting headless API server..." : "Starting server...").start();
5000
6302
  try {
5001
- await startServer({ port });
5002
- spinner.succeed("Server started");
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)
6303
+ const actualPort = await startServer({ port, headless, apiKey: options.apiKey });
6304
+ spinner.succeed(headless ? "Headless API server started" : "Server started");
6305
+ if (actualPort !== port) {
6306
+ console.log(chalk14.yellow(`
6307
+ Port ${port} was in use, started on port ${actualPort} instead`));
6308
+ }
6309
+ if (headless) {
6310
+ console.log(chalk14.green(`
6311
+ API server running on http://0.0.0.0:${actualPort}`));
6312
+ if (options.apiKey) console.log(chalk14.gray(" API key authentication enabled"));
6313
+ console.log(chalk14.gray(" Mode: headless (API only, no frontend)\n"));
6314
+ } else {
6315
+ console.log(chalk14.gray("\n Configuration:"));
6316
+ console.log(chalk14.gray(` Storage: Sample data (configure OpenSearch for persistence)`));
6317
+ console.log(chalk14.gray(` Agent: Select in UI (Demo Agent for mock, real agents require endpoints)`));
6318
+ console.log(chalk14.gray(` Judge: Select in UI (Demo Judge for mock, Bedrock requires AWS creds)
5007
6319
  `));
5008
- const url = `http://localhost:${port}`;
5009
- console.log(chalk10.green(` Server running at ${chalk10.bold(url)}
6320
+ const url = `http://localhost:${actualPort}`;
6321
+ console.log(chalk14.green(` Server running at ${chalk14.bold(url)}
5010
6322
  `));
5011
- console.log(chalk10.green(` Demo data loaded`));
5012
- if (options.browser !== false) {
5013
- console.log(chalk10.gray(" Opening browser..."));
5014
- await open(url);
6323
+ console.log(chalk14.green(` Demo data loaded`));
6324
+ if (options.browser !== false) {
6325
+ console.log(chalk14.gray(" Opening browser..."));
6326
+ await open(url);
6327
+ }
5015
6328
  }
5016
- console.log(chalk10.gray(" Press Ctrl+C to stop\n"));
6329
+ console.log(chalk14.gray(" Press Ctrl+C to stop\n"));
5017
6330
  } catch (error) {
5018
6331
  spinner.fail("Failed to start server");
5019
- console.error(chalk10.red(`
6332
+ console.error(chalk14.red(`
5020
6333
  Error: ${error instanceof Error ? error.message : error}
5021
6334
  `));
5022
6335
  process.exit(1);
@@ -5031,26 +6344,41 @@ program.addCommand(createDoctorCommand());
5031
6344
  program.addCommand(createInitCommand());
5032
6345
  program.addCommand(createMigrateCommand());
5033
6346
  program.addCommand(createCompareServicesCommand());
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) => {
5035
- console.log(chalk10.cyan.bold(`
6347
+ program.addCommand(createRemoteCommand());
6348
+ program.addCommand(createConfigureCommand());
6349
+ program.addCommand(createKillCommand());
6350
+ program.addCommand(createSetupTelemetryCommand());
6351
+ 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) => {
6352
+ console.log(chalk14.cyan.bold(`
5036
6353
  Agent Health v${version} - AI Agent Evaluation Framework
5037
6354
  `));
5038
6355
  const port = parseInt(options.port, 10);
5039
- const spinner = ora5("Starting server...").start();
6356
+ const headless = options.headless || false;
6357
+ const spinner = ora5(headless ? "Starting headless API server..." : "Starting server...").start();
5040
6358
  try {
5041
- await startServer({ port });
5042
- spinner.succeed("Server started");
5043
- const url = `http://localhost:${port}`;
5044
- console.log(chalk10.green(` Server running at ${chalk10.bold(url)}
6359
+ const actualPort = await startServer({ port, headless, apiKey: options.apiKey });
6360
+ spinner.succeed(headless ? "Headless API server started" : "Server started");
6361
+ if (actualPort !== port) {
6362
+ console.log(chalk14.yellow(`
6363
+ Port ${port} was in use, started on port ${actualPort} instead`));
6364
+ }
6365
+ const url = `http://localhost:${actualPort}`;
6366
+ if (headless) {
6367
+ console.log(chalk14.green(` API server running on http://0.0.0.0:${actualPort}`));
6368
+ if (options.apiKey) console.log(chalk14.gray(" API key authentication enabled"));
6369
+ console.log(chalk14.gray(" Mode: headless (API only, no frontend)\n"));
6370
+ } else {
6371
+ console.log(chalk14.green(` Server running at ${chalk14.bold(url)}
5045
6372
  `));
5046
- if (options.browser !== false) {
5047
- console.log(chalk10.gray(" Opening browser..."));
5048
- await open(url);
6373
+ if (options.browser !== false) {
6374
+ console.log(chalk14.gray(" Opening browser..."));
6375
+ await open(url);
6376
+ }
5049
6377
  }
5050
- console.log(chalk10.gray(" Press Ctrl+C to stop\n"));
6378
+ console.log(chalk14.gray(" Press Ctrl+C to stop\n"));
5051
6379
  } catch (error) {
5052
6380
  spinner.fail("Failed to start server");
5053
- console.error(chalk10.red(`
6381
+ console.error(chalk14.red(`
5054
6382
  Error: ${error instanceof Error ? error.message : error}
5055
6383
  `));
5056
6384
  process.exit(1);
@@ -5059,15 +6387,15 @@ program.command("serve").description("Start the Agent Health server (same as def
5059
6387
  program.on("command:*", (operands) => {
5060
6388
  const unknownCommand = operands[0];
5061
6389
  const availableCommands = program.commands.map((cmd) => cmd.name());
5062
- console.error(chalk10.red(`
6390
+ console.error(chalk14.red(`
5063
6391
  Error: Unknown command '${unknownCommand}'`));
5064
6392
  console.log("");
5065
- console.log(chalk10.cyan(" Available commands:"));
6393
+ console.log(chalk14.cyan(" Available commands:"));
5066
6394
  for (const cmd of availableCommands) {
5067
- console.log(chalk10.gray(` - ${cmd}`));
6395
+ console.log(chalk14.gray(` - ${cmd}`));
5068
6396
  }
5069
6397
  console.log("");
5070
- console.log(chalk10.gray(` Run ${chalk10.cyan("agent-health --help")} for usage information.
6398
+ console.log(chalk14.gray(` Run ${chalk14.cyan("agent-health --help")} for usage information.
5071
6399
  `));
5072
6400
  process.exitCode = 1;
5073
6401
  });