@tonyclaw/agent-inspector 2.0.18 → 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.
Files changed (72) hide show
  1. package/.output/cli.js +137 -8
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-CepU-dLW.js → CompareDrawer-D-90PIPw.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-CxRDWacg.js +114 -0
  5. package/.output/public/assets/ReplayDialog-DzlLYoTO.js +1 -0
  6. package/.output/public/assets/{RequestAnatomy-C_C6ZU3T.js → RequestAnatomy-0hbyf6oa.js} +1 -1
  7. package/.output/public/assets/ResponseView-CKe0tNqr.js +1 -0
  8. package/.output/public/assets/StreamingChunkSequence-enhfdeNh.js +1 -0
  9. package/.output/public/assets/_sessionId-CJ-SusES.js +1 -0
  10. package/.output/public/assets/index-BbFsmQRC.js +1 -0
  11. package/.output/public/assets/index-BfBgrkgU.css +1 -0
  12. package/.output/public/assets/{main-BRXsrxtv.js → main-o9-hg8g1.js} +2 -2
  13. package/.output/server/_libs/lucide-react.mjs +141 -118
  14. package/.output/server/_libs/tanstack__react-virtual.mjs +13 -2
  15. package/.output/server/_libs/tanstack__virtual-core.mjs +24 -1
  16. package/.output/server/{_sessionId-B8Bbfs-v.mjs → _sessionId-F8lm6PIb.mjs} +3 -2
  17. package/.output/server/_ssr/{CompareDrawer-TgGZLf1J.mjs → CompareDrawer-Cf1yfwfd.mjs} +4 -3
  18. package/.output/server/_ssr/{ProxyViewerContainer-B_ppaNmQ.mjs → ProxyViewerContainer-Bmu9867L.mjs} +349 -74
  19. package/.output/server/_ssr/{ReplayDialog-BXlajnMC.mjs → ReplayDialog-DxZfLBOe.mjs} +199 -17
  20. package/.output/server/_ssr/{RequestAnatomy-CCrx09-i.mjs → RequestAnatomy-CbdERGZd.mjs} +4 -3
  21. package/.output/server/_ssr/{ResponseView-BxtMJrov.mjs → ResponseView-CvLd6qxI.mjs} +4 -3
  22. package/.output/server/_ssr/{StreamingChunkSequence-BlpdSPb8.mjs → StreamingChunkSequence-B_lkRB6o.mjs} +4 -3
  23. package/.output/server/_ssr/{index-BeTMNSJy.mjs → index-ejrSItfz.mjs} +3 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{router-Ddl1tuU7.mjs → router-SbCnbDc2.mjs} +872 -232
  26. package/.output/server/{_tanstack-start-manifest_v-Bvdz_U0f.mjs → _tanstack-start-manifest_v-yx8ftqWz.mjs} +1 -1
  27. package/.output/server/index.mjs +54 -54
  28. package/README.md +11 -109
  29. package/package.json +2 -1
  30. package/src/cli/doctor.ts +150 -5
  31. package/src/components/OnboardingBanner.tsx +13 -13
  32. package/src/components/ProxyViewer.tsx +55 -38
  33. package/src/components/ProxyViewerContainer.tsx +5 -0
  34. package/src/components/providers/SettingsDialog.tsx +113 -2
  35. package/src/components/proxy-viewer/ConversationGroup.tsx +20 -3
  36. package/src/components/proxy-viewer/ConversationGroupList.tsx +144 -0
  37. package/src/components/proxy-viewer/ConversationHeader.tsx +0 -2
  38. package/src/components/proxy-viewer/ReplayDialog.tsx +200 -13
  39. package/src/components/proxy-viewer/index.ts +1 -1
  40. package/src/knowledge/openclawClient.ts +0 -8
  41. package/src/knowledge/types.ts +0 -2
  42. package/src/lib/export-logs.ts +72 -5
  43. package/src/lib/providerTestContract.ts +0 -1
  44. package/src/lib/useProviders.ts +1 -1
  45. package/src/mcp/server.ts +14 -6
  46. package/src/mcp/toolHandlers.ts +21 -4
  47. package/src/proxy/chunkStorage.ts +2 -4
  48. package/src/proxy/dataDir.ts +3 -3
  49. package/src/proxy/formats/anthropic/index.ts +0 -1
  50. package/src/proxy/formats/anthropic/schemas.ts +0 -2
  51. package/src/proxy/handler.ts +41 -29
  52. package/src/proxy/logFinalizer.ts +93 -11
  53. package/src/proxy/logger.ts +35 -10
  54. package/src/proxy/providers.ts +12 -5
  55. package/src/proxy/rawStreamCapture.ts +85 -0
  56. package/src/proxy/schemas.ts +1 -51
  57. package/src/proxy/sessionRuntime.ts +0 -9
  58. package/src/proxy/socketTracker.ts +15 -8
  59. package/src/proxy/store.ts +414 -13
  60. package/src/routes/api/logs.$id.replay.ts +66 -1
  61. package/src/routes/api/logs.stream.ts +11 -3
  62. package/src/routes/api/logs.ts +51 -17
  63. package/src/routes/api/providers.$providerId.ts +10 -3
  64. package/src/routes/api/providers.ts +6 -3
  65. package/.output/public/assets/ProxyViewerContainer-BG7rc33d.js +0 -114
  66. package/.output/public/assets/ReplayDialog-DK6SPv8X.js +0 -1
  67. package/.output/public/assets/ResponseView-Bqpra4Zw.js +0 -1
  68. package/.output/public/assets/StreamingChunkSequence-CdPbpprq.js +0 -1
  69. package/.output/public/assets/_sessionId-CsNARHj5.js +0 -1
  70. package/.output/public/assets/index-37NfO0zx.js +0 -1
  71. package/.output/public/assets/index-D_nZj9Vt.css +0 -1
  72. 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 { CapturedLog } from "../../proxy/schemas";
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 log.rawRequestBody ?? "{}";
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
- setModifiedBody(result.body);
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
- <div>
262
- <label className="text-sm font-medium mb-2 block">Request Body (JSON)</label>
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
- className="w-full h-64 p-3 font-mono text-xs bg-muted rounded-md border border-input resize-none focus:outline-none focus:ring-2 focus:ring-ring"
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) => setModifiedBody(e.target.value)}
442
+ onChange={(e) => handleBodyChange(e.target.value)}
270
443
  spellCheck={false}
271
444
  />
272
445
  </TooltipTrigger>
273
446
  <TooltipContent>
274
- Edit the request body before re-sending to the provider
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, ViewMode } from "./ConversationHeader";
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
- }
@@ -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()),
@@ -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
- export async function exportLogsAsZip(logs: CapturedLog[]): Promise<void> {
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 {
@@ -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 full ProviderConfig array, including apiKey in PLAINTEXT. MCP is localhost-only any process that can call /api/mcp can also read <dataDir>/providers.json directly, so the apiKey is not redacted. Do not expose this MCP server to non-trusted local processes.",
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 the full ProviderConfig for the given id, including apiKey in PLAINTEXT (same posture as inspector_list_providers).",
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];
@@ -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 async function listProvidersImpl(callApi: CallApiFn): Promise<ToolResult> {
151
- const res = await callApi("/api/providers");
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(callApi: CallApiFn, id: string): Promise<ToolResult> {
157
- const res = await callApi(`/api/providers/${encodeURIComponent(id)}`);
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
- // eslint-disable-next-line no-console
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
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
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;
@@ -10,9 +10,9 @@ const LEGACY_DATA_DIR_NAMES = [".llm-inspector", ".llm-agent"];
10
10
  * so every store agrees on the same path.
11
11
  *
12
12
  * Resolution order (first match wins):
13
- * 1. `AGENT_INSPECTOR_DATA_DIR` env var absolute path used as-is,
13
+ * 1. `AGENT_INSPECTOR_DATA_DIR` env var - absolute path used as-is,
14
14
  * relative path joined to the OS-specific base below.
15
- * 2. `AGENT_INSPECTOR_CONFIG_DIR` env var kept for backward compat
15
+ * 2. `AGENT_INSPECTOR_CONFIG_DIR` env var - kept for backward compat
16
16
  * with the runtime config; treated as a directory *name* (relative
17
17
  * to the OS-specific base) when not absolute.
18
18
  * 3. OS-specific base + `.agent-inspector`.
@@ -86,7 +86,7 @@ export function hasExplicitDataDir(): boolean {
86
86
  );
87
87
  }
88
88
 
89
- function migrateLegacyDataDirs(): void {
89
+ export function migrateLegacyDataDirs(): void {
90
90
  if (hasExplicitDataDir()) return;
91
91
 
92
92
  const base = defaultBaseDir();
@@ -6,7 +6,6 @@ export {
6
6
  AnthropicRequestSchema,
7
7
  AnthropicResponseSchema,
8
8
  SseEventSchema,
9
- type AnthropicRequest,
10
9
  type AnthropicResponse,
11
10
  type ResponseContentBlockType,
12
11
  } from "./schemas";
@@ -226,12 +226,10 @@ export const SseEventSchema = z.discriminatedUnion("type", [
226
226
  SseErrorEvent,
227
227
  ]);
228
228
 
229
- export type AnthropicRequest = z.infer<typeof AnthropicRequestSchema>;
230
229
  export type AnthropicResponse = z.infer<typeof AnthropicResponseSchema>;
231
230
  export type ResponseContentBlockType = z.infer<typeof ResponseContentBlock>;
232
231
 
233
232
  // Backward-compatible aliases (originally named Inspector*)
234
233
  export const InspectorRequestSchema = AnthropicRequestSchema;
235
234
  export const InspectorResponseSchema = AnthropicResponseSchema;
236
- export type InspectorRequest = AnthropicRequest;
237
235
  export type InspectorResponse = AnthropicResponse;