@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/README.md +43 -2
- package/cli/dist/index.js +1300 -112
- package/deployment/cloudformation/agent-health-observability.yaml +571 -0
- package/dist/assets/index-BOIP5L7h.js +246 -0
- package/dist/assets/index-CU9YKpAL.css +1 -0
- package/dist/index.html +2 -2
- package/lib/dist/config/index.js +50 -2
- package/lib/dist/index.js +66 -6
- package/package.json +25 -8
- package/server/dist/app.js +1276 -101
- package/server/dist/index.js +1429 -130
- package/dist/assets/index-EvPLSTAS.js +0 -267
- package/dist/assets/index-RXasQKUs.css +0 -1
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
|
|
5
|
-
import
|
|
6
|
-
import { fileURLToPath as
|
|
7
|
-
import { dirname as
|
|
8
|
-
import { readFileSync as
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
3735
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
3249
3736
|
const checkCommand = process.platform === "win32" ? `where ${command}` : `which ${command}`;
|
|
3250
3737
|
try {
|
|
3251
|
-
|
|
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
|
|
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:
|
|
4738
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
4222
4739
|
try {
|
|
4223
|
-
|
|
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/
|
|
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
|
|
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(
|
|
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 (!
|
|
5032
|
-
console.error(
|
|
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(
|
|
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(
|
|
6215
|
+
console.log(chalk14.gray(` Loaded environment from: ${envPath}`));
|
|
5045
6216
|
}
|
|
5046
6217
|
var defaultEnvPath = resolve4(process.cwd(), ".env");
|
|
5047
|
-
if (
|
|
6218
|
+
if (existsSync7(defaultEnvPath)) {
|
|
5048
6219
|
loadDotenv({ path: defaultEnvPath });
|
|
5049
6220
|
}
|
|
5050
|
-
var program = new
|
|
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(`${
|
|
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(`${
|
|
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
|
-
${
|
|
5077
|
-
${
|
|
5078
|
-
${
|
|
5079
|
-
${
|
|
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
|
-
${
|
|
5082
|
-
${
|
|
5083
|
-
${
|
|
5084
|
-
${
|
|
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
|
-
${
|
|
5087
|
-
${
|
|
5088
|
-
${
|
|
5089
|
-
${
|
|
5090
|
-
${
|
|
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
|
-
${
|
|
5093
|
-
${
|
|
5094
|
-
${
|
|
5095
|
-
${
|
|
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
|
-
${
|
|
5098
|
-
${
|
|
5099
|
-
${
|
|
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
|
-
${
|
|
5102
|
-
${
|
|
5103
|
-
${
|
|
5104
|
-
${
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
${
|
|
5108
|
-
${
|
|
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(
|
|
6289
|
+
console.log(chalk14.cyan.bold(`
|
|
5113
6290
|
Agent Health v${version} - AI Agent Evaluation Framework
|
|
5114
6291
|
`));
|
|
5115
|
-
console.log(
|
|
5116
|
-
console.log(
|
|
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 (
|
|
5120
|
-
console.log(
|
|
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(
|
|
5130
|
-
API server running on http://0.0.0.0:${
|
|
5131
|
-
if (options.apiKey) console.log(
|
|
5132
|
-
console.log(
|
|
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(
|
|
5135
|
-
console.log(
|
|
5136
|
-
console.log(
|
|
5137
|
-
console.log(
|
|
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:${
|
|
5140
|
-
console.log(
|
|
6320
|
+
const url = `http://localhost:${actualPort}`;
|
|
6321
|
+
console.log(chalk14.green(` Server running at ${chalk14.bold(url)}
|
|
5141
6322
|
`));
|
|
5142
|
-
console.log(
|
|
6323
|
+
console.log(chalk14.green(` Demo data loaded`));
|
|
5143
6324
|
if (options.browser !== false) {
|
|
5144
|
-
console.log(
|
|
6325
|
+
console.log(chalk14.gray(" Opening browser..."));
|
|
5145
6326
|
await open(url);
|
|
5146
6327
|
}
|
|
5147
6328
|
}
|
|
5148
|
-
console.log(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
5180
|
-
if (options.apiKey) console.log(
|
|
5181
|
-
console.log(
|
|
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(
|
|
6371
|
+
console.log(chalk14.green(` Server running at ${chalk14.bold(url)}
|
|
5184
6372
|
`));
|
|
5185
6373
|
if (options.browser !== false) {
|
|
5186
|
-
console.log(
|
|
6374
|
+
console.log(chalk14.gray(" Opening browser..."));
|
|
5187
6375
|
await open(url);
|
|
5188
6376
|
}
|
|
5189
6377
|
}
|
|
5190
|
-
console.log(
|
|
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(
|
|
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(
|
|
6390
|
+
console.error(chalk14.red(`
|
|
5203
6391
|
Error: Unknown command '${unknownCommand}'`));
|
|
5204
6392
|
console.log("");
|
|
5205
|
-
console.log(
|
|
6393
|
+
console.log(chalk14.cyan(" Available commands:"));
|
|
5206
6394
|
for (const cmd of availableCommands) {
|
|
5207
|
-
console.log(
|
|
6395
|
+
console.log(chalk14.gray(` - ${cmd}`));
|
|
5208
6396
|
}
|
|
5209
6397
|
console.log("");
|
|
5210
|
-
console.log(
|
|
6398
|
+
console.log(chalk14.gray(` Run ${chalk14.cyan("agent-health --help")} for usage information.
|
|
5211
6399
|
`));
|
|
5212
6400
|
process.exitCode = 1;
|
|
5213
6401
|
});
|