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