@opensearch-project/agent-health 0.3.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 Command11 } from "commander";
5
- import chalk11 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,6 +26,7 @@ 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);
31
32
  if (options.headless) process.env.AGENT_HEALTH_HEADLESS = "1";
@@ -34,11 +35,28 @@ async function startServer(options) {
34
35
  const serverPath = join(packageRoot, "server", "dist", "app.js");
35
36
  const { createApp } = await import(serverPath);
36
37
  const app = await createApp();
37
- return new Promise((resolve5) => {
38
- app.listen(options.port, "0.0.0.0", () => {
39
- 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
+ });
40
53
  });
41
- });
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;
42
60
  }
43
61
 
44
62
  // cli/commands/list.ts
@@ -101,6 +119,8 @@ var ENV_CONFIG = {
101
119
  // OpenAI-compatible (optional - for OpenAI-compatible judge/agent endpoints)
102
120
  openaiCompatibleApiKey: getEnvVar("OPENAI_COMPATIBLE_API_KEY", ""),
103
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"),
104
124
  // Claude Code Telemetry (optional - for OTEL traces from Claude Code)
105
125
  claudeCodeTelemetryEnabled: getEnvVar("CLAUDE_CODE_TELEMETRY_ENABLED", "false") === "true",
106
126
  otelExporterEndpoint: getEnvVar("OTEL_EXPORTER_OTLP_ENDPOINT", ""),
@@ -136,6 +156,16 @@ var CONNECTOR_TYPE_INFO = {
136
156
  description: "Invokes the Claude Code CLI. Server-only \u2014 use the CLI or benchmark runner.",
137
157
  serverOnly: true
138
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
+ },
139
169
  "mock": {
140
170
  label: "Mock",
141
171
  description: "Built-in demo agent for testing. No real endpoint needed.",
@@ -178,6 +208,15 @@ var DEFAULT_CONFIG = {
178
208
  headers: {},
179
209
  useTraces: false
180
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
+ },
181
220
  {
182
221
  key: "claude-code",
183
222
  name: "Claude Code",
@@ -187,6 +226,30 @@ var DEFAULT_CONFIG = {
187
226
  headers: {},
188
227
  useTraces: ENV_CONFIG.claudeCodeTelemetryEnabled && !!ENV_CONFIG.otelExporterEndpoint,
189
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
190
253
  }
191
254
  ],
192
255
  models: {
@@ -375,6 +438,7 @@ function mergeConfigs(userConfig, defaultConfig) {
375
438
  ...DEFAULT_SERVER_CONFIG,
376
439
  ...userConfig.server
377
440
  };
441
+ const telemetry = userConfig.telemetry ?? {};
378
442
  return {
379
443
  server,
380
444
  agents,
@@ -382,7 +446,8 @@ function mergeConfigs(userConfig, defaultConfig) {
382
446
  connectors,
383
447
  testCases,
384
448
  reporters,
385
- judge
449
+ judge,
450
+ telemetry
386
451
  };
387
452
  }
388
453
  async function loadUserConfig(configPath) {
@@ -1715,11 +1780,150 @@ var OpenAICompatibleConnector = class extends BaseConnector {
1715
1780
  };
1716
1781
  var openaiCompatibleConnector = new OpenAICompatibleConnector();
1717
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
+
1718
1921
  // services/connectors/index.ts
1719
1922
  connectorRegistry.register(aguiStreamingConnector);
1720
1923
  connectorRegistry.register(mockConnector);
1721
1924
  connectorRegistry.register(restConnector);
1722
1925
  connectorRegistry.register(openaiCompatibleConnector);
1926
+ connectorRegistry.register(langgraphConnector);
1723
1927
  console.log("[Connectors] Browser-safe connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
1724
1928
 
1725
1929
  // services/connectors/subprocess/SubprocessConnector.ts
@@ -1763,6 +1967,7 @@ Question: ${prompt}`;
1763
1967
  const command = endpoint || this.config.command;
1764
1968
  const args = this.config.args || [];
1765
1969
  const input = request.payload || this.buildPayload(request);
1970
+ const runId = `subprocess-${Date.now()}`;
1766
1971
  this.debug("Command:", command);
1767
1972
  this.debug("Args:", args);
1768
1973
  this.debug("Input mode:", this.config.inputMode);
@@ -1770,10 +1975,12 @@ Question: ${prompt}`;
1770
1975
  this.debug("Timeout:", this.config.timeout);
1771
1976
  this.debug("Input (first 500 chars):", input.substring(0, 500));
1772
1977
  this.debug("Working dir:", this.config.workingDir || process.cwd());
1978
+ this.debug("Run ID:", runId);
1773
1979
  const env = {
1774
1980
  ...process.env,
1775
1981
  ...this.buildAuthEnv(auth),
1776
- ...this.config.env
1982
+ ...this.config.env,
1983
+ AGENT_EVAL_RUN_ID: runId
1777
1984
  };
1778
1985
  return new Promise((resolve5, reject) => {
1779
1986
  const trajectory = [];
@@ -1832,6 +2039,17 @@ Question: ${prompt}`;
1832
2039
  this.error(`Process exited with code ${code}`);
1833
2040
  this.error("stderr:", stderr);
1834
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
+ }
1835
2053
  const finalTrajectory = this.config.outputParser === "streaming" ? trajectory : this.parseResponse({ stdout, stderr, exitCode: code });
1836
2054
  if (this.config.outputParser !== "streaming") {
1837
2055
  finalTrajectory.forEach((step) => onProgress?.(step));
@@ -1839,7 +2057,7 @@ Question: ${prompt}`;
1839
2057
  this.debug("Resolving with trajectory of", finalTrajectory.length, "steps");
1840
2058
  resolve5({
1841
2059
  trajectory: finalTrajectory,
1842
- runId: `subprocess-${Date.now()}`,
2060
+ runId,
1843
2061
  rawEvents: rawOutput,
1844
2062
  metadata: {
1845
2063
  command,
@@ -1870,6 +2088,12 @@ Question: ${prompt}`;
1870
2088
  });
1871
2089
  this.debug("========== execute() COMPLETED ==========");
1872
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
+ }
1873
2097
  /**
1874
2098
  * Parse streaming output and emit steps in real-time
1875
2099
  */
@@ -1964,6 +2188,7 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
1964
2188
  this.name = "Claude Code CLI";
1965
2189
  this.outputBuffer = "";
1966
2190
  this.thinkingBuffer = "";
2191
+ this.textBuffer = "";
1967
2192
  this.isInThinking = false;
1968
2193
  }
1969
2194
  /**
@@ -2033,11 +2258,17 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
2033
2258
  if (event.delta?.type === "thinking_delta" && event.delta.thinking) {
2034
2259
  this.thinkingBuffer += event.delta.thinking;
2035
2260
  } else if (event.delta?.type === "text_delta" && event.delta.text) {
2036
- 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 = "";
2037
2271
  }
2038
- } else if (event.type === "content_block_stop" && this.thinkingBuffer) {
2039
- steps.push(this.createStep("thinking", this.thinkingBuffer));
2040
- this.thinkingBuffer = "";
2041
2272
  } else if (event.type === "result" && event.result) {
2042
2273
  steps.push(this.createStep(
2043
2274
  "response",
@@ -2083,8 +2314,41 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
2083
2314
  resetState() {
2084
2315
  this.outputBuffer = "";
2085
2316
  this.thinkingBuffer = "";
2317
+ this.textBuffer = "";
2086
2318
  this.isInThinking = false;
2087
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
+ }
2088
2352
  /**
2089
2353
  * Build CLI args from ClaudeCodeConnectorConfig
2090
2354
  */
@@ -2185,9 +2449,214 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
2185
2449
  };
2186
2450
  var claudeCodeConnector = new ClaudeCodeConnector();
2187
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
+
2188
2656
  // services/connectors/server.ts
2189
2657
  connectorRegistry.register(subprocessConnector);
2190
2658
  connectorRegistry.register(claudeCodeConnector);
2659
+ connectorRegistry.register(strandsConnector);
2191
2660
  console.log("[Connectors] Server connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
2192
2661
 
2193
2662
  // cli/utils/serverLifecycle.ts
@@ -2329,20 +2798,35 @@ async function startServer2(port, timeout) {
2329
2798
  });
2330
2799
  let stderrOutput = "";
2331
2800
  let stdoutOutput = "";
2801
+ let resolvePortDetection;
2802
+ const portDetected = new Promise((resolve5) => {
2803
+ resolvePortDetection = resolve5;
2804
+ });
2332
2805
  child.stderr?.on("data", (data) => {
2333
2806
  stderrOutput += data.toString();
2334
2807
  });
2335
2808
  child.stdout?.on("data", (data) => {
2336
- 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
+ }
2337
2815
  });
2338
2816
  let earlyExit = false;
2339
2817
  let exitCode = null;
2340
2818
  child.on("exit", (code) => {
2341
2819
  earlyExit = true;
2342
2820
  exitCode = code;
2821
+ resolvePortDetection(port);
2343
2822
  });
2344
2823
  child.unref();
2345
- 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);
2346
2830
  if (!ready) {
2347
2831
  try {
2348
2832
  child.kill();
@@ -2368,7 +2852,7 @@ ${stdoutOutput}`);
2368
2852
  }
2369
2853
  throw new Error(`Server failed to start within ${timeout}ms on port ${port}`);
2370
2854
  }
2371
- return child;
2855
+ return { child, actualPort };
2372
2856
  }
2373
2857
  function stopServer(process2) {
2374
2858
  try {
@@ -2412,11 +2896,14 @@ async function ensureServer(config) {
2412
2896
  );
2413
2897
  }
2414
2898
  }
2415
- 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
+ }
2416
2903
  return {
2417
2904
  wasStarted: true,
2418
- baseUrl,
2419
- process: serverProcess
2905
+ baseUrl: `http://localhost:${actualPort}`,
2906
+ process: child
2420
2907
  };
2421
2908
  }
2422
2909
  function createServerCleanup(result, isCI) {
@@ -3245,10 +3732,10 @@ function getDefaultModel(config) {
3245
3732
  return Object.keys(config.models)[0] || "claude-sonnet";
3246
3733
  }
3247
3734
  async function commandExists(command) {
3248
- const { execSync: execSync2 } = await import("child_process");
3735
+ const { execSync: execSync3 } = await import("child_process");
3249
3736
  const checkCommand = process.platform === "win32" ? `where ${command}` : `which ${command}`;
3250
3737
  try {
3251
- execSync2(checkCommand, { stdio: "ignore" });
3738
+ execSync3(checkCommand, { stdio: "ignore" });
3252
3739
  return true;
3253
3740
  } catch {
3254
3741
  return false;
@@ -4185,7 +4672,7 @@ function checkEnvFile() {
4185
4672
  details: ["Env vars can be set in shell, CI/CD, or via --env-file"]
4186
4673
  };
4187
4674
  }
4188
- function checkAWSCredentials() {
4675
+ async function checkAWSCredentials() {
4189
4676
  const profile = process.env.AWS_PROFILE;
4190
4677
  const accessKey = process.env.AWS_ACCESS_KEY_ID;
4191
4678
  const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
@@ -4199,28 +4686,58 @@ function checkAWSCredentials() {
4199
4686
  if (region) {
4200
4687
  details.push(`AWS_REGION: ${region}`);
4201
4688
  }
4202
- 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
+ }
4203
4718
  return {
4204
4719
  name: "AWS Credentials",
4205
4720
  status: "ok",
4206
- message: profile ? `Profile: ${profile}` : "Using access key",
4721
+ message: profile ? `Profile: ${profile} (validated)` : "Using access key (validated)",
4207
4722
  details: details.length > 0 ? details : void 0
4208
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
+ };
4209
4735
  }
4210
- return {
4211
- name: "AWS Credentials",
4212
- status: "warning",
4213
- message: "No AWS credentials detected",
4214
- details: [
4215
- "Set AWS_PROFILE or AWS_ACCESS_KEY_ID for Bedrock judge",
4216
- "Claude Code connector also requires AWS credentials"
4217
- ]
4218
- };
4219
4736
  }
4220
4737
  async function checkClaudeCodeCLI() {
4221
- const { execSync: execSync2 } = await import("child_process");
4738
+ const { execSync: execSync3 } = await import("child_process");
4222
4739
  try {
4223
- execSync2("which claude", { stdio: "pipe" });
4740
+ execSync3("which claude", { stdio: "pipe" });
4224
4741
  return {
4225
4742
  name: "Claude Code CLI",
4226
4743
  status: "ok",
@@ -4305,6 +4822,45 @@ function checkOpenSearchObservability() {
4305
4822
  ]
4306
4823
  };
4307
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
+ }
4308
4864
  function displayResults2(results) {
4309
4865
  console.log(chalk6.bold("\n Configuration Check\n"));
4310
4866
  for (const result of results) {
@@ -4347,12 +4903,13 @@ function createDoctorCommand() {
4347
4903
  }
4348
4904
  results.push(checkConfigFile());
4349
4905
  results.push(checkEnvFile());
4350
- results.push(checkAWSCredentials());
4906
+ results.push(await checkAWSCredentials());
4351
4907
  results.push(await checkClaudeCodeCLI());
4352
4908
  results.push(checkAgents(config));
4353
4909
  results.push(checkConnectors());
4354
4910
  results.push(checkOpenSearchStorage());
4355
4911
  results.push(checkOpenSearchObservability());
4912
+ results.push(await checkTracesConnectivity());
4356
4913
  if (options.output === "json") {
4357
4914
  console.log(JSON.stringify(results, null, 2));
4358
4915
  } else {
@@ -5016,38 +5573,652 @@ function createRemoteCommand() {
5016
5573
  return remote;
5017
5574
  }
5018
5575
 
5019
- // cli/index.ts
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";
5020
5738
  var __filename3 = fileURLToPath3(import.meta.url);
5021
5739
  var __dirname3 = dirname3(__filename3);
5022
- 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");
5023
6194
  var version = "0.1.0";
5024
6195
  try {
5025
- const packageJson = JSON.parse(readFileSync3(packageJsonPath2, "utf-8"));
6196
+ const packageJson = JSON.parse(readFileSync5(packageJsonPath2, "utf-8"));
5026
6197
  version = packageJson.version;
5027
6198
  } catch {
5028
6199
  }
5029
6200
  function loadEnvFile(envPath) {
5030
6201
  const absolutePath = resolve4(process.cwd(), envPath);
5031
- if (!existsSync5(absolutePath)) {
5032
- console.error(chalk11.red(`
6202
+ if (!existsSync7(absolutePath)) {
6203
+ console.error(chalk14.red(`
5033
6204
  Error: Environment file not found: ${absolutePath}
5034
6205
  `));
5035
6206
  process.exit(1);
5036
6207
  }
5037
6208
  const result = loadDotenv({ path: absolutePath });
5038
6209
  if (result.error) {
5039
- console.error(chalk11.red(`
6210
+ console.error(chalk14.red(`
5040
6211
  Error loading environment file: ${result.error.message}
5041
6212
  `));
5042
6213
  process.exit(1);
5043
6214
  }
5044
- console.log(chalk11.gray(` Loaded environment from: ${envPath}`));
6215
+ console.log(chalk14.gray(` Loaded environment from: ${envPath}`));
5045
6216
  }
5046
6217
  var defaultEnvPath = resolve4(process.cwd(), ".env");
5047
- if (existsSync5(defaultEnvPath)) {
6218
+ if (existsSync7(defaultEnvPath)) {
5048
6219
  loadDotenv({ path: defaultEnvPath });
5049
6220
  }
5050
- var program = new Command11();
6221
+ var program = new Command14();
5051
6222
  program.name("agent-health").description("Agent Health Evaluation Framework - Evaluate and monitor AI agent performance").version(version).enablePositionalOptions().passThroughOptions().configureHelp({
5052
6223
  sortSubcommands: false,
5053
6224
  // Hide default command list — replaced by grouped custom help below
@@ -5060,95 +6231,105 @@ program.name("agent-health").description("Agent Health Evaluation Framework - Ev
5060
6231
  if (desc) {
5061
6232
  output.push(desc, "");
5062
6233
  }
5063
- output.push(`${chalk11.cyan.bold("Usage:")} ${helper.commandUsage(cmd)}`, "");
6234
+ output.push(`${chalk14.cyan.bold("Usage:")} ${helper.commandUsage(cmd)}`, "");
5064
6235
  const optionList = helper.visibleOptions(cmd).map((opt) => {
5065
6236
  const term = helper.optionTerm(opt);
5066
6237
  const desc2 = helper.optionDescription(opt);
5067
6238
  return ` ${term.padEnd(termWidth)} ${desc2}`;
5068
6239
  }).join("\n");
5069
6240
  if (optionList) {
5070
- output.push(`${chalk11.cyan.bold("Options:")}`, optionList, "");
6241
+ output.push(`${chalk14.cyan.bold("Options:")}`, optionList, "");
5071
6242
  }
5072
6243
  return output.join("\n");
5073
6244
  }
5074
6245
  });
5075
6246
  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)
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)
5080
6251
 
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
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
5085
6256
 
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
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
5091
6262
 
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
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
5096
6267
 
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)
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
5100
6272
 
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
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
5109
6286
  `);
5110
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");
5111
6288
  program.action(async (options) => {
5112
- console.log(chalk11.cyan.bold(`
6289
+ console.log(chalk14.cyan.bold(`
5113
6290
  Agent Health v${version} - AI Agent Evaluation Framework
5114
6291
  `));
5115
- console.log(chalk11.gray(` Working directory: ${process.cwd()}`));
5116
- console.log(chalk11.gray(` Package directory: ${__dirname3}`));
6292
+ console.log(chalk14.gray(` Working directory: ${process.cwd()}`));
6293
+ console.log(chalk14.gray(` Package directory: ${__dirname5}`));
5117
6294
  if (options.envFile) {
5118
6295
  loadEnvFile(options.envFile);
5119
- } else if (existsSync5(defaultEnvPath)) {
5120
- console.log(chalk11.gray(" Auto-loaded .env from current directory"));
6296
+ } else if (existsSync7(defaultEnvPath)) {
6297
+ console.log(chalk14.gray(" Auto-loaded .env from current directory"));
5121
6298
  }
5122
6299
  const port = parseInt(options.port, 10);
5123
6300
  const headless = options.headless || false;
5124
6301
  const spinner = ora5(headless ? "Starting headless API server..." : "Starting server...").start();
5125
6302
  try {
5126
- await startServer({ port, headless, apiKey: options.apiKey });
6303
+ const actualPort = await startServer({ port, headless, apiKey: options.apiKey });
5127
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
+ }
5128
6309
  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"));
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"));
5133
6314
  } 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)
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)
5138
6319
  `));
5139
- const url = `http://localhost:${port}`;
5140
- console.log(chalk11.green(` Server running at ${chalk11.bold(url)}
6320
+ const url = `http://localhost:${actualPort}`;
6321
+ console.log(chalk14.green(` Server running at ${chalk14.bold(url)}
5141
6322
  `));
5142
- console.log(chalk11.green(` Demo data loaded`));
6323
+ console.log(chalk14.green(` Demo data loaded`));
5143
6324
  if (options.browser !== false) {
5144
- console.log(chalk11.gray(" Opening browser..."));
6325
+ console.log(chalk14.gray(" Opening browser..."));
5145
6326
  await open(url);
5146
6327
  }
5147
6328
  }
5148
- console.log(chalk11.gray(" Press Ctrl+C to stop\n"));
6329
+ console.log(chalk14.gray(" Press Ctrl+C to stop\n"));
5149
6330
  } catch (error) {
5150
6331
  spinner.fail("Failed to start server");
5151
- console.error(chalk11.red(`
6332
+ console.error(chalk14.red(`
5152
6333
  Error: ${error instanceof Error ? error.message : error}
5153
6334
  `));
5154
6335
  process.exit(1);
@@ -5164,33 +6345,40 @@ program.addCommand(createInitCommand());
5164
6345
  program.addCommand(createMigrateCommand());
5165
6346
  program.addCommand(createCompareServicesCommand());
5166
6347
  program.addCommand(createRemoteCommand());
6348
+ program.addCommand(createConfigureCommand());
6349
+ program.addCommand(createKillCommand());
6350
+ program.addCommand(createSetupTelemetryCommand());
5167
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) => {
5168
- console.log(chalk11.cyan.bold(`
6352
+ console.log(chalk14.cyan.bold(`
5169
6353
  Agent Health v${version} - AI Agent Evaluation Framework
5170
6354
  `));
5171
6355
  const port = parseInt(options.port, 10);
5172
6356
  const headless = options.headless || false;
5173
6357
  const spinner = ora5(headless ? "Starting headless API server..." : "Starting server...").start();
5174
6358
  try {
5175
- await startServer({ port, headless, apiKey: options.apiKey });
6359
+ const actualPort = await startServer({ port, headless, apiKey: options.apiKey });
5176
6360
  spinner.succeed(headless ? "Headless API server started" : "Server started");
5177
- const url = `http://localhost:${port}`;
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}`;
5178
6366
  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"));
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"));
5182
6370
  } else {
5183
- console.log(chalk11.green(` Server running at ${chalk11.bold(url)}
6371
+ console.log(chalk14.green(` Server running at ${chalk14.bold(url)}
5184
6372
  `));
5185
6373
  if (options.browser !== false) {
5186
- console.log(chalk11.gray(" Opening browser..."));
6374
+ console.log(chalk14.gray(" Opening browser..."));
5187
6375
  await open(url);
5188
6376
  }
5189
6377
  }
5190
- console.log(chalk11.gray(" Press Ctrl+C to stop\n"));
6378
+ console.log(chalk14.gray(" Press Ctrl+C to stop\n"));
5191
6379
  } catch (error) {
5192
6380
  spinner.fail("Failed to start server");
5193
- console.error(chalk11.red(`
6381
+ console.error(chalk14.red(`
5194
6382
  Error: ${error instanceof Error ? error.message : error}
5195
6383
  `));
5196
6384
  process.exit(1);
@@ -5199,15 +6387,15 @@ program.command("serve").description("Start the Agent Health server (same as def
5199
6387
  program.on("command:*", (operands) => {
5200
6388
  const unknownCommand = operands[0];
5201
6389
  const availableCommands = program.commands.map((cmd) => cmd.name());
5202
- console.error(chalk11.red(`
6390
+ console.error(chalk14.red(`
5203
6391
  Error: Unknown command '${unknownCommand}'`));
5204
6392
  console.log("");
5205
- console.log(chalk11.cyan(" Available commands:"));
6393
+ console.log(chalk14.cyan(" Available commands:"));
5206
6394
  for (const cmd of availableCommands) {
5207
- console.log(chalk11.gray(` - ${cmd}`));
6395
+ console.log(chalk14.gray(` - ${cmd}`));
5208
6396
  }
5209
6397
  console.log("");
5210
- console.log(chalk11.gray(` Run ${chalk11.cyan("agent-health --help")} for usage information.
6398
+ console.log(chalk14.gray(` Run ${chalk14.cyan("agent-health --help")} for usage information.
5211
6399
  `));
5212
6400
  process.exitCode = 1;
5213
6401
  });