@tonyclaw/agent-inspector 3.0.5 → 3.0.6

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.
Files changed (35) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-Co-35_Xw.js → CompareDrawer-DsvO6QyJ.js} +1 -1
  3. package/.output/public/assets/{ProxyViewerContainer-DPv1rBo7.js → ProxyViewerContainer-CfP-komD.js} +26 -26
  4. package/.output/public/assets/{ReplayDialog-DghbvKdy.js → ReplayDialog-CQlLX1Uo.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-CRXE5v8X.js → RequestAnatomy-m1q-uVTl.js} +1 -1
  6. package/.output/public/assets/{ResponseView-CzxoQRjp.js → ResponseView-DuLLeYSh.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-DnFyF_Hr.js → StreamingChunkSequence-CN4aRFBx.js} +1 -1
  8. package/.output/public/assets/{_sessionId-DHRhWzKe.js → _sessionId-CWP4pxOU.js} +1 -1
  9. package/.output/public/assets/index-0pt1umWP.css +1 -0
  10. package/.output/public/assets/index-DSUEOPbC.js +1 -0
  11. package/.output/public/assets/{index-Dl1oki9E.js → index-zfZUz67K.js} +1 -1
  12. package/.output/public/assets/{json-viewer-B84f7oiG.js → json-viewer-_4r1LBBE.js} +1 -1
  13. package/.output/public/assets/{main-DViDnJ9X.js → main-C_nEHNUM.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +292 -203
  15. package/.output/server/{_sessionId-C43vEAWe.mjs → _sessionId-CTqCAmUF.mjs} +3 -3
  16. package/.output/server/_ssr/{CompareDrawer-DOL6q0jD.mjs → CompareDrawer-D6b6TZ-O.mjs} +3 -3
  17. package/.output/server/_ssr/{ProxyViewerContainer-S9izw3ND.mjs → ProxyViewerContainer-Dp7Va9_T.mjs} +340 -11
  18. package/.output/server/_ssr/{ReplayDialog-tqthwJh0.mjs → ReplayDialog-D_KWQ1GY.mjs} +4 -4
  19. package/.output/server/_ssr/{RequestAnatomy-CgWDjFPR.mjs → RequestAnatomy-CPubXjMX.mjs} +2 -2
  20. package/.output/server/_ssr/{ResponseView-Dk8Op622.mjs → ResponseView-CsTiNoOE.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-AlS7PK8f.mjs → StreamingChunkSequence-Bgbzt23U.mjs} +2 -2
  22. package/.output/server/_ssr/{index-BGwvMN2M.mjs → index-P1gSpuM5.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-CKHkh5U3.mjs → json-viewer-10ciVGnc.mjs} +3 -3
  25. package/.output/server/_ssr/{router-BJMaHkr1.mjs → router-BJJ0VhsB.mjs} +357 -146
  26. package/.output/server/_tanstack-start-manifest_v-CltlYL3I.mjs +4 -0
  27. package/.output/server/index.mjs +71 -71
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +19 -1
  30. package/src/components/ecosystem/AgentLabDialog.tsx +379 -0
  31. package/src/lib/ecosystemContract.ts +47 -0
  32. package/src/routes/api/ecosystem.packages.ts +263 -0
  33. package/.output/public/assets/index-D8cruW0P.css +0 -1
  34. package/.output/public/assets/index-DZyTpd2w.js +0 -1
  35. package/.output/server/_tanstack-start-manifest_v-C_HvRzgP.mjs +0 -4
@@ -0,0 +1,379 @@
1
+ import { type JSX, useMemo, useState } from "react";
2
+ import {
3
+ Activity,
4
+ Beaker,
5
+ Check,
6
+ Copy,
7
+ FlaskConical,
8
+ Network,
9
+ PackageCheck,
10
+ RefreshCw,
11
+ Rocket,
12
+ Sparkles,
13
+ Terminal,
14
+ } from "lucide-react";
15
+ import useSWR from "swr";
16
+ import {
17
+ EcosystemPackagesResponseSchema,
18
+ type EcosystemPackage,
19
+ type EcosystemPackageState,
20
+ type EcosystemRunnerPreset,
21
+ } from "../../lib/ecosystemContract";
22
+ import { fetchJson } from "../../lib/apiClient";
23
+ import { copyTextToClipboard } from "../../lib/clipboard";
24
+ import { cn } from "../../lib/utils";
25
+ import { Badge } from "../ui/badge";
26
+ import { Button } from "../ui/button";
27
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
28
+ import { INSPECTOR_ICON_TRIGGER_CLASS } from "../ui/icon-trigger";
29
+
30
+ type AgentLabDialogProps = {
31
+ currentSessionId: string | null;
32
+ logCount: number;
33
+ };
34
+
35
+ type WorkflowCard = {
36
+ id: string;
37
+ title: string;
38
+ label: string;
39
+ description: string;
40
+ metric: string;
41
+ icon: typeof Activity;
42
+ };
43
+
44
+ const WORKFLOWS: WorkflowCard[] = [
45
+ {
46
+ id: "observe",
47
+ title: "Observe",
48
+ label: "Live",
49
+ description: "Capture IDE, process, session, turn, request, response, tools, and providers.",
50
+ metric: "Inspector core",
51
+ icon: Activity,
52
+ },
53
+ {
54
+ id: "replay",
55
+ title: "Replay",
56
+ label: "Ready",
57
+ description:
58
+ "Promote real logs into repeatable replay evidence without losing the raw context.",
59
+ metric: "Session asset",
60
+ icon: RefreshCw,
61
+ },
62
+ {
63
+ id: "evaluate",
64
+ title: "Evaluate",
65
+ label: "Next",
66
+ description: "Use eval-harness to turn sessions into regression cases and provider bakeoffs.",
67
+ metric: "Eval loop",
68
+ icon: Beaker,
69
+ },
70
+ {
71
+ id: "connect",
72
+ title: "Connect",
73
+ label: "MCP",
74
+ description:
75
+ "Expose Inspector state to coding agents as queryable context and durable evidence.",
76
+ metric: "/api/mcp",
77
+ icon: Network,
78
+ },
79
+ ];
80
+
81
+ async function fetchEcosystemPackages(url: string) {
82
+ return fetchJson(
83
+ url,
84
+ EcosystemPackagesResponseSchema,
85
+ undefined,
86
+ (response) => `Failed to load ecosystem packages: ${String(response.status)}`,
87
+ );
88
+ }
89
+
90
+ function stateLabel(state: EcosystemPackageState): string {
91
+ switch (state) {
92
+ case "installed":
93
+ return "Installed";
94
+ case "update-available":
95
+ return "Update";
96
+ case "available":
97
+ return "Available";
98
+ case "planned":
99
+ return "Planned";
100
+ case "unknown":
101
+ return "Unknown";
102
+ }
103
+ }
104
+
105
+ function stateClassName(state: EcosystemPackageState): string {
106
+ switch (state) {
107
+ case "installed":
108
+ return "border-emerald-400/20 bg-emerald-400/8 text-emerald-200";
109
+ case "update-available":
110
+ return "border-amber-400/20 bg-amber-400/8 text-amber-200";
111
+ case "available":
112
+ return "border-sky-400/20 bg-sky-400/8 text-sky-200";
113
+ case "planned":
114
+ return "border-white/10 bg-white/[0.04] text-white/55";
115
+ case "unknown":
116
+ return "border-white/10 bg-white/[0.04] text-white/45";
117
+ }
118
+ }
119
+
120
+ function versionLabel(pkg: EcosystemPackage): string {
121
+ if (pkg.installedVersion !== null && pkg.latestVersion !== null) {
122
+ if (pkg.installedVersion === pkg.latestVersion) return `v${pkg.installedVersion}`;
123
+ return `v${pkg.installedVersion} -> v${pkg.latestVersion}`;
124
+ }
125
+ if (pkg.installedVersion !== null) return `v${pkg.installedVersion}`;
126
+ if (pkg.latestVersion !== null) return `latest v${pkg.latestVersion}`;
127
+ return "not published";
128
+ }
129
+
130
+ function PackageStatusCard({ pkg }: { pkg: EcosystemPackage }): JSX.Element {
131
+ return (
132
+ <div className="rounded-lg bg-white/[0.025] p-3 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
133
+ <div className="flex items-start justify-between gap-3">
134
+ <div className="min-w-0">
135
+ <div className="flex min-w-0 items-center gap-2">
136
+ <PackageCheck className="size-4 shrink-0 text-white/55" />
137
+ <div className="truncate text-sm font-semibold">{pkg.name}</div>
138
+ </div>
139
+ <div className="mt-1 truncate font-mono text-[11px] text-white/35">{pkg.npmName}</div>
140
+ </div>
141
+ <Badge className={cn("shrink-0", stateClassName(pkg.state))} variant="outline">
142
+ {stateLabel(pkg.state)}
143
+ </Badge>
144
+ </div>
145
+ <div className="mt-3 text-xs leading-5 text-muted-foreground">{pkg.description}</div>
146
+ <div className="mt-3 flex flex-wrap items-center gap-2 text-[11px]">
147
+ <span className="rounded bg-white/[0.035] px-2 py-1 font-mono text-white/50">
148
+ {versionLabel(pkg)}
149
+ </span>
150
+ <span className="rounded bg-white/[0.035] px-2 py-1 text-white/45">{pkg.capability}</span>
151
+ <span className="rounded bg-white/[0.035] px-2 py-1 font-mono text-white/35">
152
+ {pkg.installCommand}
153
+ </span>
154
+ </div>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ function WorkflowTile({ workflow }: { workflow: WorkflowCard }): JSX.Element {
160
+ const Icon = workflow.icon;
161
+ return (
162
+ <div className="rounded-lg bg-white/[0.025] p-3 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
163
+ <div className="flex items-start justify-between gap-3">
164
+ <div className="flex min-w-0 items-center gap-2">
165
+ <Icon className="size-4 shrink-0 text-cyan-200/75" />
166
+ <div className="truncate text-sm font-semibold">{workflow.title}</div>
167
+ </div>
168
+ <span className="rounded bg-white/[0.04] px-2 py-0.5 font-mono text-[10px] text-white/45">
169
+ {workflow.label}
170
+ </span>
171
+ </div>
172
+ <div className="mt-2 min-h-10 text-xs leading-5 text-muted-foreground">
173
+ {workflow.description}
174
+ </div>
175
+ <div className="mt-3 font-mono text-[11px] text-white/35">{workflow.metric}</div>
176
+ </div>
177
+ );
178
+ }
179
+
180
+ function buildSessionCommand(sessionId: string | null): string {
181
+ if (sessionId === null) {
182
+ return 'npx @tonyclaw/eval-harness inspector-smoke --title "TonyClaw Lab smoke" --latest-log-limit 5';
183
+ }
184
+ return `npx @tonyclaw/eval-harness inspector-smoke --title "TonyClaw Lab smoke ${sessionId}" --latest-log-limit 5`;
185
+ }
186
+
187
+ function RunnerPresetCard({ preset }: { preset: EcosystemRunnerPreset }): JSX.Element {
188
+ return (
189
+ <div className="rounded-lg bg-white/[0.025] p-3 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
190
+ <div className="flex items-start justify-between gap-3">
191
+ <div className="min-w-0">
192
+ <div className="flex min-w-0 items-center gap-2">
193
+ <Rocket className="size-4 shrink-0 text-cyan-200/75" />
194
+ <div className="truncate text-sm font-semibold">{preset.name}</div>
195
+ </div>
196
+ <div className="mt-1 font-mono text-[11px] text-white/35">
197
+ {preset.binary} {preset.modelFlag}
198
+ {preset.variantFlag !== null ? ` ${preset.variantFlag}` : ""}
199
+ </div>
200
+ </div>
201
+ <Badge variant="outline" className="border-white/10 bg-white/[0.04] text-white/55">
202
+ v{preset.validatedVersion}
203
+ </Badge>
204
+ </div>
205
+ <div className="mt-3 grid grid-cols-2 gap-2 text-[11px]">
206
+ <div className="rounded bg-black/15 px-2 py-1 text-white/45">logs/{preset.logDir}</div>
207
+ <div className="rounded bg-black/15 px-2 py-1 text-white/45">{preset.tools.join(", ")}</div>
208
+ <div className="rounded bg-black/15 px-2 py-1 font-mono text-white/35">
209
+ {preset.skillsDir}
210
+ </div>
211
+ <div className="rounded bg-black/15 px-2 py-1 font-mono text-white/35">
212
+ {preset.agentsDir}
213
+ </div>
214
+ </div>
215
+ </div>
216
+ );
217
+ }
218
+
219
+ export function AgentLabDialog({ currentSessionId, logCount }: AgentLabDialogProps): JSX.Element {
220
+ const [open, setOpen] = useState(false);
221
+ const [copied, setCopied] = useState(false);
222
+ const [copiedPresets, setCopiedPresets] = useState(false);
223
+ const response = useSWR("/api/ecosystem/packages", fetchEcosystemPackages, {
224
+ revalidateOnFocus: false,
225
+ revalidateIfStale: false,
226
+ });
227
+ const packages = response.data?.packages ?? [];
228
+ const runnerPresets = response.data?.runnerPresets ?? [];
229
+ const installedCount = useMemo(
230
+ () =>
231
+ packages.filter((pkg) => pkg.state === "installed" || pkg.state === "update-available")
232
+ .length,
233
+ [packages],
234
+ );
235
+ const sessionCommand = buildSessionCommand(currentSessionId);
236
+ const presetsCommand = "npx @tonyclaw/eval-harness runner-presets";
237
+
238
+ const copySessionCommand = (): void => {
239
+ void copyTextToClipboard(sessionCommand).then(() => {
240
+ setCopied(true);
241
+ window.setTimeout(() => setCopied(false), 1200);
242
+ });
243
+ };
244
+ const copyPresetsCommand = (): void => {
245
+ void copyTextToClipboard(presetsCommand).then(() => {
246
+ setCopiedPresets(true);
247
+ window.setTimeout(() => setCopiedPresets(false), 1200);
248
+ });
249
+ };
250
+
251
+ return (
252
+ <Dialog open={open} onOpenChange={setOpen}>
253
+ <DialogTrigger asChild>
254
+ <Button
255
+ variant="ghost"
256
+ size="icon"
257
+ className={INSPECTOR_ICON_TRIGGER_CLASS}
258
+ aria-label="TonyClaw Lab"
259
+ title="TonyClaw Lab"
260
+ >
261
+ <FlaskConical className="size-4" />
262
+ <span className="sr-only">TonyClaw Lab</span>
263
+ </Button>
264
+ </DialogTrigger>
265
+ <DialogContent className="flex max-h-[84vh] max-w-5xl flex-col overflow-hidden">
266
+ <DialogHeader>
267
+ <DialogTitle className="flex items-center gap-2">
268
+ <Sparkles className="size-4 text-cyan-200/80" />
269
+ TonyClaw Lab
270
+ </DialogTitle>
271
+ </DialogHeader>
272
+
273
+ <div className="overflow-y-auto pr-2">
274
+ <div className="grid gap-3 lg:grid-cols-[1.1fr_0.9fr]">
275
+ <section className="rounded-lg bg-white/[0.025] p-4 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
276
+ <div className="flex flex-wrap items-start justify-between gap-3">
277
+ <div>
278
+ <div className="text-sm font-semibold">Session asset pipeline</div>
279
+ <div className="mt-1 max-w-2xl text-xs leading-5 text-muted-foreground">
280
+ Treat captured traffic as durable assets: observe it, replay it, evaluate it,
281
+ and expose it back to agents through MCP.
282
+ </div>
283
+ </div>
284
+ <Badge variant="outline" className="border-cyan-300/20 bg-cyan-300/8 text-cyan-100">
285
+ {String(installedCount)}/{String(packages.length)} packages
286
+ </Badge>
287
+ </div>
288
+
289
+ <div className="mt-4 grid gap-3 sm:grid-cols-2">
290
+ {WORKFLOWS.map((workflow) => (
291
+ <WorkflowTile key={workflow.id} workflow={workflow} />
292
+ ))}
293
+ </div>
294
+ </section>
295
+
296
+ <section className="rounded-lg bg-white/[0.025] p-4 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
297
+ <div className="flex items-start justify-between gap-3">
298
+ <div>
299
+ <div className="text-sm font-semibold">Current session</div>
300
+ <div className="mt-1 text-xs text-muted-foreground">
301
+ {currentSessionId === null ? "MCP smoke command" : currentSessionId}
302
+ </div>
303
+ </div>
304
+ <Badge variant="outline" className="border-white/10 bg-white/[0.04] text-white/55">
305
+ {String(logCount)} logs
306
+ </Badge>
307
+ </div>
308
+
309
+ <div className="mt-4 rounded-md bg-black/20 p-3 font-mono text-[11px] leading-5 text-white/55 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.045)]">
310
+ {sessionCommand}
311
+ </div>
312
+
313
+ <div className="mt-3 flex flex-wrap items-center gap-2">
314
+ <Button size="sm" variant="secondary" onClick={copySessionCommand}>
315
+ {copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
316
+ {copied ? "Copied" : "Copy"}
317
+ </Button>
318
+ <Button size="sm" variant="outline" onClick={copyPresetsCommand}>
319
+ <Beaker className="size-3.5" />
320
+ {copiedPresets ? "Presets copied" : "Runner Presets"}
321
+ </Button>
322
+ </div>
323
+ </section>
324
+ </div>
325
+
326
+ <section className="mt-3">
327
+ <div className="mb-2 flex items-center justify-between gap-3">
328
+ <div className="text-sm font-semibold">Runner presets</div>
329
+ <div className="hidden items-center gap-1 font-mono text-[11px] text-muted-foreground sm:flex">
330
+ <Terminal className="size-3.5" />
331
+ {presetsCommand}
332
+ </div>
333
+ </div>
334
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
335
+ {runnerPresets.map((preset) => (
336
+ <RunnerPresetCard key={preset.id} preset={preset} />
337
+ ))}
338
+ {response.isLoading &&
339
+ runnerPresets.length === 0 &&
340
+ [0, 1, 2, 3].map((index) => (
341
+ <div
342
+ key={index}
343
+ className="h-32 animate-pulse rounded-lg bg-white/[0.025] shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]"
344
+ />
345
+ ))}
346
+ </div>
347
+ </section>
348
+
349
+ <section className="mt-3">
350
+ <div className="mb-2 flex items-center justify-between gap-3">
351
+ <div className="text-sm font-semibold">TonyClaw packages</div>
352
+ {response.isLoading && (
353
+ <div className="font-mono text-[11px] text-muted-foreground">checking npm...</div>
354
+ )}
355
+ </div>
356
+ {response.error !== undefined && (
357
+ <div className="rounded-lg bg-red-500/8 p-3 text-xs text-red-100 shadow-[inset_0_0_0_1px_rgba(248,113,113,0.16)]">
358
+ Package status is temporarily unavailable.
359
+ </div>
360
+ )}
361
+ <div className="grid gap-3 md:grid-cols-3">
362
+ {packages.map((pkg) => (
363
+ <PackageStatusCard key={pkg.id} pkg={pkg} />
364
+ ))}
365
+ {response.isLoading &&
366
+ packages.length === 0 &&
367
+ [0, 1, 2].map((index) => (
368
+ <div
369
+ key={index}
370
+ className="h-36 animate-pulse rounded-lg bg-white/[0.025] shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]"
371
+ />
372
+ ))}
373
+ </div>
374
+ </section>
375
+ </div>
376
+ </DialogContent>
377
+ </Dialog>
378
+ );
379
+ }
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+
3
+ export const EcosystemPackageStateSchema = z.enum([
4
+ "installed",
5
+ "update-available",
6
+ "available",
7
+ "planned",
8
+ "unknown",
9
+ ]);
10
+
11
+ export const EcosystemPackageSchema = z.object({
12
+ id: z.string(),
13
+ name: z.string(),
14
+ npmName: z.string(),
15
+ capability: z.string(),
16
+ description: z.string(),
17
+ workflow: z.string(),
18
+ installCommand: z.string(),
19
+ state: EcosystemPackageStateSchema,
20
+ installedVersion: z.string().nullable(),
21
+ latestVersion: z.string().nullable(),
22
+ primary: z.boolean(),
23
+ });
24
+
25
+ export const EcosystemRunnerPresetSchema = z.object({
26
+ id: z.string(),
27
+ name: z.string(),
28
+ validatedVersion: z.string(),
29
+ binary: z.string(),
30
+ modelFlag: z.string(),
31
+ variantFlag: z.string().nullable(),
32
+ tools: z.array(z.string()),
33
+ logDir: z.string(),
34
+ skillsDir: z.string(),
35
+ agentsDir: z.string(),
36
+ });
37
+
38
+ export const EcosystemPackagesResponseSchema = z.object({
39
+ checkedAt: z.string(),
40
+ packages: z.array(EcosystemPackageSchema),
41
+ runnerPresets: z.array(EcosystemRunnerPresetSchema),
42
+ });
43
+
44
+ export type EcosystemPackageState = z.infer<typeof EcosystemPackageStateSchema>;
45
+ export type EcosystemPackage = z.infer<typeof EcosystemPackageSchema>;
46
+ export type EcosystemRunnerPreset = z.infer<typeof EcosystemRunnerPresetSchema>;
47
+ export type EcosystemPackagesResponse = z.infer<typeof EcosystemPackagesResponseSchema>;
@@ -0,0 +1,263 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { createFileRoute } from "@tanstack/react-router";
5
+ import packageJson from "../../../package.json";
6
+ import type {
7
+ EcosystemPackage,
8
+ EcosystemPackageState,
9
+ EcosystemRunnerPreset,
10
+ } from "../../lib/ecosystemContract";
11
+
12
+ 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
+ type PackageVersionResult = {
28
+ installedVersion: string | null;
29
+ latestVersion: string | null;
30
+ state: EcosystemPackageState;
31
+ };
32
+
33
+ const ECOSYSTEM_PACKAGES: PackageDefinition[] = [
34
+ {
35
+ id: "inspector",
36
+ name: "Agent Inspector",
37
+ npmName: "@tonyclaw/agent-inspector",
38
+ capability: "Observe",
39
+ description: "Captures proxy traffic, sessions, turns, tools, providers, and raw bodies.",
40
+ workflow: "Keep every agent run visible and reloadable.",
41
+ primary: true,
42
+ },
43
+ {
44
+ id: "mcp",
45
+ name: "Inspector MCP",
46
+ npmName: "@tonyclaw/agent-inspector-mcp",
47
+ capability: "Connect",
48
+ description: "Exposes sessions, logs, providers, runs, and evidence to MCP clients.",
49
+ workflow: "Let Codex, OpenCode, MiMo Code, and other tools query Inspector as memory.",
50
+ primary: true,
51
+ },
52
+ {
53
+ id: "eval-harness",
54
+ name: "Eval Harness",
55
+ npmName: "@tonyclaw/eval-harness",
56
+ capability: "Evaluate",
57
+ description:
58
+ "Runs MCP-aware agent evaluations, runner presets, regressions, and model bakeoffs.",
59
+ workflow: "Promote Inspector evidence into repeatable evaluation batches.",
60
+ primary: false,
61
+ },
62
+ ];
63
+
64
+ const RUNNER_PRESETS: EcosystemRunnerPreset[] = [
65
+ {
66
+ id: "opencode",
67
+ name: "OpenCode",
68
+ validatedVersion: "1.17.11",
69
+ binary: "opencode",
70
+ modelFlag: "--model",
71
+ variantFlag: "--variant",
72
+ tools: ["opencode", "node"],
73
+ logDir: "opencode",
74
+ skillsDir: ".opencode/skills",
75
+ agentsDir: ".opencode/agents",
76
+ },
77
+ {
78
+ id: "mimo",
79
+ name: "MiMo Code",
80
+ validatedVersion: "0.1.4",
81
+ binary: "mimo",
82
+ modelFlag: "--model",
83
+ variantFlag: "--agent",
84
+ tools: ["mimo", "node"],
85
+ logDir: "mimocode",
86
+ skillsDir: ".mimocode/skills",
87
+ agentsDir: ".mimocode/agents",
88
+ },
89
+ {
90
+ id: "codex",
91
+ name: "Codex CLI",
92
+ validatedVersion: "0.142.5",
93
+ binary: "codex",
94
+ modelFlag: "--model",
95
+ variantFlag: null,
96
+ tools: ["codex"],
97
+ logDir: "codex",
98
+ skillsDir: ".codex/skills",
99
+ agentsDir: ".codex/agents",
100
+ },
101
+ {
102
+ id: "claude",
103
+ name: "Claude Code",
104
+ validatedVersion: "2.1.195",
105
+ binary: "claude",
106
+ modelFlag: "--model",
107
+ variantFlag: null,
108
+ tools: ["claude"],
109
+ logDir: "claude",
110
+ skillsDir: ".claude/skills",
111
+ agentsDir: ".claude/agents",
112
+ },
113
+ ];
114
+
115
+ const NPM_TIMEOUT_MS = 4000;
116
+
117
+ function npmCommand(): string {
118
+ return process.platform === "win32" ? "npm.cmd" : "npm";
119
+ }
120
+
121
+ function runNpm(args: readonly string[]): Promise<NpmResult> {
122
+ return new Promise((resolve) => {
123
+ try {
124
+ execFile(
125
+ npmCommand(),
126
+ [...args],
127
+ { timeout: NPM_TIMEOUT_MS, windowsHide: true },
128
+ (error, stdout) => {
129
+ resolve({
130
+ ok: error === null,
131
+ stdout: String(stdout).trim(),
132
+ });
133
+ },
134
+ );
135
+ } catch {
136
+ resolve({ ok: false, stdout: "" });
137
+ }
138
+ });
139
+ }
140
+
141
+ async function readGlobalNpmRoot(): Promise<string | null> {
142
+ const result = await runNpm(["root", "-g"]);
143
+ if (!result.ok || result.stdout.length === 0) return null;
144
+ return result.stdout;
145
+ }
146
+
147
+ function readGlobalNpmRootFallback(): string | null {
148
+ switch (process.platform) {
149
+ case "win32": {
150
+ const appData = process.env["APPDATA"];
151
+ if (appData === undefined || appData.length === 0) return null;
152
+ return join(appData, "npm", "node_modules");
153
+ }
154
+ case "darwin":
155
+ case "linux":
156
+ case "aix":
157
+ case "freebsd":
158
+ case "openbsd":
159
+ case "sunos": {
160
+ const prefix = process.env["PREFIX"];
161
+ if (prefix !== undefined && prefix.length > 0) return join(prefix, "lib", "node_modules");
162
+ return "/usr/local/lib/node_modules";
163
+ }
164
+ case "android":
165
+ case "cygwin":
166
+ case "haiku":
167
+ case "netbsd":
168
+ return null;
169
+ }
170
+
171
+ return null;
172
+ }
173
+
174
+ function packageJsonPath(globalRoot: string, npmName: string): string {
175
+ const parts = npmName.split("/");
176
+ return join(globalRoot, ...parts, "package.json");
177
+ }
178
+
179
+ function readJsonStringField(raw: unknown, field: string): string | null {
180
+ if (typeof raw !== "object" || raw === null) return null;
181
+ const descriptor = Object.getOwnPropertyDescriptor(raw, field);
182
+ if (descriptor === undefined) return null;
183
+ return typeof descriptor.value === "string" ? descriptor.value : null;
184
+ }
185
+
186
+ async function readInstalledVersion(
187
+ globalRoot: string | null,
188
+ npmName: string,
189
+ ): Promise<string | null> {
190
+ if (npmName === packageJson.name) return packageJson.version;
191
+ if (globalRoot === null) return null;
192
+
193
+ try {
194
+ const raw = await readFile(packageJsonPath(globalRoot, npmName), "utf8");
195
+ return readJsonStringField(JSON.parse(raw), "version");
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+
201
+ async function readLatestVersion(npmName: string): Promise<string | null> {
202
+ const result = await runNpm(["view", npmName, "version", "--json"]);
203
+ if (!result.ok || result.stdout.length === 0) return null;
204
+
205
+ try {
206
+ const parsed: unknown = JSON.parse(result.stdout);
207
+ return typeof parsed === "string" && parsed.length > 0 ? parsed : null;
208
+ } catch {
209
+ return result.stdout.length > 0 ? result.stdout : null;
210
+ }
211
+ }
212
+
213
+ function packageState(
214
+ installedVersion: string | null,
215
+ latestVersion: string | null,
216
+ ): EcosystemPackageState {
217
+ if (installedVersion !== null && latestVersion !== null && installedVersion !== latestVersion) {
218
+ return "update-available";
219
+ }
220
+ if (installedVersion !== null) return "installed";
221
+ if (latestVersion !== null) return "available";
222
+ return "planned";
223
+ }
224
+
225
+ async function describePackage(
226
+ definition: PackageDefinition,
227
+ globalRoot: string | null,
228
+ ): Promise<EcosystemPackage> {
229
+ const [installedVersion, latestVersion] = await Promise.all([
230
+ readInstalledVersion(globalRoot, definition.npmName),
231
+ readLatestVersion(definition.npmName),
232
+ ]);
233
+ const state = packageState(installedVersion, latestVersion);
234
+
235
+ return {
236
+ ...definition,
237
+ installCommand: `npm install -g ${definition.npmName}`,
238
+ installedVersion,
239
+ latestVersion,
240
+ state,
241
+ };
242
+ }
243
+
244
+ async function listEcosystemPackages(): Promise<EcosystemPackage[]> {
245
+ const globalRoot = (await readGlobalNpmRoot()) ?? readGlobalNpmRootFallback();
246
+ return Promise.all(
247
+ ECOSYSTEM_PACKAGES.map((definition) => describePackage(definition, globalRoot)),
248
+ );
249
+ }
250
+
251
+ export const Route = createFileRoute("/api/ecosystem/packages")({
252
+ server: {
253
+ handlers: {
254
+ GET: async () => {
255
+ return Response.json({
256
+ checkedAt: new Date().toISOString(),
257
+ packages: await listEcosystemPackages(),
258
+ runnerPresets: RUNNER_PRESETS,
259
+ });
260
+ },
261
+ },
262
+ },
263
+ });