@tonyclaw/agent-inspector 2.0.13 → 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-CrMyE23D.js → CompareDrawer-B6WCFRJq.js} +1 -1
  4. package/.output/public/assets/{ProxyViewerContainer-BZuo0px3.js → ProxyViewerContainer-CofGRJoO.js} +31 -31
  5. package/.output/public/assets/{ReplayDialog-TQGhDdTa.js → ReplayDialog-O_53zkX8.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-D6kQYtfa.js → RequestAnatomy-CqBSMZv5.js} +1 -1
  7. package/.output/public/assets/{ResponseView-C6ybWuB8.js → ResponseView-BvbA6zot.js} +1 -1
  8. package/.output/public/assets/{StreamingChunkSequence-Bvs59_-I.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-DvFSJlOd.js → main-Biir9ZGe.js} +2 -2
  12. package/.output/server/{_sessionId-DHYJxPb7.mjs → _sessionId-B77Gh4IL.mjs} +2 -2
  13. package/.output/server/_ssr/{CompareDrawer-meimDBeA.mjs → CompareDrawer-D4-RHFqW.mjs} +2 -2
  14. package/.output/server/_ssr/{ProxyViewerContainer-Co-WKq6u.mjs → ProxyViewerContainer-YP28-_2H.mjs} +31 -10
  15. package/.output/server/_ssr/{ReplayDialog-cclcGyR9.mjs → ReplayDialog-D9rQlrcY.mjs} +3 -3
  16. package/.output/server/_ssr/{RequestAnatomy-to_rezzu.mjs → RequestAnatomy-BXCRWD9H.mjs} +2 -2
  17. package/.output/server/_ssr/{ResponseView-CZZI4IZd.mjs → ResponseView-DEwWMqSS.mjs} +2 -2
  18. package/.output/server/_ssr/{StreamingChunkSequence-BTF4xSYX.mjs → StreamingChunkSequence-CdrTPf5v.mjs} +2 -2
  19. package/.output/server/_ssr/{index-D2fpJFP5.mjs → index-Chf1DYqT.mjs} +2 -2
  20. package/.output/server/_ssr/index.mjs +2 -2
  21. package/.output/server/_ssr/{router-RDBAF-Iu.mjs → router-BHXU5jWm.mjs} +89 -46
  22. package/.output/server/{_tanstack-start-manifest_v-DB9sseoH.mjs → _tanstack-start-manifest_v-D1dtacWy.mjs} +1 -1
  23. package/.output/server/index.mjs +59 -59
  24. package/README.md +141 -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-BNG_mnaH.js +0 -1
  37. package/.output/public/assets/index-BUxIwHhC.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,146 @@ 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
+
104
+ ## Jenkins Release Webhook
105
+
106
+ The `agent-inspector-release` Jenkins pipeline can be triggered from GitCode by
107
+ using the Jenkins Git plugin `notifyCommit` endpoint. The Jenkins instance must
108
+ be reachable from GitCode; `localhost`, LAN-only addresses, or NAT public IPs
109
+ without port forwarding will not work. Use a public Jenkins URL, a reverse
110
+ proxy, or a tunnel such as Cloudflare Tunnel, frp, or ngrok.
111
+
112
+ Create a GitCode WebHook under the repository settings with these values:
113
+
114
+ ```text
115
+ URL:
116
+ https://<public-jenkins-host>/git/notifyCommit?url=https://gitcode.com/TonyClaw/agent-inspector&token=<jenkins-git-notify-token>
117
+
118
+ WebHook password/signing secret:
119
+ leave empty
120
+
121
+ POST content type:
122
+ application/json
123
+
124
+ Events:
125
+ Push events only
126
+
127
+ Active:
128
+ enabled
129
+ ```
130
+
131
+ The Jenkins job must have SCM polling enabled so the Git plugin can schedule the
132
+ pipeline after receiving the `notifyCommit` request. Do not commit the real
133
+ Jenkins notify token to the repository.
134
+
135
+ ### Local Push Fallback
136
+
137
+ When GitCode cannot reach a local Jenkins instance from the public internet, the
138
+ tracked Husky `pre-push` hook can notify Jenkins from the developer machine that
139
+ runs `git push`.
140
+
141
+ Configure the local secret values once per clone:
142
+
143
+ ```bash
144
+ git config --local agentInspector.jenkinsUrl http://localhost:8080
145
+ git config --local agentInspector.repositoryUrl https://gitcode.com/TonyClaw/agent-inspector
146
+ git config --local agentInspector.jenkinsNotifyToken <jenkins-git-notify-token>
147
+ ```
148
+
149
+ On Windows PowerShell:
150
+
151
+ ```powershell
152
+ git config --local agentInspector.jenkinsUrl http://localhost:8080
153
+ git config --local agentInspector.repositoryUrl https://gitcode.com/TonyClaw/agent-inspector
154
+ git config --local agentInspector.jenkinsNotifyToken <jenkins-git-notify-token>
155
+ ```
156
+
157
+ After a `main` branch push, the hook waits briefly and then calls Jenkins
158
+ `/git/notifyCommit` from the local machine. The hook can also be configured with
159
+ environment variables instead of local Git config:
160
+
161
+ ```bash
162
+ AGENT_INSPECTOR_JENKINS_URL=http://localhost:8080
163
+ AGENT_INSPECTOR_REPOSITORY_URL=https://gitcode.com/TonyClaw/agent-inspector
164
+ AGENT_INSPECTOR_JENKINS_NOTIFY_TOKEN=<jenkins-git-notify-token>
165
+ AGENT_INSPECTOR_JENKINS_PUSH_DELAY_SECONDS=10
166
+ ```
167
+
168
+ The hook writes local diagnostics to `.git/hooks/jenkins-pre-push.log`. The
169
+ tracked template contains no secret values; every developer must configure their
170
+ own token locally.
171
+
172
+ For repositories that do not use Husky, install the same template as a native Git
173
+ hook:
174
+
175
+ ```bash
176
+ cp scripts/hooks/pre-push-jenkins-notify .git/hooks/pre-push
177
+ chmod +x .git/hooks/pre-push
178
+ ```
179
+
180
+ ### Verified PR Workflow
181
+
182
+ For local Jenkins instances that GitCode cannot reach, use the verified PR script
183
+ instead of relying on GitCode WebHook delivery. The entire flow is outbound from
184
+ the developer machine:
185
+
186
+ 1. Push the current commit to a temporary GitCode branch.
187
+ 2. Trigger the local Jenkins job with `GIT_REF=<temporary-branch>`.
188
+ 3. Wait for the Jenkins result.
189
+ 4. Create a GitCode Pull Request to `main` only when Jenkins returns `SUCCESS`.
190
+
191
+ Configure local secrets once per clone:
192
+
193
+ ```powershell
194
+ git config --local agentInspector.jenkinsUrl http://localhost:8080
195
+ git config --local agentInspector.jenkinsJob agent-inspector-release
196
+ git config --local agentInspector.jenkinsUser <jenkins-user>
197
+ git config --local agentInspector.jenkinsApiToken <jenkins-api-token>
198
+ git config --local agentInspector.gitcodeAccessToken <gitcode-access-token>
199
+ git config --local agentInspector.gitcodeOwner TonyClaw
200
+ git config --local agentInspector.gitcodeRepo agent-inspector
201
+ ```
202
+
203
+ Then run:
204
+
205
+ ```powershell
206
+ .\scripts\jenkins\verified-pr.ps1
207
+ ```
208
+
209
+ The script pushes a branch named like `jenkins-verify/<user>/<sha>`, waits for
210
+ Jenkins, and creates a PR through the GitCode API after a successful build. The
211
+ temporary branch is kept as the PR source branch and is configured for deletion
212
+ after merge when GitCode supports source-branch pruning.
213
+
74
214
  ## Project Direction
75
215
 
76
216
  Agent Inspector should stay close to the proven inspector codebase while growing into an agent knowledge layer. The memory flow has four layers:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/agent-inspector",
3
- "version": "2.0.13",
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
+ }