@tonyclaw/agent-inspector 3.0.7 → 3.0.9
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/.output/cli.js +245 -171
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-C0Kbsm-Q.js → CompareDrawer-CJKL4LMm.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-DBUy29O-.js +106 -0
- package/.output/public/assets/ReplayDialog-BYanl-a_.js +1 -0
- package/.output/public/assets/{RequestAnatomy-fa0JXy5p.js → RequestAnatomy-DvPz1jqE.js} +1 -1
- package/.output/public/assets/ResponseView-sZnxp9GN.js +3 -0
- package/.output/public/assets/StreamingChunkSequence-G-tiYiBd.js +1 -0
- package/.output/public/assets/{_sessionId-D5fYH2C0.js → _sessionId-_hDcsmXq.js} +1 -1
- package/.output/public/assets/agent-inspector-58K1_MsC.ico +0 -0
- package/.output/public/assets/index-DAPAWsAd.js +1 -0
- package/.output/public/assets/index-DspnLqUW.css +1 -0
- package/.output/public/assets/{index-BEsqvywM.js → index-SuOSQb2_.js} +1 -1
- package/.output/public/assets/{json-viewer-B0QRbMSf.js → json-viewer-yQdhgjaw.js} +1 -1
- package/.output/public/assets/{main-CuKZD7n0.js → main-CgjcDxjR.js} +2 -2
- package/.output/server/_libs/radix-ui__react-dialog.mjs +2 -2
- package/.output/server/{_sessionId-DdBTK3qm.mjs → _sessionId-BGISf2En.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-m82cVZ73.mjs → CompareDrawer-s-UF9gW3.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-7geuf78f.mjs → ProxyViewerContainer-C0Sbz9ED.mjs} +1033 -530
- package/.output/server/_ssr/{ReplayDialog-B8RlWLLH.mjs → ReplayDialog-DvS8gvJr.mjs} +58 -23
- package/.output/server/_ssr/{RequestAnatomy-BE7pmwpv.mjs → RequestAnatomy-BwhWglJL.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-D-sDsdQr.mjs → ResponseView-wBVreqQf.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-DU_hThJr.mjs → StreamingChunkSequence-Dr6eg-MJ.mjs} +30 -20
- package/.output/server/_ssr/{index-D0sV8UbE.mjs → index-B98Wia1X.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-CaDh1zrw.mjs → json-viewer-D7L8y0FH.mjs} +2 -2
- package/.output/server/_ssr/{router-Zh2iZ_u0.mjs → router-g9mo0nWT.mjs} +459 -196
- package/.output/server/_tanstack-start-manifest_v-CbGc-o50.mjs +4 -0
- package/.output/server/index.mjs +76 -69
- package/package.json +1 -1
- package/src/assets/IDE/opencode.svg +1 -1
- package/src/assets/IDE/xiaomimimo.svg +1 -1
- package/src/cli/detect-tools.ts +2 -27
- package/src/cli/templates/codex-skill-onboard.ts +1 -1
- package/src/cli/templates/skill-onboard.ts +2 -3
- package/src/cli.ts +9 -86
- package/src/components/ProxyViewer.tsx +249 -37
- package/src/components/ProxyViewerContainer.tsx +167 -22
- package/src/components/clients/ClientLogo.tsx +38 -3
- package/src/components/ecosystem/AgentLabDialog.tsx +204 -3
- package/src/components/groups/GroupsDialog.tsx +28 -40
- package/src/components/providers/ImportWizardDialog.tsx +51 -101
- package/src/components/providers/ProvidersPanel.tsx +82 -68
- package/src/components/providers/SettingsDialog.tsx +11 -12
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +15 -23
- package/src/components/proxy-viewer/ReplayDialog.tsx +65 -22
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +33 -20
- package/src/components/proxy-viewer/TurnGroup.tsx +0 -16
- package/src/components/proxy-viewer/log-formats/anthropic.ts +5 -5
- package/src/components/proxy-viewer/log-formats/openai.ts +7 -10
- package/src/components/proxy-viewer/requestTools.ts +5 -2
- package/src/lib/apiClient.ts +43 -10
- package/src/lib/ecosystemContract.ts +32 -0
- package/src/lib/export-logs.ts +37 -3
- package/src/proxy/ecosystemRegistry.ts +244 -0
- package/src/proxy/ecosystemTasks.ts +153 -0
- package/src/proxy/platformCommands.ts +212 -0
- package/src/proxy/socketTracker.ts +84 -143
- package/src/routes/api/ecosystem.packages.$packageId.help.ts +16 -0
- package/src/routes/api/ecosystem.packages.$packageId.install.ts +16 -0
- package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +19 -0
- package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +16 -0
- package/src/routes/api/ecosystem.packages.ts +1 -248
- package/src/routes/api/ecosystem.tasks.$taskId.ts +16 -0
- package/src/routes/api/ecosystem.tasks.ts +12 -0
- package/src/routes/api/providers.$providerId.test.log.ts +28 -24
- package/.output/public/assets/ProxyViewerContainer-p-rpYyiF.js +0 -106
- package/.output/public/assets/ReplayDialog-CM3dhP8v.js +0 -1
- package/.output/public/assets/ResponseView-Bq6sC7Ay.js +0 -3
- package/.output/public/assets/StreamingChunkSequence-CGOun3Df.js +0 -1
- package/.output/public/assets/index-C29Olfzt.js +0 -1
- package/.output/public/assets/index-DgANE0r3.css +0 -1
- package/.output/server/_tanstack-start-manifest_v-LCE-2AaF.mjs +0 -4
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { safeGetOwnProperty } from "../../../lib/objectUtils";
|
|
8
8
|
import type { AnatomySegment } from "../anatomy/types";
|
|
9
9
|
import { countCharacters, estimateTokens } from "../anatomy/tokenEstimate";
|
|
10
|
+
import { parseRequestTools } from "../requestTools";
|
|
10
11
|
import type { LogFormatAdapter } from "./types";
|
|
11
12
|
import { emptyRequestAnalysis, EMPTY_RESPONSE_ANALYSIS } from "./types";
|
|
12
13
|
|
|
@@ -146,28 +147,24 @@ export const openAILogFormatAdapter: LogFormatAdapter = {
|
|
|
146
147
|
analyzeRequest(rawBody) {
|
|
147
148
|
if (rawBody === null) return emptyRequestAnalysis(rawBody);
|
|
148
149
|
try {
|
|
149
|
-
const
|
|
150
|
+
const parsedBody: unknown = JSON.parse(rawBody);
|
|
151
|
+
const toolCount = parseRequestTools(parsedBody)?.tools.length ?? null;
|
|
152
|
+
const result = OpenAIRequestSchema.safeParse(parsedBody);
|
|
150
153
|
if (!result.success) {
|
|
151
|
-
const responsesResult = OpenAIResponsesRequestSchema.safeParse(
|
|
154
|
+
const responsesResult = OpenAIResponsesRequestSchema.safeParse(parsedBody);
|
|
152
155
|
if (!responsesResult.success) return emptyRequestAnalysis(rawBody);
|
|
153
156
|
return {
|
|
154
157
|
parsed: responsesResult.data,
|
|
155
158
|
comparisonValue: responsesResult.data,
|
|
156
159
|
messageCount: countResponsesInput(responsesResult.data.input),
|
|
157
|
-
toolCount
|
|
158
|
-
responsesResult.data.tools !== undefined && responsesResult.data.tools.length > 0
|
|
159
|
-
? responsesResult.data.tools.length
|
|
160
|
-
: null,
|
|
160
|
+
toolCount,
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
return {
|
|
164
164
|
parsed: result.data,
|
|
165
165
|
comparisonValue: result.data,
|
|
166
166
|
messageCount: result.data.messages.length,
|
|
167
|
-
toolCount
|
|
168
|
-
result.data.tools !== undefined && result.data.tools.length > 0
|
|
169
|
-
? result.data.tools.length
|
|
170
|
-
: null,
|
|
167
|
+
toolCount,
|
|
171
168
|
};
|
|
172
169
|
} catch {
|
|
173
170
|
return emptyRequestAnalysis(rawBody);
|
|
@@ -145,8 +145,11 @@ function parseAnthropicTool(tool: unknown): RequestToolDefinition | null {
|
|
|
145
145
|
function parseOpenAITool(tool: unknown): RequestToolDefinition | null {
|
|
146
146
|
const fn = safeGetOwnProperty(tool, "function");
|
|
147
147
|
const toolType = safeGetOwnProperty(tool, "type");
|
|
148
|
-
if (fn === undefined && toolType !== "
|
|
149
|
-
const name =
|
|
148
|
+
if (fn === undefined && typeof toolType !== "string") return null;
|
|
149
|
+
const name =
|
|
150
|
+
safeGetOwnProperty(fn, "name") ??
|
|
151
|
+
safeGetOwnProperty(tool, "name") ??
|
|
152
|
+
(typeof toolType === "string" ? toolType : undefined);
|
|
150
153
|
if (typeof name !== "string" || name.length === 0) return null;
|
|
151
154
|
const description =
|
|
152
155
|
safeGetOwnProperty(fn, "description") ?? safeGetOwnProperty(tool, "description");
|
package/src/lib/apiClient.ts
CHANGED
|
@@ -61,28 +61,61 @@ export class ApiTimeoutError extends Error {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Fetch
|
|
65
|
-
*
|
|
66
|
-
*
|
|
64
|
+
* Fetch any resource with a browser-side timeout while preserving a caller
|
|
65
|
+
* supplied abort signal. The timeout is reported as `ApiTimeoutError`; caller
|
|
66
|
+
* cancellations keep the native abort shape so UI code can distinguish them.
|
|
67
67
|
*/
|
|
68
|
-
export async function
|
|
68
|
+
export async function fetchWithTimeout(
|
|
69
69
|
input: RequestInfo | URL,
|
|
70
|
-
schema: z.ZodType<T>,
|
|
71
70
|
timeoutMs: number,
|
|
72
71
|
init?: RequestInit,
|
|
73
|
-
|
|
74
|
-
): Promise<T> {
|
|
72
|
+
): Promise<Response> {
|
|
75
73
|
const controller = new AbortController();
|
|
76
|
-
|
|
74
|
+
let timedOut = false;
|
|
75
|
+
const timeout = setTimeout(() => {
|
|
76
|
+
timedOut = true;
|
|
77
|
+
controller.abort();
|
|
78
|
+
}, timeoutMs);
|
|
79
|
+
|
|
80
|
+
const callerSignal = init?.signal ?? undefined;
|
|
81
|
+
const abortFromCaller = (): void => controller.abort();
|
|
82
|
+
if (callerSignal !== undefined) {
|
|
83
|
+
if (callerSignal.aborted) {
|
|
84
|
+
controller.abort();
|
|
85
|
+
} else {
|
|
86
|
+
callerSignal.addEventListener("abort", abortFromCaller, { once: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
77
89
|
|
|
78
90
|
try {
|
|
79
|
-
return await
|
|
91
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
80
92
|
} catch (error) {
|
|
81
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
93
|
+
if (timedOut && error instanceof DOMException && error.name === "AbortError") {
|
|
82
94
|
throw new ApiTimeoutError(timeoutMs);
|
|
83
95
|
}
|
|
84
96
|
throw error;
|
|
85
97
|
} finally {
|
|
86
98
|
clearTimeout(timeout);
|
|
99
|
+
callerSignal?.removeEventListener("abort", abortFromCaller);
|
|
87
100
|
}
|
|
88
101
|
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Fetch a JSON resource with a browser-side timeout. This prevents UI loading
|
|
105
|
+
* states from waiting forever when a tab keeps a stale connection across a
|
|
106
|
+
* local server force-restart.
|
|
107
|
+
*/
|
|
108
|
+
export async function fetchJsonWithTimeout<T>(
|
|
109
|
+
input: RequestInfo | URL,
|
|
110
|
+
schema: z.ZodType<T>,
|
|
111
|
+
timeoutMs: number,
|
|
112
|
+
init?: RequestInit,
|
|
113
|
+
errorFallback?: (response: Response) => string,
|
|
114
|
+
): Promise<T> {
|
|
115
|
+
const response = await fetchWithTimeout(input, timeoutMs, init);
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const fallback = errorFallback?.(response) ?? `Request failed with status ${response.status}`;
|
|
118
|
+
throw new Error(await readApiError(response, fallback));
|
|
119
|
+
}
|
|
120
|
+
return parseJsonResponse(response, schema);
|
|
121
|
+
}
|
|
@@ -41,7 +41,39 @@ export const EcosystemPackagesResponseSchema = z.object({
|
|
|
41
41
|
runnerPresets: z.array(EcosystemRunnerPresetSchema),
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
export const EcosystemTaskActionSchema = z.enum(["install", "upgrade", "help", "runner-presets"]);
|
|
45
|
+
|
|
46
|
+
export const EcosystemTaskStatusSchema = z.enum(["queued", "running", "succeeded", "failed"]);
|
|
47
|
+
|
|
48
|
+
export const EcosystemTaskSchema = z.object({
|
|
49
|
+
id: z.string(),
|
|
50
|
+
packageId: z.string(),
|
|
51
|
+
npmName: z.string(),
|
|
52
|
+
action: EcosystemTaskActionSchema,
|
|
53
|
+
status: EcosystemTaskStatusSchema,
|
|
54
|
+
command: z.string(),
|
|
55
|
+
output: z.string(),
|
|
56
|
+
createdAt: z.string(),
|
|
57
|
+
startedAt: z.string().nullable(),
|
|
58
|
+
finishedAt: z.string().nullable(),
|
|
59
|
+
exitCode: z.number().nullable(),
|
|
60
|
+
error: z.string().nullable(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const EcosystemTaskResponseSchema = z.object({
|
|
64
|
+
task: EcosystemTaskSchema,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const EcosystemTasksResponseSchema = z.object({
|
|
68
|
+
tasks: z.array(EcosystemTaskSchema),
|
|
69
|
+
});
|
|
70
|
+
|
|
44
71
|
export type EcosystemPackageState = z.infer<typeof EcosystemPackageStateSchema>;
|
|
45
72
|
export type EcosystemPackage = z.infer<typeof EcosystemPackageSchema>;
|
|
46
73
|
export type EcosystemRunnerPreset = z.infer<typeof EcosystemRunnerPresetSchema>;
|
|
47
74
|
export type EcosystemPackagesResponse = z.infer<typeof EcosystemPackagesResponseSchema>;
|
|
75
|
+
export type EcosystemTaskAction = z.infer<typeof EcosystemTaskActionSchema>;
|
|
76
|
+
export type EcosystemTaskStatus = z.infer<typeof EcosystemTaskStatusSchema>;
|
|
77
|
+
export type EcosystemTask = z.infer<typeof EcosystemTaskSchema>;
|
|
78
|
+
export type EcosystemTaskResponse = z.infer<typeof EcosystemTaskResponseSchema>;
|
|
79
|
+
export type EcosystemTasksResponse = z.infer<typeof EcosystemTasksResponseSchema>;
|
package/src/lib/export-logs.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import JSZip from "jszip";
|
|
2
2
|
import type { CapturedLog } from "../contracts";
|
|
3
|
+
import { fetchWithTimeout } from "./apiClient";
|
|
3
4
|
import packageJson from "../../package.json";
|
|
4
5
|
|
|
5
6
|
export type ExportMode = "redacted" | "raw";
|
|
@@ -8,6 +9,7 @@ export type ExportLogsResult =
|
|
|
8
9
|
| {
|
|
9
10
|
ok: false;
|
|
10
11
|
message: string;
|
|
12
|
+
canceled?: boolean;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
const REDACTED = "[REDACTED]";
|
|
@@ -23,6 +25,7 @@ const SECRET_TEXT_PATTERNS: readonly RegExp[] = [
|
|
|
23
25
|
const MAX_EXPORT_LOGS = 200;
|
|
24
26
|
const MAX_EXPORT_TEXT_CHARS = 25_000_000;
|
|
25
27
|
const MAX_STREAMING_CHUNK_EXPORTS = 50;
|
|
28
|
+
const STREAMING_CHUNK_EXPORT_TIMEOUT_MS = 10_000;
|
|
26
29
|
|
|
27
30
|
export type ExportManifest = {
|
|
28
31
|
version: string;
|
|
@@ -65,9 +68,27 @@ function exportLimitMessage(logs: readonly CapturedLog[]): string | null {
|
|
|
65
68
|
return null;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
type ExportLogsOptions = {
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function exportCanceled(signal: AbortSignal | undefined): ExportLogsResult | null {
|
|
76
|
+
if (signal?.aborted === true) {
|
|
77
|
+
return { ok: false, message: "Export canceled.", canceled: true };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function fetchStreamingChunks(
|
|
83
|
+
logId: number,
|
|
84
|
+
signal: AbortSignal | undefined,
|
|
85
|
+
): Promise<string | null> {
|
|
69
86
|
try {
|
|
70
|
-
const response = await
|
|
87
|
+
const response = await fetchWithTimeout(
|
|
88
|
+
`/api/logs/${logId}/chunks`,
|
|
89
|
+
STREAMING_CHUNK_EXPORT_TIMEOUT_MS,
|
|
90
|
+
{ signal },
|
|
91
|
+
);
|
|
71
92
|
if (!response.ok) return null;
|
|
72
93
|
const data: unknown = await response.json();
|
|
73
94
|
return JSON.stringify(data, null, 2);
|
|
@@ -145,7 +166,11 @@ export function buildExportManifest(
|
|
|
145
166
|
export async function exportLogsAsZip(
|
|
146
167
|
logs: CapturedLog[],
|
|
147
168
|
mode: ExportMode = "redacted",
|
|
169
|
+
options: ExportLogsOptions = {},
|
|
148
170
|
): Promise<ExportLogsResult> {
|
|
171
|
+
const initialCancel = exportCanceled(options.signal);
|
|
172
|
+
if (initialCancel !== null) return initialCancel;
|
|
173
|
+
|
|
149
174
|
const limitMessage = exportLimitMessage(logs);
|
|
150
175
|
if (limitMessage !== null) return { ok: false, message: limitMessage };
|
|
151
176
|
|
|
@@ -167,6 +192,9 @@ export async function exportLogsAsZip(
|
|
|
167
192
|
|
|
168
193
|
// Add regular request/response files
|
|
169
194
|
for (const log of logs) {
|
|
195
|
+
const canceled = exportCanceled(options.signal);
|
|
196
|
+
if (canceled !== null) return canceled;
|
|
197
|
+
|
|
170
198
|
if (log.rawRequestBody !== null) {
|
|
171
199
|
zip.file(`#${log.id}.Request.json`, exportText(log.rawRequestBody, mode));
|
|
172
200
|
}
|
|
@@ -177,7 +205,10 @@ export async function exportLogsAsZip(
|
|
|
177
205
|
|
|
178
206
|
// Fetch SSE chunks sequentially so exports do not spike browser memory.
|
|
179
207
|
for (const log of streamingLogs) {
|
|
180
|
-
const
|
|
208
|
+
const canceled = exportCanceled(options.signal);
|
|
209
|
+
if (canceled !== null) return canceled;
|
|
210
|
+
|
|
211
|
+
const chunks = await fetchStreamingChunks(log.id, options.signal);
|
|
181
212
|
if (chunks !== null) {
|
|
182
213
|
zip.file(`#${log.id}.SSE.Response.json`, exportText(chunks, mode));
|
|
183
214
|
}
|
|
@@ -185,6 +216,9 @@ export async function exportLogsAsZip(
|
|
|
185
216
|
|
|
186
217
|
// Generate and download ZIP
|
|
187
218
|
const blob = await zip.generateAsync({ type: "blob" });
|
|
219
|
+
const canceled = exportCanceled(options.signal);
|
|
220
|
+
if (canceled !== null) return canceled;
|
|
221
|
+
|
|
188
222
|
const url = URL.createObjectURL(blob);
|
|
189
223
|
|
|
190
224
|
const anchor = document.createElement("a");
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import packageJson from "../../package.json";
|
|
5
|
+
import type {
|
|
6
|
+
EcosystemPackage,
|
|
7
|
+
EcosystemPackageState,
|
|
8
|
+
EcosystemRunnerPreset,
|
|
9
|
+
} from "../lib/ecosystemContract";
|
|
10
|
+
import { npmSpawnCommand } from "./platformCommands";
|
|
11
|
+
|
|
12
|
+
export type PackageDefinition = {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
npmName: string;
|
|
16
|
+
capability: string;
|
|
17
|
+
description: string;
|
|
18
|
+
workflow: string;
|
|
19
|
+
primary: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type NpmResult = {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
stdout: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const ECOSYSTEM_PACKAGES: PackageDefinition[] = [
|
|
28
|
+
{
|
|
29
|
+
id: "inspector",
|
|
30
|
+
name: "Agent Inspector",
|
|
31
|
+
npmName: "@tonyclaw/agent-inspector",
|
|
32
|
+
capability: "Observe",
|
|
33
|
+
description: "Captures proxy traffic, sessions, turns, tools, providers, and raw bodies.",
|
|
34
|
+
workflow: "Keep every agent run visible and reloadable.",
|
|
35
|
+
primary: true,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "mcp",
|
|
39
|
+
name: "Inspector MCP",
|
|
40
|
+
npmName: "@tonyclaw/agent-inspector-mcp",
|
|
41
|
+
capability: "Connect",
|
|
42
|
+
description: "Exposes sessions, logs, providers, runs, and evidence to MCP clients.",
|
|
43
|
+
workflow: "Let Codex, OpenCode, MiMo Code, and other tools query Inspector as memory.",
|
|
44
|
+
primary: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "eval-harness",
|
|
48
|
+
name: "Eval Harness",
|
|
49
|
+
npmName: "@tonyclaw/eval-harness",
|
|
50
|
+
capability: "Evaluate",
|
|
51
|
+
description:
|
|
52
|
+
"Runs MCP-aware agent evaluations, runner presets, regressions, and model bakeoffs.",
|
|
53
|
+
workflow: "Promote Inspector evidence into repeatable evaluation batches.",
|
|
54
|
+
primary: false,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export const RUNNER_PRESETS: EcosystemRunnerPreset[] = [
|
|
59
|
+
{
|
|
60
|
+
id: "opencode",
|
|
61
|
+
name: "OpenCode",
|
|
62
|
+
validatedVersion: "1.17.11",
|
|
63
|
+
binary: "opencode",
|
|
64
|
+
modelFlag: "--model",
|
|
65
|
+
variantFlag: "--variant",
|
|
66
|
+
tools: ["opencode", "node"],
|
|
67
|
+
logDir: "opencode",
|
|
68
|
+
skillsDir: ".opencode/skills",
|
|
69
|
+
agentsDir: ".opencode/agents",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "mimo",
|
|
73
|
+
name: "MiMo Code",
|
|
74
|
+
validatedVersion: "0.1.4",
|
|
75
|
+
binary: "mimo",
|
|
76
|
+
modelFlag: "--model",
|
|
77
|
+
variantFlag: "--agent",
|
|
78
|
+
tools: ["mimo", "node"],
|
|
79
|
+
logDir: "mimocode",
|
|
80
|
+
skillsDir: ".mimocode/skills",
|
|
81
|
+
agentsDir: ".mimocode/agents",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "codex",
|
|
85
|
+
name: "Codex CLI",
|
|
86
|
+
validatedVersion: "0.142.5",
|
|
87
|
+
binary: "codex",
|
|
88
|
+
modelFlag: "--model",
|
|
89
|
+
variantFlag: null,
|
|
90
|
+
tools: ["codex"],
|
|
91
|
+
logDir: "codex",
|
|
92
|
+
skillsDir: ".codex/skills",
|
|
93
|
+
agentsDir: ".codex/agents",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "claude",
|
|
97
|
+
name: "Claude Code",
|
|
98
|
+
validatedVersion: "2.1.195",
|
|
99
|
+
binary: "claude",
|
|
100
|
+
modelFlag: "--model",
|
|
101
|
+
variantFlag: null,
|
|
102
|
+
tools: ["claude"],
|
|
103
|
+
logDir: "claude",
|
|
104
|
+
skillsDir: ".claude/skills",
|
|
105
|
+
agentsDir: ".claude/agents",
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const NPM_TIMEOUT_MS = 4000;
|
|
110
|
+
|
|
111
|
+
function runNpm(args: readonly string[]): Promise<NpmResult> {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
const command = npmSpawnCommand(args);
|
|
114
|
+
try {
|
|
115
|
+
execFile(
|
|
116
|
+
command.command,
|
|
117
|
+
command.args,
|
|
118
|
+
{ timeout: NPM_TIMEOUT_MS, windowsHide: true },
|
|
119
|
+
(error, stdout) => {
|
|
120
|
+
resolve({
|
|
121
|
+
ok: error === null,
|
|
122
|
+
stdout: String(stdout).trim(),
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
} catch {
|
|
127
|
+
resolve({ ok: false, stdout: "" });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function readGlobalNpmRoot(): Promise<string | null> {
|
|
133
|
+
const result = await runNpm(["root", "-g"]);
|
|
134
|
+
if (!result.ok || result.stdout.length === 0) return null;
|
|
135
|
+
return result.stdout;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readGlobalNpmRootFallback(): string | null {
|
|
139
|
+
switch (process.platform) {
|
|
140
|
+
case "win32": {
|
|
141
|
+
const appData = process.env["APPDATA"];
|
|
142
|
+
if (appData === undefined || appData.length === 0) return null;
|
|
143
|
+
return join(appData, "npm", "node_modules");
|
|
144
|
+
}
|
|
145
|
+
case "darwin":
|
|
146
|
+
case "linux":
|
|
147
|
+
case "aix":
|
|
148
|
+
case "freebsd":
|
|
149
|
+
case "openbsd":
|
|
150
|
+
case "sunos": {
|
|
151
|
+
const prefix = process.env["PREFIX"];
|
|
152
|
+
if (prefix !== undefined && prefix.length > 0) return join(prefix, "lib", "node_modules");
|
|
153
|
+
return "/usr/local/lib/node_modules";
|
|
154
|
+
}
|
|
155
|
+
case "android":
|
|
156
|
+
case "cygwin":
|
|
157
|
+
case "haiku":
|
|
158
|
+
case "netbsd":
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function packageJsonPath(globalRoot: string, npmName: string): string {
|
|
166
|
+
const parts = npmName.split("/");
|
|
167
|
+
return join(globalRoot, ...parts, "package.json");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readJsonStringField(raw: unknown, field: string): string | null {
|
|
171
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
172
|
+
const descriptor = Object.getOwnPropertyDescriptor(raw, field);
|
|
173
|
+
if (descriptor === undefined) return null;
|
|
174
|
+
return typeof descriptor.value === "string" ? descriptor.value : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function readInstalledVersion(
|
|
178
|
+
globalRoot: string | null,
|
|
179
|
+
npmName: string,
|
|
180
|
+
): Promise<string | null> {
|
|
181
|
+
if (npmName === packageJson.name) return packageJson.version;
|
|
182
|
+
if (globalRoot === null) return null;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const raw = await readFile(packageJsonPath(globalRoot, npmName), "utf8");
|
|
186
|
+
return readJsonStringField(JSON.parse(raw), "version");
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function readLatestVersion(npmName: string): Promise<string | null> {
|
|
193
|
+
const result = await runNpm(["view", npmName, "version", "--json"]);
|
|
194
|
+
if (!result.ok || result.stdout.length === 0) return null;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const parsed: unknown = JSON.parse(result.stdout);
|
|
198
|
+
return typeof parsed === "string" && parsed.length > 0 ? parsed : null;
|
|
199
|
+
} catch {
|
|
200
|
+
return result.stdout.length > 0 ? result.stdout : null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function packageState(
|
|
205
|
+
installedVersion: string | null,
|
|
206
|
+
latestVersion: string | null,
|
|
207
|
+
): EcosystemPackageState {
|
|
208
|
+
if (installedVersion !== null && latestVersion !== null && installedVersion !== latestVersion) {
|
|
209
|
+
return "update-available";
|
|
210
|
+
}
|
|
211
|
+
if (installedVersion !== null) return "installed";
|
|
212
|
+
if (latestVersion !== null) return "available";
|
|
213
|
+
return "planned";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function describePackage(
|
|
217
|
+
definition: PackageDefinition,
|
|
218
|
+
globalRoot: string | null,
|
|
219
|
+
): Promise<EcosystemPackage> {
|
|
220
|
+
const [installedVersion, latestVersion] = await Promise.all([
|
|
221
|
+
readInstalledVersion(globalRoot, definition.npmName),
|
|
222
|
+
readLatestVersion(definition.npmName),
|
|
223
|
+
]);
|
|
224
|
+
const state = packageState(installedVersion, latestVersion);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
...definition,
|
|
228
|
+
installCommand: `npm install -g ${definition.npmName}`,
|
|
229
|
+
installedVersion,
|
|
230
|
+
latestVersion,
|
|
231
|
+
state,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function listEcosystemPackages(): Promise<EcosystemPackage[]> {
|
|
236
|
+
const globalRoot = (await readGlobalNpmRoot()) ?? readGlobalNpmRootFallback();
|
|
237
|
+
return Promise.all(
|
|
238
|
+
ECOSYSTEM_PACKAGES.map((definition) => describePackage(definition, globalRoot)),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function findEcosystemPackage(packageId: string): PackageDefinition | null {
|
|
243
|
+
return ECOSYSTEM_PACKAGES.find((definition) => definition.id === packageId) ?? null;
|
|
244
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import type {
|
|
5
|
+
EcosystemTask,
|
|
6
|
+
EcosystemTaskAction,
|
|
7
|
+
EcosystemTaskStatus,
|
|
8
|
+
} from "../lib/ecosystemContract";
|
|
9
|
+
import { findEcosystemPackage } from "./ecosystemRegistry";
|
|
10
|
+
import { npmCommand, npmSpawnCommand } from "./platformCommands";
|
|
11
|
+
|
|
12
|
+
const MAX_TASKS = 50;
|
|
13
|
+
const OUTPUT_LIMIT = 40_000;
|
|
14
|
+
const INSTALL_TIMEOUT_MS = 120_000;
|
|
15
|
+
const HELP_TIMEOUT_MS = 30_000;
|
|
16
|
+
|
|
17
|
+
const tasks = new Map<string, EcosystemTask>();
|
|
18
|
+
|
|
19
|
+
function nowIso(): string {
|
|
20
|
+
return new Date().toISOString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function trimOutput(output: string): string {
|
|
24
|
+
if (output.length <= OUTPUT_LIMIT) return output;
|
|
25
|
+
return output.slice(output.length - OUTPUT_LIMIT);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function appendTaskOutput(task: EcosystemTask, chunk: string): void {
|
|
29
|
+
task.output = trimOutput(`${task.output}${chunk}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rememberTask(task: EcosystemTask): void {
|
|
33
|
+
tasks.set(task.id, task);
|
|
34
|
+
while (tasks.size > MAX_TASKS) {
|
|
35
|
+
const oldest = tasks.keys().next().value;
|
|
36
|
+
if (typeof oldest !== "string") return;
|
|
37
|
+
tasks.delete(oldest);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function packageActionArgs(action: EcosystemTaskAction, npmName: string): string[] {
|
|
42
|
+
switch (action) {
|
|
43
|
+
case "install":
|
|
44
|
+
case "upgrade":
|
|
45
|
+
return ["install", "-g", npmName];
|
|
46
|
+
case "help":
|
|
47
|
+
return ["exec", "--yes", npmName, "--", "--help"];
|
|
48
|
+
case "runner-presets":
|
|
49
|
+
return ["exec", "--yes", "@tonyclaw/eval-harness", "--", "runner-presets"];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function packageActionTimeout(action: EcosystemTaskAction): number {
|
|
54
|
+
switch (action) {
|
|
55
|
+
case "install":
|
|
56
|
+
case "upgrade":
|
|
57
|
+
return INSTALL_TIMEOUT_MS;
|
|
58
|
+
case "help":
|
|
59
|
+
case "runner-presets":
|
|
60
|
+
return HELP_TIMEOUT_MS;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function taskCommand(args: readonly string[]): string {
|
|
65
|
+
return `${npmCommand()} ${args.join(" ")}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function completeTask(
|
|
69
|
+
task: EcosystemTask,
|
|
70
|
+
status: Exclude<EcosystemTaskStatus, "queued" | "running">,
|
|
71
|
+
exitCode: number | null,
|
|
72
|
+
error: string | null,
|
|
73
|
+
): void {
|
|
74
|
+
task.status = status;
|
|
75
|
+
task.exitCode = exitCode;
|
|
76
|
+
task.error = error;
|
|
77
|
+
task.finishedAt = nowIso();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getEcosystemTask(taskId: string): EcosystemTask | null {
|
|
81
|
+
return tasks.get(taskId) ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function listEcosystemTasks(): EcosystemTask[] {
|
|
85
|
+
return [...tasks.values()].sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function startEcosystemTask(
|
|
89
|
+
packageId: string,
|
|
90
|
+
action: EcosystemTaskAction,
|
|
91
|
+
): EcosystemTask | null {
|
|
92
|
+
const definition =
|
|
93
|
+
action === "runner-presets"
|
|
94
|
+
? findEcosystemPackage("eval-harness")
|
|
95
|
+
: findEcosystemPackage(packageId);
|
|
96
|
+
if (definition === null) return null;
|
|
97
|
+
if (action === "runner-presets" && packageId !== "eval-harness") return null;
|
|
98
|
+
|
|
99
|
+
const args = packageActionArgs(action, definition.npmName);
|
|
100
|
+
const task: EcosystemTask = {
|
|
101
|
+
id: randomUUID(),
|
|
102
|
+
packageId: definition.id,
|
|
103
|
+
npmName: definition.npmName,
|
|
104
|
+
action,
|
|
105
|
+
status: "queued",
|
|
106
|
+
command: taskCommand(args),
|
|
107
|
+
output: "",
|
|
108
|
+
createdAt: nowIso(),
|
|
109
|
+
startedAt: null,
|
|
110
|
+
finishedAt: null,
|
|
111
|
+
exitCode: null,
|
|
112
|
+
error: null,
|
|
113
|
+
};
|
|
114
|
+
rememberTask(task);
|
|
115
|
+
|
|
116
|
+
const spawnCommand = npmSpawnCommand(args);
|
|
117
|
+
const child = spawn(spawnCommand.command, spawnCommand.args, {
|
|
118
|
+
windowsHide: true,
|
|
119
|
+
shell: false,
|
|
120
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
121
|
+
});
|
|
122
|
+
const timeout = setTimeout(() => {
|
|
123
|
+
child.kill();
|
|
124
|
+
appendTaskOutput(task, `\nTimed out after ${String(packageActionTimeout(action) / 1000)}s.\n`);
|
|
125
|
+
}, packageActionTimeout(action));
|
|
126
|
+
|
|
127
|
+
task.status = "running";
|
|
128
|
+
task.startedAt = nowIso();
|
|
129
|
+
appendTaskOutput(task, `$ ${task.command}\n`);
|
|
130
|
+
|
|
131
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
132
|
+
appendTaskOutput(task, chunk.toString("utf8"));
|
|
133
|
+
});
|
|
134
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
135
|
+
appendTaskOutput(task, chunk.toString("utf8"));
|
|
136
|
+
});
|
|
137
|
+
child.on("error", (error) => {
|
|
138
|
+
clearTimeout(timeout);
|
|
139
|
+
appendTaskOutput(task, `\n${error.message}\n`);
|
|
140
|
+
completeTask(task, "failed", null, error.message);
|
|
141
|
+
});
|
|
142
|
+
child.on("close", (code) => {
|
|
143
|
+
clearTimeout(timeout);
|
|
144
|
+
if (task.status === "failed") return;
|
|
145
|
+
if (code === 0) {
|
|
146
|
+
completeTask(task, "succeeded", code, null);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
completeTask(task, "failed", code, `Command exited with code ${String(code ?? "unknown")}`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return task;
|
|
153
|
+
}
|