@tonyclaw/agent-inspector 3.0.7 → 3.0.8

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 (66) hide show
  1. package/.output/cli.js +245 -171
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-C0Kbsm-Q.js → CompareDrawer-Bth_Ywyt.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-C73EU9Jq.js +106 -0
  5. package/.output/public/assets/ReplayDialog-C2YYAuTe.js +1 -0
  6. package/.output/public/assets/{RequestAnatomy-fa0JXy5p.js → RequestAnatomy-8k9dAX2j.js} +1 -1
  7. package/.output/public/assets/ResponseView-DEL6JcXm.js +3 -0
  8. package/.output/public/assets/StreamingChunkSequence-DngmoR9j.js +1 -0
  9. package/.output/public/assets/{_sessionId-D5fYH2C0.js → _sessionId-4vk9RMZ5.js} +1 -1
  10. package/.output/public/assets/index-DBEB1rHe.css +1 -0
  11. package/.output/public/assets/{index-BEsqvywM.js → index-DINLvnm9.js} +1 -1
  12. package/.output/public/assets/index-LPIGiUMP.js +1 -0
  13. package/.output/public/assets/{json-viewer-B0QRbMSf.js → json-viewer-CH5dvS2Y.js} +1 -1
  14. package/.output/public/assets/{main-CuKZD7n0.js → main-GWkZn4xy.js} +2 -2
  15. package/.output/server/{_sessionId-DdBTK3qm.mjs → _sessionId-EgsjdkN7.mjs} +3 -3
  16. package/.output/server/_ssr/{CompareDrawer-m82cVZ73.mjs → CompareDrawer-CaM8FYuD.mjs} +2 -2
  17. package/.output/server/_ssr/{ProxyViewerContainer-7geuf78f.mjs → ProxyViewerContainer-EJIogcGd.mjs} +545 -309
  18. package/.output/server/_ssr/{ReplayDialog-B8RlWLLH.mjs → ReplayDialog-BPpJ2-q8.mjs} +58 -23
  19. package/.output/server/_ssr/{RequestAnatomy-BE7pmwpv.mjs → RequestAnatomy-DeQAWnZx.mjs} +2 -2
  20. package/.output/server/_ssr/{ResponseView-D-sDsdQr.mjs → ResponseView-Dshm8iQ7.mjs} +2 -2
  21. package/.output/server/_ssr/{StreamingChunkSequence-DU_hThJr.mjs → StreamingChunkSequence-32yIU2hn.mjs} +30 -20
  22. package/.output/server/_ssr/{index-D0sV8UbE.mjs → index-XRcx9blU.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-CaDh1zrw.mjs → json-viewer-C0aX5rqC.mjs} +2 -2
  25. package/.output/server/_ssr/{router-Zh2iZ_u0.mjs → router-BI-Mvxz9.mjs} +441 -172
  26. package/.output/server/_tanstack-start-manifest_v-jKScklL_.mjs +4 -0
  27. package/.output/server/index.mjs +71 -71
  28. package/package.json +1 -1
  29. package/src/assets/IDE/opencode.svg +1 -1
  30. package/src/assets/IDE/xiaomimimo.svg +1 -1
  31. package/src/cli/detect-tools.ts +2 -27
  32. package/src/cli/templates/codex-skill-onboard.ts +1 -1
  33. package/src/cli/templates/skill-onboard.ts +2 -3
  34. package/src/cli.ts +9 -86
  35. package/src/components/ProxyViewer.tsx +55 -8
  36. package/src/components/ProxyViewerContainer.tsx +30 -19
  37. package/src/components/clients/ClientLogo.tsx +22 -2
  38. package/src/components/ecosystem/AgentLabDialog.tsx +204 -3
  39. package/src/components/groups/GroupsDialog.tsx +28 -40
  40. package/src/components/providers/ImportWizardDialog.tsx +51 -101
  41. package/src/components/providers/ProvidersPanel.tsx +82 -68
  42. package/src/components/providers/SettingsDialog.tsx +11 -12
  43. package/src/components/proxy-viewer/AgentTraceSummary.tsx +15 -23
  44. package/src/components/proxy-viewer/ReplayDialog.tsx +65 -22
  45. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +33 -20
  46. package/src/lib/apiClient.ts +43 -10
  47. package/src/lib/ecosystemContract.ts +32 -0
  48. package/src/lib/export-logs.ts +37 -3
  49. package/src/proxy/ecosystemRegistry.ts +244 -0
  50. package/src/proxy/ecosystemTasks.ts +153 -0
  51. package/src/proxy/platformCommands.ts +212 -0
  52. package/src/proxy/socketTracker.ts +84 -143
  53. package/src/routes/api/ecosystem.packages.$packageId.help.ts +16 -0
  54. package/src/routes/api/ecosystem.packages.$packageId.install.ts +16 -0
  55. package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +19 -0
  56. package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +16 -0
  57. package/src/routes/api/ecosystem.packages.ts +1 -248
  58. package/src/routes/api/ecosystem.tasks.$taskId.ts +16 -0
  59. package/src/routes/api/ecosystem.tasks.ts +12 -0
  60. package/.output/public/assets/ProxyViewerContainer-p-rpYyiF.js +0 -106
  61. package/.output/public/assets/ReplayDialog-CM3dhP8v.js +0 -1
  62. package/.output/public/assets/ResponseView-Bq6sC7Ay.js +0 -3
  63. package/.output/public/assets/StreamingChunkSequence-CGOun3Df.js +0 -1
  64. package/.output/public/assets/index-C29Olfzt.js +0 -1
  65. package/.output/public/assets/index-DgANE0r3.css +0 -1
  66. package/.output/server/_tanstack-start-manifest_v-LCE-2AaF.mjs +0 -4
@@ -1,6 +1,6 @@
1
1
  import { Braces, Minimize2, RotateCcw } from "lucide-react";
2
2
  import type { JSX } from "react";
3
- import { useCallback, useEffect, useMemo, useState } from "react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { z } from "zod";
5
5
  import { useProviders } from "../../lib/useProviders";
6
6
  import { Button } from "../ui/button";
@@ -9,6 +9,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
9
9
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
10
10
  import { ResponseView } from "./ResponseView";
11
11
  import { CapturedLogSchema, type CapturedLog } from "../../contracts";
12
+ import { ApiTimeoutError, fetchJsonWithTimeout } from "../../lib/apiClient";
12
13
  import { dispatchLogFocusRequest } from "./logFocus";
13
14
  import {
14
15
  buildReplayComparisons,
@@ -53,6 +54,9 @@ const ReplayHistorySchema = z.object({
53
54
  total: z.number(),
54
55
  });
55
56
 
57
+ const REPLAY_TIMEOUT_MS = 60_000;
58
+ const REPLAY_HISTORY_TIMEOUT_MS = 10_000;
59
+
56
60
  function formatJsonForEdit(value: string): string {
57
61
  try {
58
62
  const parsed: unknown = JSON.parse(value);
@@ -175,17 +179,26 @@ export function ReplayDialog({
175
179
  const [replayHistory, setReplayHistory] = useState<CapturedLog[]>([]);
176
180
  const [loading, setLoading] = useState(false);
177
181
  const [error, setError] = useState<string | null>(null);
182
+ const replayAbortRef = useRef<AbortController | null>(null);
183
+ const replayRequestIdRef = useRef(0);
178
184
  const jsonEditState = useMemo(() => readJsonEditState(modifiedBody), [modifiedBody]);
179
185
 
186
+ const cancelReplay = useCallback(() => {
187
+ replayRequestIdRef.current += 1;
188
+ replayAbortRef.current?.abort();
189
+ replayAbortRef.current = null;
190
+ setLoading(false);
191
+ }, []);
192
+
180
193
  const refreshReplayHistory = useCallback(async (): Promise<void> => {
181
194
  if (!open) return;
182
195
  try {
183
- const response = await fetch(`/api/logs?replayOf=${String(log.id)}`);
184
- if (!response.ok) return;
185
- const data: unknown = await response.json();
186
- const parsed = ReplayHistorySchema.safeParse(data);
187
- if (!parsed.success) return;
188
- setReplayHistory(parsed.data.logs);
196
+ const data = await fetchJsonWithTimeout(
197
+ `/api/logs?replayOf=${String(log.id)}`,
198
+ ReplayHistorySchema,
199
+ REPLAY_HISTORY_TIMEOUT_MS,
200
+ );
201
+ setReplayHistory(data.logs);
189
202
  } catch {
190
203
  // Replay history is helpful context, not required for replay itself.
191
204
  }
@@ -195,44 +208,74 @@ export function ReplayDialog({
195
208
  void refreshReplayHistory();
196
209
  }, [refreshReplayHistory]);
197
210
 
211
+ useEffect(() => {
212
+ if (open) return undefined;
213
+ cancelReplay();
214
+ return undefined;
215
+ }, [cancelReplay, open]);
216
+
217
+ useEffect(() => {
218
+ return () => {
219
+ replayRequestIdRef.current += 1;
220
+ replayAbortRef.current?.abort();
221
+ replayAbortRef.current = null;
222
+ };
223
+ }, []);
224
+
198
225
  async function handleReplay() {
199
226
  if (!jsonEditState.valid) {
200
227
  setError("Request body must be valid JSON before replay.");
201
228
  return;
202
229
  }
203
230
 
231
+ replayAbortRef.current?.abort();
232
+ const requestId = replayRequestIdRef.current + 1;
233
+ replayRequestIdRef.current = requestId;
234
+ const controller = new AbortController();
235
+ replayAbortRef.current = controller;
236
+
204
237
  setLoading(true);
205
238
  setError(null);
206
239
  setReplayResult(null);
207
240
 
208
241
  try {
209
- const res = await fetch(`/api/logs/${log.id}/replay`, {
210
- method: "POST",
211
- headers: { "Content-Type": "application/json" },
212
- body: JSON.stringify({ modifiedBody }),
213
- });
214
-
215
- const json: unknown = await res.json();
216
- const parsed = ReplayResultSchema.safeParse(json);
217
- if (!parsed.success) {
218
- setError("Invalid response from server");
219
- setLoading(false);
220
- return;
221
- }
222
- const data = parsed.data;
242
+ const data = await fetchJsonWithTimeout(
243
+ `/api/logs/${log.id}/replay`,
244
+ ReplayResultSchema,
245
+ REPLAY_TIMEOUT_MS,
246
+ {
247
+ method: "POST",
248
+ headers: { "Content-Type": "application/json" },
249
+ body: JSON.stringify({ modifiedBody }),
250
+ signal: controller.signal,
251
+ },
252
+ );
253
+ if (replayRequestIdRef.current !== requestId) return;
223
254
  setReplayResult(data);
224
255
  await refreshReplayHistory();
225
256
  if (!data.success) {
226
257
  setError(data.error ?? "Replay failed");
227
258
  }
228
259
  } catch (err) {
260
+ if (replayRequestIdRef.current !== requestId) return;
261
+ if (
262
+ err instanceof ApiTimeoutError ||
263
+ (err instanceof DOMException && err.name === "AbortError")
264
+ ) {
265
+ setError("Replay timed out or was canceled.");
266
+ return;
267
+ }
229
268
  setError(err instanceof Error ? err.message : "Network error");
230
269
  } finally {
231
- setLoading(false);
270
+ if (replayRequestIdRef.current === requestId) {
271
+ replayAbortRef.current = null;
272
+ setLoading(false);
273
+ }
232
274
  }
233
275
  }
234
276
 
235
277
  function handleClose() {
278
+ cancelReplay();
236
279
  setReplayResult(null);
237
280
  setError(null);
238
281
  onOpenChange(false);
@@ -1,8 +1,10 @@
1
- import { memo, Suspense, useState, useEffect, useMemo, type JSX } from "react";
1
+ import { memo, Suspense, useState, useEffect, useMemo, useRef, type JSX } from "react";
2
2
  import { ChevronDown, ChevronRight, Loader2 } from "lucide-react";
3
+ import { z } from "zod";
3
4
  import { Badge } from "../ui/badge";
4
5
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
5
- import type { StreamingChunk } from "../../contracts";
6
+ import { StreamingChunkSchema, type StreamingChunk } from "../../contracts";
7
+ import { fetchJsonWithTimeout } from "../../lib/apiClient";
6
8
  import { LazyJsonViewer } from "./lazy";
7
9
 
8
10
  export type StreamingChunkSequenceProps = {
@@ -21,6 +23,13 @@ type ChunkGroup = {
21
23
  chunks: StreamingChunk[];
22
24
  };
23
25
 
26
+ const StreamingChunksResponseSchema = z.object({
27
+ chunks: z.array(StreamingChunkSchema),
28
+ truncated: z.boolean().optional(),
29
+ });
30
+
31
+ const CHUNK_FETCH_TIMEOUT_MS = 10_000;
32
+
24
33
  export const StreamingChunkSequence = memo(function StreamingChunkSequence({
25
34
  logId,
26
35
  truncated,
@@ -28,33 +37,37 @@ export const StreamingChunkSequence = memo(function StreamingChunkSequence({
28
37
  const [containerExpanded, setContainerExpanded] = useState(false);
29
38
  const [chunkState, setChunkState] = useState<ChunkState>({ status: "idle" });
30
39
  const [expandedIndices, setExpandedIndices] = useState<Set<number>>(new Set());
40
+ const loadedLogIdRef = useRef<number | null>(null);
31
41
 
32
42
  useEffect(() => {
33
- if (!containerExpanded || chunkState.status !== "idle") return;
43
+ if (!containerExpanded) return;
44
+ if (loadedLogIdRef.current === logId) return;
34
45
 
35
- let cancelled = false;
46
+ const controller = new AbortController();
36
47
  setChunkState({ status: "loading" });
37
48
 
38
- fetch(`/api/logs/${logId}/chunks`)
39
- .then((res) => {
40
- if (!res.ok) {
41
- return Promise.reject(new Error("Chunks not found"));
42
- }
43
- return res.json();
44
- })
45
- .then((data: { chunks: StreamingChunk[]; truncated?: boolean }) => {
46
- if (!cancelled) {
47
- setChunkState({ status: "success", chunks: data.chunks });
48
- }
49
+ void fetchJsonWithTimeout(
50
+ `/api/logs/${logId}/chunks`,
51
+ StreamingChunksResponseSchema,
52
+ CHUNK_FETCH_TIMEOUT_MS,
53
+ { signal: controller.signal },
54
+ () => "Chunks not found",
55
+ )
56
+ .then((data) => {
57
+ loadedLogIdRef.current = logId;
58
+ setChunkState({ status: "success", chunks: data.chunks });
49
59
  })
50
- .catch(() => {
51
- if (!cancelled) {
52
- setChunkState({ status: "error", message: "Chunk data unavailable" });
53
- }
60
+ .catch((error: unknown) => {
61
+ if (error instanceof DOMException && error.name === "AbortError") return;
62
+ loadedLogIdRef.current = null;
63
+ setChunkState({
64
+ status: "error",
65
+ message: error instanceof Error ? error.message : "Chunk data unavailable",
66
+ });
54
67
  });
55
68
 
56
69
  return () => {
57
- cancelled = true;
70
+ controller.abort();
58
71
  };
59
72
  }, [containerExpanded, logId]);
60
73
 
@@ -61,28 +61,61 @@ export class ApiTimeoutError extends Error {
61
61
  }
62
62
 
63
63
  /**
64
- * Fetch a JSON resource with a browser-side timeout. This prevents UI loading
65
- * states from waiting forever when a tab keeps a stale connection across a
66
- * local server force-restart.
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 fetchJsonWithTimeout<T>(
68
+ export async function fetchWithTimeout(
69
69
  input: RequestInfo | URL,
70
- schema: z.ZodType<T>,
71
70
  timeoutMs: number,
72
71
  init?: RequestInit,
73
- errorFallback?: (response: Response) => string,
74
- ): Promise<T> {
72
+ ): Promise<Response> {
75
73
  const controller = new AbortController();
76
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
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 fetchJson(input, schema, { ...init, signal: controller.signal }, errorFallback);
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>;
@@ -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
- async function fetchStreamingChunks(logId: number): Promise<string | null> {
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 fetch(`/api/logs/${logId}/chunks`);
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 chunks = await fetchStreamingChunks(log.id);
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
+ }