@tonyclaw/agent-inspector 2.0.17 → 2.0.19
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 +149 -40
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-DDg7Cs4N.js → CompareDrawer-D-90PIPw.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CxRDWacg.js +114 -0
- package/.output/public/assets/ReplayDialog-DzlLYoTO.js +1 -0
- package/.output/public/assets/{RequestAnatomy-DR8OICOJ.js → RequestAnatomy-0hbyf6oa.js} +1 -1
- package/.output/public/assets/ResponseView-CKe0tNqr.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-enhfdeNh.js +1 -0
- package/.output/public/assets/_sessionId-CJ-SusES.js +1 -0
- package/.output/public/assets/index-BbFsmQRC.js +1 -0
- package/.output/public/assets/index-BfBgrkgU.css +1 -0
- package/.output/public/assets/{main-bwZlEXw2.js → main-o9-hg8g1.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +141 -118
- package/.output/server/_libs/tanstack__react-virtual.mjs +13 -2
- package/.output/server/_libs/tanstack__virtual-core.mjs +24 -1
- package/.output/server/{_sessionId-BEuJJhqT.mjs → _sessionId-F8lm6PIb.mjs} +3 -2
- package/.output/server/_ssr/{CompareDrawer-yTO93GMz.mjs → CompareDrawer-Cf1yfwfd.mjs} +4 -3
- package/.output/server/_ssr/{ProxyViewerContainer-C8qCkHGB.mjs → ProxyViewerContainer-Bmu9867L.mjs} +349 -74
- package/.output/server/_ssr/{ReplayDialog-DRF9PG7Z.mjs → ReplayDialog-DxZfLBOe.mjs} +199 -17
- package/.output/server/_ssr/{RequestAnatomy-DBcuV_jV.mjs → RequestAnatomy-CbdERGZd.mjs} +4 -3
- package/.output/server/_ssr/{ResponseView-JJm78HZT.mjs → ResponseView-CvLd6qxI.mjs} +4 -3
- package/.output/server/_ssr/{StreamingChunkSequence-DyGKjBYx.mjs → StreamingChunkSequence-B_lkRB6o.mjs} +4 -3
- package/.output/server/_ssr/{index-T7JG28t6.mjs → index-ejrSItfz.mjs} +3 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-BJob8RN3.mjs → router-SbCnbDc2.mjs} +896 -240
- package/.output/server/{_tanstack-start-manifest_v-CuiFi3XC.mjs → _tanstack-start-manifest_v-yx8ftqWz.mjs} +1 -1
- package/.output/server/index.mjs +60 -60
- package/README.md +26 -109
- package/package.json +2 -1
- package/src/cli/doctor.ts +160 -8
- package/src/cli/templates/skill-onboard.ts +2 -20
- package/src/components/OnboardingBanner.tsx +13 -13
- package/src/components/ProxyViewer.tsx +55 -38
- package/src/components/ProxyViewerContainer.tsx +5 -0
- package/src/components/providers/SettingsDialog.tsx +113 -2
- package/src/components/proxy-viewer/ConversationGroup.tsx +20 -3
- package/src/components/proxy-viewer/ConversationGroupList.tsx +144 -0
- package/src/components/proxy-viewer/ConversationHeader.tsx +0 -2
- package/src/components/proxy-viewer/ReplayDialog.tsx +200 -13
- package/src/components/proxy-viewer/index.ts +1 -1
- package/src/knowledge/openclawClient.ts +0 -8
- package/src/knowledge/types.ts +0 -2
- package/src/lib/export-logs.ts +72 -5
- package/src/lib/providerTestContract.ts +0 -1
- package/src/lib/useProviders.ts +1 -1
- package/src/mcp/server.ts +14 -6
- package/src/mcp/toolHandlers.ts +21 -4
- package/src/proxy/chunkStorage.ts +2 -4
- package/src/proxy/dataDir.ts +37 -15
- package/src/proxy/formats/anthropic/index.ts +0 -1
- package/src/proxy/formats/anthropic/schemas.ts +0 -2
- package/src/proxy/handler.ts +41 -29
- package/src/proxy/logFinalizer.ts +93 -11
- package/src/proxy/logger.ts +35 -10
- package/src/proxy/providers.ts +12 -5
- package/src/proxy/rawStreamCapture.ts +85 -0
- package/src/proxy/schemas.ts +1 -51
- package/src/proxy/sessionRuntime.ts +0 -9
- package/src/proxy/socketTracker.ts +15 -8
- package/src/proxy/store.ts +414 -13
- package/src/routes/api/logs.$id.replay.ts +66 -1
- package/src/routes/api/logs.stream.ts +11 -3
- package/src/routes/api/logs.ts +51 -17
- package/src/routes/api/providers.$providerId.ts +10 -3
- package/src/routes/api/providers.ts +6 -3
- package/.output/public/assets/ProxyViewerContainer-DbbK1y7N.js +0 -114
- package/.output/public/assets/ReplayDialog-C1zWNkoz.js +0 -1
- package/.output/public/assets/ResponseView-Dh9iSj6h.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-B_xElyH3.js +0 -1
- package/.output/public/assets/_sessionId-gv1a_NNu.js +0 -1
- package/.output/public/assets/index-D_nZj9Vt.css +0 -1
- package/.output/public/assets/index-u-LwDaeH.js +0 -1
- package/src/components/ui/json-expansion-button.tsx +0 -56
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { RotateCcw } from "lucide-react";
|
|
1
|
+
import { Braces, Minimize2, RotateCcw } from "lucide-react";
|
|
2
2
|
import type { JSX } from "react";
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { useProviders } from "../../lib/useProviders";
|
|
6
6
|
import { Button } from "../ui/button";
|
|
@@ -8,7 +8,8 @@ import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/
|
|
|
8
8
|
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
|
-
import type
|
|
11
|
+
import { CapturedLogSchema, type CapturedLog } from "../../proxy/schemas";
|
|
12
|
+
import { dispatchLogFocusRequest } from "./logFocus";
|
|
12
13
|
import {
|
|
13
14
|
buildReplayComparisons,
|
|
14
15
|
buildReplayMetrics,
|
|
@@ -27,6 +28,7 @@ const ReplayResultSchema = z.object({
|
|
|
27
28
|
outputTokens: z.number().nullable().optional(),
|
|
28
29
|
elapsedMs: z.number().nullable().optional(),
|
|
29
30
|
streaming: z.boolean().optional(),
|
|
31
|
+
replayLogId: z.number().optional(),
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
type ReplayResult = z.infer<typeof ReplayResultSchema>;
|
|
@@ -43,6 +45,43 @@ type ReplayModelOption = {
|
|
|
43
45
|
model: string;
|
|
44
46
|
};
|
|
45
47
|
|
|
48
|
+
type JsonEditState = { valid: true; message: string } | { valid: false; message: string };
|
|
49
|
+
|
|
50
|
+
const ReplayHistorySchema = z.object({
|
|
51
|
+
logs: z.array(CapturedLogSchema),
|
|
52
|
+
total: z.number(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function formatJsonForEdit(value: string): string {
|
|
56
|
+
try {
|
|
57
|
+
const parsed: unknown = JSON.parse(value);
|
|
58
|
+
return JSON.stringify(parsed, null, 2);
|
|
59
|
+
} catch {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function compactJsonForEdit(value: string): string {
|
|
65
|
+
try {
|
|
66
|
+
const parsed: unknown = JSON.parse(value);
|
|
67
|
+
return JSON.stringify(parsed);
|
|
68
|
+
} catch {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readJsonEditState(value: string): JsonEditState {
|
|
74
|
+
try {
|
|
75
|
+
JSON.parse(value);
|
|
76
|
+
return { valid: true, message: "Valid JSON" };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return {
|
|
79
|
+
valid: false,
|
|
80
|
+
message: err instanceof Error ? err.message : "Invalid JSON",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
46
85
|
function formatElapsed(ms: number): string {
|
|
47
86
|
if (ms < 1000) return `${String(ms)}ms`;
|
|
48
87
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
@@ -122,14 +161,40 @@ function ReplayComparisonTable({
|
|
|
122
161
|
|
|
123
162
|
export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JSX.Element {
|
|
124
163
|
const { providers } = useProviders();
|
|
164
|
+
const originalBody = log.rawRequestBody ?? "{}";
|
|
125
165
|
const [modifiedBody, setModifiedBody] = useState<string>(() => {
|
|
126
|
-
return
|
|
166
|
+
return formatJsonForEdit(originalBody);
|
|
127
167
|
});
|
|
128
168
|
const [replayResult, setReplayResult] = useState<ReplayResult | null>(null);
|
|
169
|
+
const [replayHistory, setReplayHistory] = useState<CapturedLog[]>([]);
|
|
129
170
|
const [loading, setLoading] = useState(false);
|
|
130
171
|
const [error, setError] = useState<string | null>(null);
|
|
172
|
+
const jsonEditState = useMemo(() => readJsonEditState(modifiedBody), [modifiedBody]);
|
|
173
|
+
|
|
174
|
+
const refreshReplayHistory = useCallback(async (): Promise<void> => {
|
|
175
|
+
if (!open) return;
|
|
176
|
+
try {
|
|
177
|
+
const response = await fetch(`/api/logs?replayOf=${String(log.id)}`);
|
|
178
|
+
if (!response.ok) return;
|
|
179
|
+
const data: unknown = await response.json();
|
|
180
|
+
const parsed = ReplayHistorySchema.safeParse(data);
|
|
181
|
+
if (!parsed.success) return;
|
|
182
|
+
setReplayHistory(parsed.data.logs);
|
|
183
|
+
} catch {
|
|
184
|
+
// Replay history is helpful context, not required for replay itself.
|
|
185
|
+
}
|
|
186
|
+
}, [log.id, open]);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
void refreshReplayHistory();
|
|
190
|
+
}, [refreshReplayHistory]);
|
|
131
191
|
|
|
132
192
|
async function handleReplay() {
|
|
193
|
+
if (!jsonEditState.valid) {
|
|
194
|
+
setError("Request body must be valid JSON before replay.");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
133
198
|
setLoading(true);
|
|
134
199
|
setError(null);
|
|
135
200
|
setReplayResult(null);
|
|
@@ -150,6 +215,7 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
150
215
|
}
|
|
151
216
|
const data = parsed.data;
|
|
152
217
|
setReplayResult(data);
|
|
218
|
+
await refreshReplayHistory();
|
|
153
219
|
if (!data.success) {
|
|
154
220
|
setError(data.error ?? "Replay failed");
|
|
155
221
|
}
|
|
@@ -166,6 +232,30 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
166
232
|
onOpenChange(false);
|
|
167
233
|
}
|
|
168
234
|
|
|
235
|
+
function handleBodyChange(value: string): void {
|
|
236
|
+
setModifiedBody(value);
|
|
237
|
+
setReplayResult(null);
|
|
238
|
+
setError(null);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function handleFormatBody(): void {
|
|
242
|
+
handleBodyChange(formatJsonForEdit(modifiedBody));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function handleCompactBody(): void {
|
|
246
|
+
handleBodyChange(compactJsonForEdit(modifiedBody));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function handleResetBody(): void {
|
|
250
|
+
handleBodyChange(formatJsonForEdit(originalBody));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function handleOpenReplayLog(logId: number): void {
|
|
254
|
+
window.location.hash = `log-${String(logId)}`;
|
|
255
|
+
dispatchLogFocusRequest({ logId, tab: "request" });
|
|
256
|
+
onOpenChange(false);
|
|
257
|
+
}
|
|
258
|
+
|
|
169
259
|
const replayModelOptions = useMemo<ReplayModelOption[]>(() => {
|
|
170
260
|
const options: ReplayModelOption[] = [];
|
|
171
261
|
for (const provider of providers) {
|
|
@@ -188,9 +278,7 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
188
278
|
setError(result.error);
|
|
189
279
|
return;
|
|
190
280
|
}
|
|
191
|
-
|
|
192
|
-
setReplayResult(null);
|
|
193
|
-
setError(null);
|
|
281
|
+
handleBodyChange(formatJsonForEdit(result.body));
|
|
194
282
|
}
|
|
195
283
|
|
|
196
284
|
const originalMetrics = buildReplayMetrics({
|
|
@@ -214,6 +302,7 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
214
302
|
});
|
|
215
303
|
const replayComparisons =
|
|
216
304
|
replayMetrics === null ? [] : buildReplayComparisons(originalMetrics, replayMetrics);
|
|
305
|
+
const latestReplayHistory = replayHistory.slice(-5).reverse();
|
|
217
306
|
|
|
218
307
|
return (
|
|
219
308
|
<Dialog open={open} onOpenChange={handleClose}>
|
|
@@ -258,20 +347,104 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
258
347
|
</select>
|
|
259
348
|
</div>
|
|
260
349
|
)}
|
|
261
|
-
|
|
262
|
-
<
|
|
350
|
+
{latestReplayHistory.length > 0 && (
|
|
351
|
+
<div className="space-y-2 rounded-md border border-border bg-muted/20 px-3 py-2">
|
|
352
|
+
<div className="flex items-center justify-between gap-2">
|
|
353
|
+
<div className="text-xs font-medium">Replay history</div>
|
|
354
|
+
<Button
|
|
355
|
+
type="button"
|
|
356
|
+
variant="ghost"
|
|
357
|
+
size="sm"
|
|
358
|
+
onClick={() => {
|
|
359
|
+
void refreshReplayHistory();
|
|
360
|
+
}}
|
|
361
|
+
>
|
|
362
|
+
Refresh
|
|
363
|
+
</Button>
|
|
364
|
+
</div>
|
|
365
|
+
<div className="grid gap-1">
|
|
366
|
+
{latestReplayHistory.map((historyLog) => (
|
|
367
|
+
<button
|
|
368
|
+
key={historyLog.id}
|
|
369
|
+
type="button"
|
|
370
|
+
onClick={() => handleOpenReplayLog(historyLog.id)}
|
|
371
|
+
className="grid grid-cols-[auto_1fr_auto_auto] items-center gap-2 rounded-sm px-2 py-1 text-left text-xs hover:bg-muted"
|
|
372
|
+
>
|
|
373
|
+
<span className="font-mono text-emerald-600">#{historyLog.id}</span>
|
|
374
|
+
<span className="truncate font-mono text-muted-foreground">
|
|
375
|
+
{historyLog.model ?? "unknown"}
|
|
376
|
+
</span>
|
|
377
|
+
<span className="font-mono text-muted-foreground">
|
|
378
|
+
{historyLog.responseStatus ?? "-"}
|
|
379
|
+
</span>
|
|
380
|
+
<span className="font-mono text-muted-foreground">
|
|
381
|
+
{historyLog.elapsedMs === null ? "-" : formatElapsed(historyLog.elapsedMs)}
|
|
382
|
+
</span>
|
|
383
|
+
</button>
|
|
384
|
+
))}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
<div className="space-y-2">
|
|
389
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
390
|
+
<label htmlFor={`replay-body-${String(log.id)}`} className="text-sm font-medium">
|
|
391
|
+
Request Body
|
|
392
|
+
</label>
|
|
393
|
+
<div className="flex items-center gap-2">
|
|
394
|
+
<span
|
|
395
|
+
className={
|
|
396
|
+
jsonEditState.valid ? "text-xs text-emerald-500" : "text-xs text-destructive"
|
|
397
|
+
}
|
|
398
|
+
>
|
|
399
|
+
{jsonEditState.message}
|
|
400
|
+
</span>
|
|
401
|
+
<Button
|
|
402
|
+
type="button"
|
|
403
|
+
variant="outline"
|
|
404
|
+
size="sm"
|
|
405
|
+
onClick={handleFormatBody}
|
|
406
|
+
disabled={!jsonEditState.valid}
|
|
407
|
+
title="Format JSON"
|
|
408
|
+
>
|
|
409
|
+
<Braces className="size-3.5" />
|
|
410
|
+
Format
|
|
411
|
+
</Button>
|
|
412
|
+
<Button
|
|
413
|
+
type="button"
|
|
414
|
+
variant="outline"
|
|
415
|
+
size="sm"
|
|
416
|
+
onClick={handleCompactBody}
|
|
417
|
+
disabled={!jsonEditState.valid}
|
|
418
|
+
title="Compact JSON"
|
|
419
|
+
>
|
|
420
|
+
<Minimize2 className="size-3.5" />
|
|
421
|
+
Compact
|
|
422
|
+
</Button>
|
|
423
|
+
<Button
|
|
424
|
+
type="button"
|
|
425
|
+
variant="ghost"
|
|
426
|
+
size="sm"
|
|
427
|
+
onClick={handleResetBody}
|
|
428
|
+
title="Reset to original request"
|
|
429
|
+
>
|
|
430
|
+
<RotateCcw className="size-3.5" />
|
|
431
|
+
Reset
|
|
432
|
+
</Button>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
263
435
|
<TooltipProvider>
|
|
264
436
|
<Tooltip>
|
|
265
437
|
<TooltipTrigger asChild>
|
|
266
438
|
<textarea
|
|
267
|
-
|
|
439
|
+
id={`replay-body-${String(log.id)}`}
|
|
440
|
+
className="w-full min-h-80 p-3 font-mono text-xs leading-5 bg-muted rounded-md border border-input resize-y tabular-nums focus:outline-none focus:ring-2 focus:ring-ring"
|
|
268
441
|
value={modifiedBody}
|
|
269
|
-
onChange={(e) =>
|
|
442
|
+
onChange={(e) => handleBodyChange(e.target.value)}
|
|
270
443
|
spellCheck={false}
|
|
271
444
|
/>
|
|
272
445
|
</TooltipTrigger>
|
|
273
446
|
<TooltipContent>
|
|
274
|
-
Edit
|
|
447
|
+
Edit formatted JSON before re-sending to the provider
|
|
275
448
|
</TooltipContent>
|
|
276
449
|
</Tooltip>
|
|
277
450
|
</TooltipProvider>
|
|
@@ -288,7 +461,7 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
288
461
|
onClick={() => {
|
|
289
462
|
void handleReplay();
|
|
290
463
|
}}
|
|
291
|
-
disabled={loading}
|
|
464
|
+
disabled={loading || !jsonEditState.valid}
|
|
292
465
|
>
|
|
293
466
|
{loading ? "Replaying..." : "Replay"}
|
|
294
467
|
</Button>
|
|
@@ -298,6 +471,20 @@ export function ReplayDialog({ log, open, onOpenChange }: ReplayDialogProps): JS
|
|
|
298
471
|
<ReplayComparisonTable comparisons={replayComparisons.slice(0, 4)} />
|
|
299
472
|
)}
|
|
300
473
|
|
|
474
|
+
{replayResult?.success === true && replayResult.replayLogId !== undefined && (
|
|
475
|
+
<div className="flex flex-wrap items-center justify-between gap-2 rounded-md border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-600">
|
|
476
|
+
<span>Replay saved as test log #{replayResult.replayLogId}.</span>
|
|
477
|
+
<Button
|
|
478
|
+
type="button"
|
|
479
|
+
variant="outline"
|
|
480
|
+
size="sm"
|
|
481
|
+
onClick={() => handleOpenReplayLog(replayResult.replayLogId ?? log.id)}
|
|
482
|
+
>
|
|
483
|
+
Open saved log
|
|
484
|
+
</Button>
|
|
485
|
+
</div>
|
|
486
|
+
)}
|
|
487
|
+
|
|
301
488
|
{replayResult && replayResult.success && (
|
|
302
489
|
<Tabs defaultValue="parsed">
|
|
303
490
|
<TabsList>
|
|
@@ -4,6 +4,6 @@ export {
|
|
|
4
4
|
getConversationId,
|
|
5
5
|
groupLogsByConversation,
|
|
6
6
|
} from "./ConversationHeader";
|
|
7
|
-
export type { ConversationGroupData
|
|
7
|
+
export type { ConversationGroupData } from "./ConversationHeader";
|
|
8
8
|
export { LogEntry } from "./LogEntry";
|
|
9
9
|
export { ThreadConnector } from "./ThreadConnector";
|
|
@@ -26,10 +26,6 @@ function getLegacyEndpoint(): string | null {
|
|
|
26
26
|
return endpoint.trim();
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function isLegacyBackendConfigured(): boolean {
|
|
30
|
-
return getLegacyEndpoint() !== null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
29
|
function buildMemoryUrl(endpoint: string): string {
|
|
34
30
|
return endpoint.endsWith("/memories") ? endpoint : `${endpoint.replace(/\/$/, "")}/memories`;
|
|
35
31
|
}
|
|
@@ -141,7 +137,3 @@ export async function searchOpenClaw(
|
|
|
141
137
|
}
|
|
142
138
|
return { results: [], warning: "OpenClaw search returned an unparseable response." };
|
|
143
139
|
}
|
|
144
|
-
|
|
145
|
-
export function isOpenClawPromotionConfigured(): boolean {
|
|
146
|
-
return isOpenClawFileBridgeConfigured() || isLegacyBackendConfigured();
|
|
147
|
-
}
|
package/src/knowledge/types.ts
CHANGED
|
@@ -21,8 +21,6 @@ export const KnowledgeEvidenceSchema = z.object({
|
|
|
21
21
|
models: z.array(z.string()),
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
export type KnowledgeEvidence = z.infer<typeof KnowledgeEvidenceSchema>;
|
|
25
|
-
|
|
26
24
|
export const RedactionMetadataSchema = z.object({
|
|
27
25
|
redacted: z.boolean(),
|
|
28
26
|
patterns: z.array(z.string()),
|
package/src/lib/export-logs.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import JSZip from "jszip";
|
|
2
2
|
import type { CapturedLog } from "../proxy/schemas";
|
|
3
3
|
|
|
4
|
+
export type ExportMode = "redacted" | "raw";
|
|
5
|
+
|
|
6
|
+
const REDACTED = "[REDACTED]";
|
|
7
|
+
const SECRET_KEY_PATTERN =
|
|
8
|
+
/(^|[-_])(api[-_]?key|authorization|cookie|token|secret|password|access[-_]?token|refresh[-_]?token)([-_]|$)/i;
|
|
9
|
+
const SECRET_TEXT_PATTERNS: readonly RegExp[] = [
|
|
10
|
+
/Bearer\s+[A-Za-z0-9._~+/=-]{12,}/gi,
|
|
11
|
+
/\bsk-[A-Za-z0-9_-]{12,}\b/g,
|
|
12
|
+
/\bsk-ant-[A-Za-z0-9_-]{12,}\b/g,
|
|
13
|
+
/\bghp_[A-Za-z0-9_]{12,}\b/g,
|
|
14
|
+
/\bglpat-[A-Za-z0-9_-]{12,}\b/g,
|
|
15
|
+
];
|
|
16
|
+
|
|
4
17
|
async function fetchStreamingChunks(logId: number): Promise<string | null> {
|
|
5
18
|
try {
|
|
6
19
|
const response = await fetch(`/api/logs/${logId}/chunks`);
|
|
@@ -12,16 +25,70 @@ async function fetchStreamingChunks(logId: number): Promise<string | null> {
|
|
|
12
25
|
}
|
|
13
26
|
}
|
|
14
27
|
|
|
15
|
-
|
|
28
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
29
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function redactValue(value: unknown): unknown {
|
|
33
|
+
if (Array.isArray(value)) return value.map((item) => redactValue(item));
|
|
34
|
+
if (!isRecord(value)) return value;
|
|
35
|
+
|
|
36
|
+
const result: Record<string, unknown> = {};
|
|
37
|
+
for (const [key, item] of Object.entries(value)) {
|
|
38
|
+
result[key] = SECRET_KEY_PATTERN.test(key) ? REDACTED : redactValue(item);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function redactJsonText(text: string): string {
|
|
44
|
+
const redactText = (value: string): string => {
|
|
45
|
+
let result = value;
|
|
46
|
+
for (const pattern of SECRET_TEXT_PATTERNS) {
|
|
47
|
+
result = result.replace(pattern, REDACTED);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const parsed: unknown = JSON.parse(text);
|
|
54
|
+
return redactText(JSON.stringify(redactValue(parsed), null, 2));
|
|
55
|
+
} catch {
|
|
56
|
+
return redactText(text);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function exportText(text: string, mode: ExportMode): string {
|
|
61
|
+
return mode === "redacted" ? redactJsonText(text) : text;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function exportLogsAsZip(
|
|
65
|
+
logs: CapturedLog[],
|
|
66
|
+
mode: ExportMode = "redacted",
|
|
67
|
+
): Promise<void> {
|
|
16
68
|
const zip = new JSZip();
|
|
69
|
+
const suffix = mode === "redacted" ? "redacted" : "raw";
|
|
70
|
+
zip.file(
|
|
71
|
+
"manifest.json",
|
|
72
|
+
JSON.stringify(
|
|
73
|
+
{
|
|
74
|
+
exportedAt: new Date().toISOString(),
|
|
75
|
+
mode,
|
|
76
|
+
redacted: mode === "redacted",
|
|
77
|
+
logCount: logs.length,
|
|
78
|
+
logIds: logs.map((log) => log.id),
|
|
79
|
+
},
|
|
80
|
+
null,
|
|
81
|
+
2,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
17
84
|
|
|
18
85
|
// Add regular request/response files
|
|
19
86
|
for (const log of logs) {
|
|
20
87
|
if (log.rawRequestBody !== null) {
|
|
21
|
-
zip.file(`#${log.id}.Request.json`, log.rawRequestBody);
|
|
88
|
+
zip.file(`#${log.id}.Request.json`, exportText(log.rawRequestBody, mode));
|
|
22
89
|
}
|
|
23
90
|
if (log.responseText !== null) {
|
|
24
|
-
zip.file(`#${log.id}.Response.json`, log.responseText);
|
|
91
|
+
zip.file(`#${log.id}.Response.json`, exportText(log.responseText, mode));
|
|
25
92
|
}
|
|
26
93
|
}
|
|
27
94
|
|
|
@@ -31,7 +98,7 @@ export async function exportLogsAsZip(logs: CapturedLog[]): Promise<void> {
|
|
|
31
98
|
streamingLogs.map(async (log) => {
|
|
32
99
|
const chunks = await fetchStreamingChunks(log.id);
|
|
33
100
|
if (chunks !== null) {
|
|
34
|
-
zip.file(`#${log.id}.SSE.Response.json`, chunks);
|
|
101
|
+
zip.file(`#${log.id}.SSE.Response.json`, exportText(chunks, mode));
|
|
35
102
|
}
|
|
36
103
|
}),
|
|
37
104
|
);
|
|
@@ -42,7 +109,7 @@ export async function exportLogsAsZip(logs: CapturedLog[]): Promise<void> {
|
|
|
42
109
|
|
|
43
110
|
const anchor = document.createElement("a");
|
|
44
111
|
anchor.href = url;
|
|
45
|
-
anchor.download = `agent-inspector-export-${new Date().toISOString().slice(0, 10)}.zip`;
|
|
112
|
+
anchor.download = `agent-inspector-export-${suffix}-${new Date().toISOString().slice(0, 10)}.zip`;
|
|
46
113
|
document.body.appendChild(anchor);
|
|
47
114
|
anchor.click();
|
|
48
115
|
document.body.removeChild(anchor);
|
|
@@ -73,7 +73,6 @@ export type ProviderTestErrorType = z.infer<typeof ProviderTestErrorTypeSchema>;
|
|
|
73
73
|
export type ProviderTestResult = z.infer<typeof ProviderTestResultSchema>;
|
|
74
74
|
export type ProviderTestState = z.infer<typeof ProviderTestStateSchema>;
|
|
75
75
|
export type ProviderTestResults = z.infer<typeof ProviderTestResultsSchema>;
|
|
76
|
-
export type ModelTestResults = z.infer<typeof ModelTestResultsSchema>;
|
|
77
76
|
|
|
78
77
|
export function createPendingProviderTestResults(): ProviderTestResults {
|
|
79
78
|
return {
|
package/src/lib/useProviders.ts
CHANGED
|
@@ -13,7 +13,7 @@ async function fetcher(url: string): Promise<ProviderConfig[]> {
|
|
|
13
13
|
|
|
14
14
|
export function useProviders() {
|
|
15
15
|
const response: SWRResponse<ProviderConfig[], Error> = useSWR<ProviderConfig[], Error>(
|
|
16
|
-
"/api/providers",
|
|
16
|
+
"/api/providers?includeSecrets=1",
|
|
17
17
|
fetcher,
|
|
18
18
|
{
|
|
19
19
|
revalidateOnFocus: false,
|
package/src/mcp/server.ts
CHANGED
|
@@ -216,10 +216,15 @@ function registerTools(server: McpServer): void {
|
|
|
216
216
|
{
|
|
217
217
|
title: "List configured LLM providers",
|
|
218
218
|
description:
|
|
219
|
-
"Returns the
|
|
220
|
-
inputSchema: z.object({
|
|
219
|
+
"Returns the configured ProviderConfig array with apiKey redacted by default. Set includeSecrets=true only when the user explicitly needs plaintext local provider keys; this exposes secrets from <dataDir>/providers.json.",
|
|
220
|
+
inputSchema: z.object({
|
|
221
|
+
includeSecrets: z
|
|
222
|
+
.boolean()
|
|
223
|
+
.optional()
|
|
224
|
+
.describe("Return plaintext provider API keys. Exposes local secrets when true."),
|
|
225
|
+
}),
|
|
221
226
|
},
|
|
222
|
-
() => safeCall(() => listProvidersImpl(callApi)),
|
|
227
|
+
(args) => safeCall(() => listProvidersImpl(callApi, args)),
|
|
223
228
|
);
|
|
224
229
|
|
|
225
230
|
// ----- Action tools -----
|
|
@@ -229,12 +234,16 @@ function registerTools(server: McpServer): void {
|
|
|
229
234
|
{
|
|
230
235
|
title: "Get a single provider by id",
|
|
231
236
|
description:
|
|
232
|
-
"Returns
|
|
237
|
+
"Returns one ProviderConfig with apiKey redacted by default. Set includeSecrets=true only when the user explicitly needs the plaintext local provider key.",
|
|
233
238
|
inputSchema: z.object({
|
|
234
239
|
id: z.string().describe("The provider id."),
|
|
240
|
+
includeSecrets: z
|
|
241
|
+
.boolean()
|
|
242
|
+
.optional()
|
|
243
|
+
.describe("Return the plaintext provider API key. Exposes a local secret when true."),
|
|
235
244
|
}),
|
|
236
245
|
},
|
|
237
|
-
({ id }) => safeCall(() => getProviderImpl(callApi, id)),
|
|
246
|
+
({ id, includeSecrets }) => safeCall(() => getProviderImpl(callApi, id, { includeSecrets })),
|
|
238
247
|
);
|
|
239
248
|
|
|
240
249
|
server.registerTool(
|
|
@@ -393,4 +402,3 @@ export const TOOL_NAMES = [
|
|
|
393
402
|
"inspector_search_knowledge",
|
|
394
403
|
"inspector_get_project_context",
|
|
395
404
|
] as const;
|
|
396
|
-
export type ToolName = (typeof TOOL_NAMES)[number];
|
package/src/mcp/toolHandlers.ts
CHANGED
|
@@ -147,14 +147,31 @@ export async function listModelsImpl(callApi: CallApiFn): Promise<ToolResult> {
|
|
|
147
147
|
return textJson(await res.json());
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
export
|
|
151
|
-
|
|
150
|
+
export type ProviderReadArgs = {
|
|
151
|
+
includeSecrets?: boolean;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
function includeSecretsQuery(includeSecrets: boolean | undefined): string {
|
|
155
|
+
return includeSecrets === true ? "?includeSecrets=1" : "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function listProvidersImpl(
|
|
159
|
+
callApi: CallApiFn,
|
|
160
|
+
args: ProviderReadArgs = {},
|
|
161
|
+
): Promise<ToolResult> {
|
|
162
|
+
const res = await callApi(`/api/providers${includeSecretsQuery(args.includeSecrets)}`);
|
|
152
163
|
if (!res.ok) return toolError(`GET /api/providers returned ${res.status}`);
|
|
153
164
|
return textJson(await res.json());
|
|
154
165
|
}
|
|
155
166
|
|
|
156
|
-
export async function getProviderImpl(
|
|
157
|
-
|
|
167
|
+
export async function getProviderImpl(
|
|
168
|
+
callApi: CallApiFn,
|
|
169
|
+
id: string,
|
|
170
|
+
args: ProviderReadArgs = {},
|
|
171
|
+
): Promise<ToolResult> {
|
|
172
|
+
const res = await callApi(
|
|
173
|
+
`/api/providers/${encodeURIComponent(id)}${includeSecretsQuery(args.includeSecrets)}`,
|
|
174
|
+
);
|
|
158
175
|
if (!res.ok) return toolError(`GET /api/providers/${id} returned ${res.status}`);
|
|
159
176
|
return textJson(await res.json());
|
|
160
177
|
}
|
|
@@ -76,8 +76,7 @@ export function writeChunks(logId: number, chunks: StreamingChunk[], truncated?:
|
|
|
76
76
|
try {
|
|
77
77
|
renameSync(tempPath, targetPath);
|
|
78
78
|
} catch (err) {
|
|
79
|
-
|
|
80
|
-
console.warn("[chunkStorage] Rename failed, falling back to copy+delete:", err);
|
|
79
|
+
logger.warn("[chunkStorage] Rename failed, falling back to copy+delete:", String(err));
|
|
81
80
|
// If rename fails, try copy + delete
|
|
82
81
|
try {
|
|
83
82
|
copyFileSync(tempPath, targetPath);
|
|
@@ -100,8 +99,7 @@ export function readChunks(path: string): StreamingChunksData | null {
|
|
|
100
99
|
}
|
|
101
100
|
try {
|
|
102
101
|
const content = readFileSync(path, "utf-8");
|
|
103
|
-
|
|
104
|
-
const parsed = JSON.parse(content);
|
|
102
|
+
const parsed: unknown = JSON.parse(content);
|
|
105
103
|
const result = StreamingChunksDataSchema.safeParse(parsed);
|
|
106
104
|
if (!result.success) return null;
|
|
107
105
|
return result.data;
|