@tonyclaw/agent-inspector 2.0.14 → 2.0.15

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 (37) hide show
  1. package/.output/cli.js +84 -0
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-BR_kCD7Q.js → CompareDrawer-B6WCFRJq.js} +1 -1
  4. package/.output/public/assets/{ProxyViewerContainer-vc50msJj.js → ProxyViewerContainer-CofGRJoO.js} +31 -31
  5. package/.output/public/assets/{ReplayDialog-DsAYQBHZ.js → ReplayDialog-O_53zkX8.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-B4uqyUt8.js → RequestAnatomy-CqBSMZv5.js} +1 -1
  7. package/.output/public/assets/{ResponseView-C1vQL-fM.js → ResponseView-BvbA6zot.js} +1 -1
  8. package/.output/public/assets/{StreamingChunkSequence-BECMV-xf.js → StreamingChunkSequence-Bnf6djjc.js} +1 -1
  9. package/.output/public/assets/_sessionId-9ICCw6YA.js +1 -0
  10. package/.output/public/assets/index-DVkMYH8H.js +1 -0
  11. package/.output/public/assets/{main-D6wegveF.js → main-Biir9ZGe.js} +2 -2
  12. package/.output/server/{_sessionId-CeHY-3xa.mjs → _sessionId-B77Gh4IL.mjs} +2 -2
  13. package/.output/server/_ssr/{CompareDrawer-blWGaGiT.mjs → CompareDrawer-D4-RHFqW.mjs} +2 -2
  14. package/.output/server/_ssr/{ProxyViewerContainer-Dt_9BuM9.mjs → ProxyViewerContainer-YP28-_2H.mjs} +31 -10
  15. package/.output/server/_ssr/{ReplayDialog-mzsq7AjW.mjs → ReplayDialog-D9rQlrcY.mjs} +3 -3
  16. package/.output/server/_ssr/{RequestAnatomy-CDAcXMBK.mjs → RequestAnatomy-BXCRWD9H.mjs} +2 -2
  17. package/.output/server/_ssr/{ResponseView-B__xXCsZ.mjs → ResponseView-DEwWMqSS.mjs} +2 -2
  18. package/.output/server/_ssr/{StreamingChunkSequence-BnJkTdXF.mjs → StreamingChunkSequence-CdrTPf5v.mjs} +2 -2
  19. package/.output/server/_ssr/{index-B2-uF9XT.mjs → index-Chf1DYqT.mjs} +2 -2
  20. package/.output/server/_ssr/index.mjs +2 -2
  21. package/.output/server/_ssr/{router-BpEBhA7S.mjs → router-BHXU5jWm.mjs} +89 -46
  22. package/.output/server/{_tanstack-start-manifest_v-CHekFSzj.mjs → _tanstack-start-manifest_v-D1dtacWy.mjs} +1 -1
  23. package/.output/server/index.mjs +63 -63
  24. package/README.md +31 -1
  25. package/package.json +1 -1
  26. package/src/cli.ts +91 -0
  27. package/src/components/ProxyViewer.tsx +32 -24
  28. package/src/components/ProxyViewerContainer.tsx +2 -1
  29. package/src/components/providers/SettingsDialog.tsx +10 -0
  30. package/src/lib/runtimeConfig.ts +4 -0
  31. package/src/lib/useStripConfig.ts +10 -2
  32. package/src/proxy/config.ts +33 -3
  33. package/src/proxy/handler.ts +30 -13
  34. package/src/proxy/logFinalizer.ts +4 -2
  35. package/src/routes/api/providers.$providerId.test.log.ts +17 -7
  36. package/.output/public/assets/_sessionId-3bjQPmHl.js +0 -1
  37. package/.output/public/assets/index-_7blCOK0.js +0 -1
package/README.md CHANGED
@@ -7,7 +7,7 @@ The npm package and CLI remain `agent-inspector`; the platform name is Agent Ins
7
7
  ## What It Does
8
8
 
9
9
  - Runs a local transparent proxy for Anthropic and OpenAI-compatible APIs.
10
- - Captures model requests, responses, streaming chunks, tool definitions, token usage, and errors.
10
+ - Captures model requests, responses, tool definitions, token usage, and errors.
11
11
  - Provides a web UI for browsing sessions and logs in real time.
12
12
  - Includes a Chrome companion extension for quick capture status and log navigation.
13
13
  - Generates reviewable knowledge candidates from agent sessions.
@@ -71,6 +71,36 @@ ANTHROPIC_BASE_URL=http://localhost:25947/proxy <tool>
71
71
 
72
72
  The web UI runs at http://localhost:25947.
73
73
 
74
+ ## Capture Modes
75
+
76
+ Agent Inspector starts in `simple` capture mode by default:
77
+
78
+ ```bash
79
+ agent-inspector
80
+ agent-inspector --mode simple
81
+ ```
82
+
83
+ Simple mode keeps the proxy lightweight for everyday use. It records the data
84
+ needed for the session timeline, request/response views, token usage, provider
85
+ routing, errors, and knowledge workflows, but it does not retain raw request
86
+ headers, upstream headers, or detailed streaming chunk artifacts.
87
+
88
+ Use `full` mode when you need protocol-level diagnostics:
89
+
90
+ ```bash
91
+ agent-inspector --mode full
92
+ ```
93
+
94
+ Full mode captures the additional raw details used by the `Raw Headers`,
95
+ `Headers`, and `Raw Response` views. It is useful for provider debugging,
96
+ header transformation checks, and SSE stream inspection, but it uses more CPU,
97
+ memory, and disk.
98
+
99
+ The mode is decided at process start. To change modes, restart the server. You
100
+ can also set `AGENT_INSPECTOR_MODE=full` or
101
+ `AGENT_INSPECTOR_CAPTURE_MODE=full`; the CLI flag takes precedence over the
102
+ environment.
103
+
74
104
  ## Jenkins Release Webhook
75
105
 
76
106
  The `agent-inspector-release` Jenkins pipeline can be triggered from GitCode by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/agent-inspector",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
4
4
  "type": "module",
5
5
  "description": "Agent observability and knowledge capture layer for AI coding tools.",
6
6
  "license": "MIT",
package/src/cli.ts CHANGED
@@ -11,6 +11,9 @@ const __dirname = dirname(__filename);
11
11
  const DEFAULT_PORT = 25947;
12
12
  const LOCAL_PROBE_TIMEOUT_MS = 2000;
13
13
  const BRANDED_WINDOWS_RUNTIME_EXE = "agent-inspector.exe";
14
+ const DEFAULT_CAPTURE_MODE = "simple";
15
+
16
+ type CaptureMode = "simple" | "full";
14
17
 
15
18
  process.title = "Agent Inspector";
16
19
 
@@ -56,6 +59,24 @@ async function isInspectorHealthy(port: number): Promise<boolean> {
56
59
  }
57
60
  }
58
61
 
62
+ async function getRunningCaptureMode(port: number): Promise<CaptureMode | null> {
63
+ const controller = new AbortController();
64
+ const timeout = setTimeout(() => controller.abort(), LOCAL_PROBE_TIMEOUT_MS);
65
+ try {
66
+ const response = await fetch(`http://127.0.0.1:${port}/api/config`, {
67
+ cache: "no-store",
68
+ signal: controller.signal,
69
+ });
70
+ if (!response.ok) return null;
71
+ const raw: unknown = await response.json();
72
+ return readCaptureMode(raw);
73
+ } catch {
74
+ return null;
75
+ } finally {
76
+ clearTimeout(timeout);
77
+ }
78
+ }
79
+
59
80
  function isPortAcceptingConnections(port: number): Promise<boolean> {
60
81
  return new Promise((resolve) => {
61
82
  const socket = createConnection({ host: "127.0.0.1", port });
@@ -138,9 +159,31 @@ function resolveServerCommand(outputDir: string, serverPath: string): ServerComm
138
159
  : { command: process.execPath, args: [serverPath] };
139
160
  }
140
161
 
162
+ function parseCaptureMode(value: string | undefined): CaptureMode | null {
163
+ switch (value) {
164
+ case undefined:
165
+ return null;
166
+ case "simple":
167
+ return "simple";
168
+ case "full":
169
+ return "full";
170
+ default:
171
+ return null;
172
+ }
173
+ }
174
+
175
+ function readCaptureMode(raw: unknown): CaptureMode | null {
176
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
177
+ const descriptor = Object.getOwnPropertyDescriptor(raw, "captureMode");
178
+ const value: unknown = descriptor?.value;
179
+ return typeof value === "string" ? parseCaptureMode(value) : null;
180
+ }
181
+
141
182
  async function runStart(args: string[]): Promise<void> {
142
183
  const envPort = process.env["PORT"];
143
184
  const portDefault = envPort !== undefined ? Number(envPort) : DEFAULT_PORT;
185
+ const envMode =
186
+ process.env["AGENT_INSPECTOR_CAPTURE_MODE"] ?? process.env["AGENT_INSPECTOR_MODE"];
144
187
 
145
188
  let port = portDefault;
146
189
  let open = true;
@@ -149,9 +192,34 @@ async function runStart(args: string[]): Promise<void> {
149
192
  let forceRestart = false;
150
193
  let configDir: string | undefined;
151
194
  let providersJson: string | undefined;
195
+ let captureMode = DEFAULT_CAPTURE_MODE;
196
+ let captureModeWasSpecified = false;
197
+
198
+ if (envMode !== undefined && envMode !== "") {
199
+ const parsedMode = parseCaptureMode(envMode);
200
+ if (parsedMode === null) {
201
+ console.error(`Invalid capture mode: ${envMode}. Use simple or full.`);
202
+ process.exitCode = 1;
203
+ return;
204
+ }
205
+ captureMode = parsedMode;
206
+ captureModeWasSpecified = true;
207
+ }
152
208
 
153
209
  for (let i = 0; i < args.length; i++) {
154
210
  const arg = args[i] ?? "";
211
+ if (arg.startsWith("--mode=") || arg.startsWith("--capture-mode=")) {
212
+ const value = arg.slice(arg.indexOf("=") + 1);
213
+ const parsedMode = parseCaptureMode(value);
214
+ if (parsedMode === null) {
215
+ console.error(`Invalid capture mode: ${value}. Use simple or full.`);
216
+ process.exitCode = 1;
217
+ return;
218
+ }
219
+ captureMode = parsedMode;
220
+ captureModeWasSpecified = true;
221
+ continue;
222
+ }
155
223
  switch (arg) {
156
224
  case "--port":
157
225
  case "-p":
@@ -181,6 +249,20 @@ async function runStart(args: string[]): Promise<void> {
181
249
  providersJson = args[i + 1];
182
250
  i++;
183
251
  break;
252
+ case "--mode":
253
+ case "--capture-mode": {
254
+ const value = args[i + 1];
255
+ const parsedMode = parseCaptureMode(value);
256
+ if (parsedMode === null) {
257
+ console.error(`Invalid capture mode: ${String(value)}. Use simple or full.`);
258
+ process.exitCode = 1;
259
+ return;
260
+ }
261
+ captureMode = parsedMode;
262
+ captureModeWasSpecified = true;
263
+ i++;
264
+ break;
265
+ }
184
266
  default:
185
267
  break;
186
268
  }
@@ -270,6 +352,13 @@ async function runStart(args: string[]): Promise<void> {
270
352
 
271
353
  if (!forceRestart && (await isInspectorHealthy(port))) {
272
354
  console.log(`agent-inspector is already running at ${url}`);
355
+ if (captureModeWasSpecified) {
356
+ const runningMode = await getRunningCaptureMode(port);
357
+ if (runningMode !== null && runningMode !== captureMode) {
358
+ console.log(`Existing instance capture mode is ${runningMode}; requested ${captureMode}.`);
359
+ console.log(`Use --force-restart to restart with ${captureMode} mode.`);
360
+ }
361
+ }
273
362
  console.log(`Use --force-restart to restart the existing instance.`);
274
363
  if (open && openWasSpecified) {
275
364
  openBrowser(url);
@@ -289,6 +378,7 @@ async function runStart(args: string[]): Promise<void> {
289
378
  }
290
379
 
291
380
  console.log(`Server running at ${url}`);
381
+ console.log(` Capture mode: ${captureMode}`);
292
382
  console.log(` Proxy: ${url}/proxy`);
293
383
  console.log(``);
294
384
  console.log(`Route AI coding tools through the proxy:`);
@@ -339,6 +429,7 @@ async function runStart(args: string[]): Promise<void> {
339
429
  if (providersJson !== undefined) {
340
430
  serverEnv["AGENT_INSPECTOR_PROVIDERS_JSON"] = providersJson;
341
431
  }
432
+ serverEnv["AGENT_INSPECTOR_CAPTURE_MODE"] = captureMode;
342
433
  const serverProcess = spawn(serverCommand.command, serverCommand.args, {
343
434
  stdio: background ? ["ignore", "ignore", "ignore"] : "inherit",
344
435
  detached: background,
@@ -3,7 +3,7 @@ import { ArrowDownRight, ArrowLeft, ArrowUpRight, Check, Copy, Download, Plus }
3
3
 
4
4
  import type { CapturedLog } from "../proxy/schemas";
5
5
  import { exportLogsAsZip } from "../lib/export-logs";
6
- import type { TimeDisplayFormat } from "../lib/runtimeConfig";
6
+ import type { CaptureMode, TimeDisplayFormat } from "../lib/runtimeConfig";
7
7
  import { formatTimestampRange } from "../lib/timeDisplay";
8
8
  import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
9
9
  import { useProviders } from "../lib/useProviders";
@@ -599,6 +599,7 @@ export type ProxyViewerProps = {
599
599
  onClearGroup: (ids: number[]) => void;
600
600
  viewMode: "simple" | "full";
601
601
  onViewModeChange: (mode: "simple" | "full") => void;
602
+ captureMode: CaptureMode;
602
603
  /** Live strip-Claude-Code-billing-header flag, sourced once at the container. */
603
604
  strip: boolean;
604
605
  /** Slow-response threshold in seconds. `0` disables the warning indicator. */
@@ -625,6 +626,7 @@ export function ProxyViewer({
625
626
  onClearGroup,
626
627
  viewMode,
627
628
  onViewModeChange,
629
+ captureMode,
628
630
  strip,
629
631
  slowResponseThresholdSeconds,
630
632
  timeDisplayFormat,
@@ -821,30 +823,36 @@ export function ProxyViewer({
821
823
  ))}
822
824
  </SelectContent>
823
825
  </Select>
824
- <div className="flex items-center border border-border rounded-md overflow-hidden">
825
- <button
826
- type="button"
827
- onClick={() => onViewModeChange("simple")}
828
- className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
829
- viewMode === "simple"
830
- ? "bg-muted text-foreground"
831
- : "text-muted-foreground hover:bg-muted/50"
832
- }`}
833
- >
826
+ {captureMode === "full" ? (
827
+ <div className="flex items-center border border-border rounded-md overflow-hidden">
828
+ <button
829
+ type="button"
830
+ onClick={() => onViewModeChange("simple")}
831
+ className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
832
+ viewMode === "simple"
833
+ ? "bg-muted text-foreground"
834
+ : "text-muted-foreground hover:bg-muted/50"
835
+ }`}
836
+ >
837
+ Simple
838
+ </button>
839
+ <button
840
+ type="button"
841
+ onClick={() => onViewModeChange("full")}
842
+ className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
843
+ viewMode === "full"
844
+ ? "bg-muted text-foreground"
845
+ : "text-muted-foreground hover:bg-muted/50"
846
+ }`}
847
+ >
848
+ Full
849
+ </button>
850
+ </div>
851
+ ) : (
852
+ <div className="flex h-8 items-center rounded-md border border-border bg-muted px-3 text-xs text-foreground">
834
853
  Simple
835
- </button>
836
- <button
837
- type="button"
838
- onClick={() => onViewModeChange("full")}
839
- className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
840
- viewMode === "full"
841
- ? "bg-muted text-foreground"
842
- : "text-muted-foreground hover:bg-muted/50"
843
- }`}
844
- >
845
- Full
846
- </button>
847
- </div>
854
+ </div>
855
+ )}
848
856
  <div className="flex-1" />
849
857
  {!hasSessionContext && (
850
858
  <span className="text-muted-foreground text-xs font-mono">
@@ -330,7 +330,7 @@ export function ProxyViewerContainer({
330
330
 
331
331
  // Read the strip config once at the container so the virtualized list does
332
332
  // not need N independent SWR subscriptions per row.
333
- const { strip, slowResponseThresholdSeconds, timeDisplayFormat } = useStripConfig();
333
+ const { strip, captureMode, slowResponseThresholdSeconds, timeDisplayFormat } = useStripConfig();
334
334
 
335
335
  return (
336
336
  <>
@@ -352,6 +352,7 @@ export function ProxyViewerContainer({
352
352
  onClearGroup={handleClearGroup}
353
353
  viewMode={viewMode}
354
354
  onViewModeChange={setViewMode}
355
+ captureMode={captureMode}
355
356
  strip={strip}
356
357
  slowResponseThresholdSeconds={slowResponseThresholdSeconds}
357
358
  timeDisplayFormat={timeDisplayFormat}
@@ -196,6 +196,7 @@ function OnboardingSettingsTab(): JSX.Element {
196
196
  function ProxySettingsTab(): JSX.Element {
197
197
  const {
198
198
  strip,
199
+ captureMode,
199
200
  slowResponseThresholdSeconds,
200
201
  timeDisplayFormat,
201
202
  isLoading,
@@ -205,6 +206,7 @@ function ProxySettingsTab(): JSX.Element {
205
206
  } = useStripConfig();
206
207
  const [error, setError] = useState<string | null>(null);
207
208
  const [pending, setPending] = useState(false);
209
+ const alternateCaptureMode = captureMode === "full" ? "simple" : "full";
208
210
 
209
211
  const handleToggle = useCallback(
210
212
  async (next: boolean) => {
@@ -253,6 +255,14 @@ function ProxySettingsTab(): JSX.Element {
253
255
 
254
256
  return (
255
257
  <div className="space-y-4">
258
+ <div className="space-y-1">
259
+ <h3 className="text-sm font-semibold">Capture mode</h3>
260
+ <p className="text-xs text-muted-foreground">
261
+ This process is running in <code>{captureMode}</code> mode. Restart with{" "}
262
+ <code>agent-inspector --mode {alternateCaptureMode}</code> to change it.
263
+ </p>
264
+ </div>
265
+
256
266
  <div className="space-y-1">
257
267
  <h3 className="text-sm font-semibold">Claude Code billing header</h3>
258
268
  <p className="text-xs text-muted-foreground">
@@ -3,10 +3,13 @@ import { z } from "zod";
3
3
  export const DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS = 10;
4
4
  export const MAX_SLOW_RESPONSE_THRESHOLD_SECONDS = 600;
5
5
  export const DEFAULT_TIME_DISPLAY_FORMAT = "time";
6
+ export const DEFAULT_CAPTURE_MODE = "simple";
6
7
 
7
8
  export const TimeDisplayFormatSchema = z.enum(["time", "full"]);
9
+ export const CaptureModeSchema = z.enum(["simple", "full"]);
8
10
 
9
11
  export type TimeDisplayFormat = z.infer<typeof TimeDisplayFormatSchema>;
12
+ export type CaptureMode = z.infer<typeof CaptureModeSchema>;
10
13
 
11
14
  /**
12
15
  * Schema for the runtime proxy config. Shared between server
@@ -26,6 +29,7 @@ export const RuntimeConfigSchema = z.object({
26
29
  .max(MAX_SLOW_RESPONSE_THRESHOLD_SECONDS)
27
30
  .default(DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS),
28
31
  timeDisplayFormat: TimeDisplayFormatSchema.default(DEFAULT_TIME_DISPLAY_FORMAT),
32
+ captureMode: CaptureModeSchema.default(DEFAULT_CAPTURE_MODE),
29
33
  });
30
34
 
31
35
  export type RuntimeConfig = z.infer<typeof RuntimeConfigSchema>;
@@ -1,8 +1,10 @@
1
1
  import useSWR, { type SWRResponse, useSWRConfig } from "swr";
2
2
  import { fetchJson } from "./apiClient";
3
3
  import {
4
+ DEFAULT_CAPTURE_MODE,
4
5
  DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
5
6
  DEFAULT_TIME_DISPLAY_FORMAT,
7
+ type CaptureMode,
6
8
  RuntimeConfigSchema,
7
9
  type TimeDisplayFormat,
8
10
  type RuntimeConfig,
@@ -22,7 +24,9 @@ async function fetcher(url: string): Promise<RuntimeConfig> {
22
24
  /**
23
25
  * PATCH the runtime config. Exported separately for testability.
24
26
  */
25
- export async function setRuntimeConfig(patch: Partial<RuntimeConfig>): Promise<RuntimeConfig> {
27
+ type RuntimeConfigPatch = Partial<Omit<RuntimeConfig, "captureMode">>;
28
+
29
+ export async function setRuntimeConfig(patch: RuntimeConfigPatch): Promise<RuntimeConfig> {
26
30
  return fetchJson(
27
31
  STRIP_CONFIG_SWR_KEY,
28
32
  RuntimeConfigSchema,
@@ -37,6 +41,7 @@ export async function setRuntimeConfig(patch: Partial<RuntimeConfig>): Promise<R
37
41
 
38
42
  export type UseStripConfig = {
39
43
  strip: boolean;
44
+ captureMode: CaptureMode;
40
45
  slowResponseThresholdSeconds: number;
41
46
  timeDisplayFormat: TimeDisplayFormat;
42
47
  isLoading: boolean;
@@ -66,16 +71,18 @@ export function useStripConfig(): UseStripConfig {
66
71
  const { mutate: globalMutate } = useSWRConfig();
67
72
 
68
73
  const strip = response.data?.stripClaudeCodeBillingHeader ?? false;
74
+ const captureMode = response.data?.captureMode ?? DEFAULT_CAPTURE_MODE;
69
75
  const slowResponseThresholdSeconds =
70
76
  response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS;
71
77
  const timeDisplayFormat = response.data?.timeDisplayFormat ?? DEFAULT_TIME_DISPLAY_FORMAT;
72
78
 
73
- const optimisticConfig = (patch: Partial<RuntimeConfig>): RuntimeConfig => ({
79
+ const optimisticConfig = (patch: RuntimeConfigPatch): RuntimeConfig => ({
74
80
  stripClaudeCodeBillingHeader: response.data?.stripClaudeCodeBillingHeader ?? false,
75
81
  hasSeenOnboarding: response.data?.hasSeenOnboarding ?? false,
76
82
  slowResponseThresholdSeconds:
77
83
  response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
78
84
  timeDisplayFormat: response.data?.timeDisplayFormat ?? DEFAULT_TIME_DISPLAY_FORMAT,
85
+ captureMode,
79
86
  ...patch,
80
87
  });
81
88
 
@@ -113,6 +120,7 @@ export function useStripConfig(): UseStripConfig {
113
120
 
114
121
  return {
115
122
  strip,
123
+ captureMode,
116
124
  slowResponseThresholdSeconds,
117
125
  timeDisplayFormat,
118
126
  isLoading: response.isLoading,
@@ -11,9 +11,12 @@ import { join } from "node:path";
11
11
  import { logger } from "./logger";
12
12
  import { getDataDir } from "./dataDir";
13
13
  import {
14
+ CaptureModeSchema,
15
+ DEFAULT_CAPTURE_MODE,
14
16
  DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
15
17
  DEFAULT_TIME_DISPLAY_FORMAT,
16
18
  RuntimeConfigSchema,
19
+ type CaptureMode,
17
20
  type RuntimeConfig,
18
21
  } from "../lib/runtimeConfig";
19
22
 
@@ -52,7 +55,22 @@ function getConfigTempPath(): string {
52
55
  // Internal: writeable singleton. Read via getConfig(), mutate via setConfig().
53
56
  let currentConfig: RuntimeConfig = resolveInitialConfig();
54
57
 
58
+ type MutableRuntimeConfig = Omit<RuntimeConfig, "captureMode">;
59
+
60
+ function resolveInitialCaptureMode(): CaptureMode {
61
+ const value = process.env["AGENT_INSPECTOR_CAPTURE_MODE"] ?? process.env["AGENT_INSPECTOR_MODE"];
62
+ if (value === undefined || value === "") return DEFAULT_CAPTURE_MODE;
63
+
64
+ const result = CaptureModeSchema.safeParse(value);
65
+ if (result.success) return result.data;
66
+
67
+ logger.warn(`[config] Invalid capture mode ${value}; falling back to ${DEFAULT_CAPTURE_MODE}`);
68
+ return DEFAULT_CAPTURE_MODE;
69
+ }
70
+
55
71
  function resolveInitialConfig(): RuntimeConfig {
72
+ const captureMode = resolveInitialCaptureMode();
73
+
56
74
  // 1. Persisted file
57
75
  const filePath = getConfigFilePath();
58
76
  if (existsSync(filePath)) {
@@ -62,7 +80,7 @@ function resolveInitialConfig(): RuntimeConfig {
62
80
  const result = RuntimeConfigSchema.safeParse(parsed);
63
81
  if (result.success) {
64
82
  logger.info(`[config] Loaded persisted config from ${filePath}`);
65
- return result.data;
83
+ return { ...result.data, captureMode };
66
84
  }
67
85
  logger.warn(
68
86
  `[config] Persisted config at ${filePath} is invalid; falling back to env/default`,
@@ -81,6 +99,7 @@ function resolveInitialConfig(): RuntimeConfig {
81
99
  hasSeenOnboarding: false,
82
100
  slowResponseThresholdSeconds: DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
83
101
  timeDisplayFormat: DEFAULT_TIME_DISPLAY_FORMAT,
102
+ captureMode,
84
103
  };
85
104
  }
86
105
 
@@ -90,6 +109,7 @@ function resolveInitialConfig(): RuntimeConfig {
90
109
  hasSeenOnboarding: false,
91
110
  slowResponseThresholdSeconds: DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
92
111
  timeDisplayFormat: DEFAULT_TIME_DISPLAY_FORMAT,
112
+ captureMode,
93
113
  };
94
114
  }
95
115
 
@@ -106,10 +126,11 @@ export function getConfig(): RuntimeConfig {
106
126
  * do not throw — the in-memory value is always updated so the caller
107
127
  * sees a consistent response.
108
128
  */
109
- export function setConfig(patch: Partial<RuntimeConfig>): RuntimeConfig {
129
+ export function setConfig(patch: Partial<MutableRuntimeConfig>): RuntimeConfig {
110
130
  const next: RuntimeConfig = {
111
131
  ...currentConfig,
112
132
  ...patch,
133
+ captureMode: currentConfig.captureMode,
113
134
  };
114
135
  currentConfig = next;
115
136
  persistConfig(next);
@@ -152,7 +173,7 @@ function persistConfig(value: RuntimeConfig): boolean {
152
173
  }
153
174
 
154
175
  try {
155
- writeFileSync(tempPath, JSON.stringify(value), "utf-8");
176
+ writeFileSync(tempPath, JSON.stringify(toPersistedConfig(value)), "utf-8");
156
177
  } catch (err) {
157
178
  logger.error(`[config] Failed to write config temp file: ${String(err)}`);
158
179
  return false;
@@ -173,3 +194,12 @@ function persistConfig(value: RuntimeConfig): boolean {
173
194
  }
174
195
  }
175
196
  }
197
+
198
+ function toPersistedConfig(value: RuntimeConfig): Record<string, unknown> {
199
+ return {
200
+ stripClaudeCodeBillingHeader: value.stripClaudeCodeBillingHeader,
201
+ hasSeenOnboarding: value.hasSeenOnboarding,
202
+ slowResponseThresholdSeconds: value.slowResponseThresholdSeconds,
203
+ timeDisplayFormat: value.timeDisplayFormat,
204
+ };
205
+ }
@@ -36,16 +36,21 @@ import {
36
36
  * Strips all custom/non-standard headers from the request and replaces with
37
37
  * unified proxy identity. Only preserves standard HTTP headers needed for API calls.
38
38
  */
39
- function buildProxyHeaders(originalHeaders: Headers): {
39
+ function buildProxyHeaders(
40
+ originalHeaders: Headers,
41
+ captureRawHeaders: boolean,
42
+ ): {
40
43
  headers: Headers;
41
- rawHeaders: Record<string, string>;
44
+ rawHeaders: Record<string, string> | undefined;
42
45
  } {
43
- const rawHeaders: Record<string, string> = {};
46
+ const rawHeaders: Record<string, string> | undefined = captureRawHeaders ? {} : undefined;
44
47
  const headers = new Headers();
45
48
 
46
- originalHeaders.forEach((value, key) => {
47
- rawHeaders[key.toLowerCase()] = value;
48
- });
49
+ if (rawHeaders !== undefined) {
50
+ originalHeaders.forEach((value, key) => {
51
+ rawHeaders[key.toLowerCase()] = value;
52
+ });
53
+ }
49
54
 
50
55
  headers.set(HEADER_USER_AGENT, PROXY_IDENTITY);
51
56
  headers.set(HEADER_X_PROXY_IDENTITY, PROXY_IDENTITY);
@@ -128,6 +133,7 @@ function handleStreamingResponse(
128
133
  startTime: number,
129
134
  upstreamUrl: string,
130
135
  log: CapturedLog,
136
+ collectStreamingChunks: boolean,
131
137
  ): Response {
132
138
  log.streaming = true;
133
139
  log.responseStatus = upstreamRes.status;
@@ -151,6 +157,7 @@ function handleStreamingResponse(
151
157
  elapsedMs,
152
158
  responseStatus: upstreamRes.status,
153
159
  rawStream: full,
160
+ collectStreamingChunks,
154
161
  });
155
162
  },
156
163
  });
@@ -175,6 +182,7 @@ function handleStreamingResponse(
175
182
  elapsedMs,
176
183
  rawStream: full,
177
184
  hasChunks,
185
+ collectStreamingChunks,
178
186
  });
179
187
  }
180
188
  });
@@ -208,8 +216,9 @@ export async function handleProxy(req: Request): Promise<Response> {
208
216
  // Important: the original `requestBody` must be preserved so the log stores
209
217
  // the unmodified request (the "Raw Request" tab needs the true bytes the
210
218
  // client sent). The rewritten body is only used for the upstream fetch.
219
+ const config = getConfig();
211
220
  let bodyToForward = requestBody;
212
- if (getConfig().stripClaudeCodeBillingHeader && requestBody !== null && parsed.isMessages) {
221
+ if (config.stripClaudeCodeBillingHeader && requestBody !== null && parsed.isMessages) {
213
222
  const stripped = stripClaudeCodeBillingHeader(requestBody);
214
223
  if (stripped.removed > 0) {
215
224
  logger.info(
@@ -248,7 +257,11 @@ export async function handleProxy(req: Request): Promise<Response> {
248
257
  const upstreamUrl = buildUpstreamUrl(upstreamBase, parsed.normalizedPath);
249
258
  const startTime = Date.now();
250
259
 
251
- const { headers: upstreamHeaders, rawHeaders } = buildProxyHeaders(req.headers);
260
+ const captureFullDetails = config.captureMode === "full";
261
+ const { headers: upstreamHeaders, rawHeaders } = buildProxyHeaders(
262
+ req.headers,
263
+ captureFullDetails,
264
+ );
252
265
  setUpstreamHost(upstreamHeaders, upstreamBase);
253
266
  injectProviderAuth(upstreamHeaders, matchedProviderConfig);
254
267
 
@@ -277,10 +290,14 @@ export async function handleProxy(req: Request): Promise<Response> {
277
290
  // full PowerShell round-trip on top of everything else before
278
291
  // `fetch` is even issued.
279
292
  const [clientInfo, preAcquiredId] = await Promise.all([getClientInfo(req), getNextLogId()]);
280
- const upstreamHeadersObj: Record<string, string> = {};
281
- upstreamHeaders.forEach((value, key) => {
282
- upstreamHeadersObj[key.toLowerCase()] = value;
283
- });
293
+ const upstreamHeadersObj: Record<string, string> | undefined = captureFullDetails
294
+ ? {}
295
+ : undefined;
296
+ if (upstreamHeadersObj !== undefined) {
297
+ upstreamHeaders.forEach((value, key) => {
298
+ upstreamHeadersObj[key.toLowerCase()] = value;
299
+ });
300
+ }
284
301
  const log = await createLog(
285
302
  req.method,
286
303
  parsed.apiPath,
@@ -330,5 +347,5 @@ export async function handleProxy(req: Request): Promise<Response> {
330
347
  return handleNonStreamingResponse(upstreamRes, responseBody, startTime, upstreamUrl, log);
331
348
  }
332
349
 
333
- return handleStreamingResponse(upstreamRes, req, startTime, upstreamUrl, log);
350
+ return handleStreamingResponse(upstreamRes, req, startTime, upstreamUrl, log, captureFullDetails);
334
351
  }
@@ -22,12 +22,14 @@ export type FinalizeStreamingLogJob = BaseFinalizeLogJob & {
22
22
  type: "streaming";
23
23
  responseStatus: number;
24
24
  rawStream: string;
25
+ collectStreamingChunks: boolean;
25
26
  };
26
27
 
27
28
  export type FinalizeStreamAbortLogJob = BaseFinalizeLogJob & {
28
29
  type: "stream-abort";
29
30
  rawStream: string;
30
31
  hasChunks: boolean;
32
+ collectStreamingChunks: boolean;
31
33
  };
32
34
 
33
35
  export type FinalizeLogJob =
@@ -145,7 +147,7 @@ function finalizeStreaming(job: FinalizeStreamingLogJob, log: CapturedLog): Fina
145
147
  job.rawStream,
146
148
  log,
147
149
  log.model ?? undefined,
148
- true,
150
+ job.collectStreamingChunks,
149
151
  );
150
152
  persistStreamingChunks(log);
151
153
 
@@ -168,7 +170,7 @@ function finalizeStreamAbort(job: FinalizeStreamAbortLogJob, log: CapturedLog):
168
170
  job.rawStream,
169
171
  log,
170
172
  log.model ?? undefined,
171
- true,
173
+ job.collectStreamingChunks,
172
174
  );
173
175
  persistStreamingChunks(log);
174
176
  } else {