@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.
- package/.output/cli.js +245 -171
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-C0Kbsm-Q.js → CompareDrawer-Bth_Ywyt.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-C73EU9Jq.js +106 -0
- package/.output/public/assets/ReplayDialog-C2YYAuTe.js +1 -0
- package/.output/public/assets/{RequestAnatomy-fa0JXy5p.js → RequestAnatomy-8k9dAX2j.js} +1 -1
- package/.output/public/assets/ResponseView-DEL6JcXm.js +3 -0
- package/.output/public/assets/StreamingChunkSequence-DngmoR9j.js +1 -0
- package/.output/public/assets/{_sessionId-D5fYH2C0.js → _sessionId-4vk9RMZ5.js} +1 -1
- package/.output/public/assets/index-DBEB1rHe.css +1 -0
- package/.output/public/assets/{index-BEsqvywM.js → index-DINLvnm9.js} +1 -1
- package/.output/public/assets/index-LPIGiUMP.js +1 -0
- package/.output/public/assets/{json-viewer-B0QRbMSf.js → json-viewer-CH5dvS2Y.js} +1 -1
- package/.output/public/assets/{main-CuKZD7n0.js → main-GWkZn4xy.js} +2 -2
- package/.output/server/{_sessionId-DdBTK3qm.mjs → _sessionId-EgsjdkN7.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-m82cVZ73.mjs → CompareDrawer-CaM8FYuD.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-7geuf78f.mjs → ProxyViewerContainer-EJIogcGd.mjs} +545 -309
- package/.output/server/_ssr/{ReplayDialog-B8RlWLLH.mjs → ReplayDialog-BPpJ2-q8.mjs} +58 -23
- package/.output/server/_ssr/{RequestAnatomy-BE7pmwpv.mjs → RequestAnatomy-DeQAWnZx.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-D-sDsdQr.mjs → ResponseView-Dshm8iQ7.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-DU_hThJr.mjs → StreamingChunkSequence-32yIU2hn.mjs} +30 -20
- package/.output/server/_ssr/{index-D0sV8UbE.mjs → index-XRcx9blU.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-CaDh1zrw.mjs → json-viewer-C0aX5rqC.mjs} +2 -2
- package/.output/server/_ssr/{router-Zh2iZ_u0.mjs → router-BI-Mvxz9.mjs} +441 -172
- package/.output/server/_tanstack-start-manifest_v-jKScklL_.mjs +4 -0
- package/.output/server/index.mjs +71 -71
- 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 +55 -8
- package/src/components/ProxyViewerContainer.tsx +30 -19
- package/src/components/clients/ClientLogo.tsx +22 -2
- 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/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/.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
|
@@ -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
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
setReplayHistory(
|
|
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
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
43
|
+
if (!containerExpanded) return;
|
|
44
|
+
if (loadedLogIdRef.current === logId) return;
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
const controller = new AbortController();
|
|
36
47
|
setChunkState({ status: "loading" });
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.then((data
|
|
46
|
-
|
|
47
|
-
|
|
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 (
|
|
52
|
-
|
|
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
|
-
|
|
70
|
+
controller.abort();
|
|
58
71
|
};
|
|
59
72
|
}, [containerExpanded, logId]);
|
|
60
73
|
|
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
|
+
}
|