@tonyclaw/agent-inspector 2.0.0 → 2.0.2

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 (44) hide show
  1. package/.output/cli.js +353 -54
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-CU5ZrWcL.js → CompareDrawer-Bp7_x-5N.js} +1 -1
  4. package/.output/public/assets/{ProxyViewerContainer-pEBqVp1d.js → ProxyViewerContainer-USuxPy-K.js} +13 -13
  5. package/.output/public/assets/{ReplayDialog-F58yNg5j.js → ReplayDialog-DFHCd0yx.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-C9lT0qE_.js → RequestAnatomy-ehyrskxt.js} +1 -1
  7. package/.output/public/assets/{ResponseView-DHJq6bnz.js → ResponseView-BNGyc8e_.js} +1 -1
  8. package/.output/public/assets/{StreamingChunkSequence-BTgfpFUT.js → StreamingChunkSequence-Bjs4Lqwn.js} +1 -1
  9. package/.output/public/assets/_sessionId-D_SeK_qp.js +1 -0
  10. package/.output/public/assets/index-BGGOWR7A.js +1 -0
  11. package/.output/public/assets/index-CIL46Z2y.css +1 -0
  12. package/.output/public/assets/{json-viewer-CZVYLR8j.js → json-viewer-6uV_YXws.js} +1 -1
  13. package/.output/public/assets/{main-DHs7FBK3.js → main-FSGUGtEL.js} +2 -2
  14. package/.output/server/{_sessionId-wMLPvC5g.mjs → _sessionId-_bf9vUww.mjs} +2 -2
  15. package/.output/server/_ssr/{CompareDrawer-BU4V0uVf.mjs → CompareDrawer-DIth2DQM.mjs} +3 -3
  16. package/.output/server/_ssr/{ProxyViewerContainer-BnRwFEnn.mjs → ProxyViewerContainer-249bTH-T.mjs} +24 -30
  17. package/.output/server/_ssr/{ReplayDialog-C7dn9pd_.mjs → ReplayDialog-C1aGx0y1.mjs} +4 -4
  18. package/.output/server/_ssr/{RequestAnatomy-C1rWpe9-.mjs → RequestAnatomy-D2bCiEJn.mjs} +2 -2
  19. package/.output/server/_ssr/{ResponseView-hGpPaYsf.mjs → ResponseView-DP6k4Xs_.mjs} +3 -3
  20. package/.output/server/_ssr/{StreamingChunkSequence-BRWI1r_G.mjs → StreamingChunkSequence-HyXZV-b5.mjs} +3 -3
  21. package/.output/server/_ssr/{index-BKURLVPz.mjs → index-Bt47f9pn.mjs} +2 -2
  22. package/.output/server/_ssr/index.mjs +2 -2
  23. package/.output/server/_ssr/{json-viewer-BBd2DtQP.mjs → json-viewer-Co-YRwUP.mjs} +2 -2
  24. package/.output/server/_ssr/{router-BcZ0D6AB.mjs → router-to_OJirX.mjs} +383 -43
  25. package/.output/server/{_tanstack-start-manifest_v-1y8ZVxRI.mjs → _tanstack-start-manifest_v-Bd-2YRWo.mjs} +1 -1
  26. package/.output/server/index.mjs +64 -64
  27. package/README.md +57 -4
  28. package/package.json +9 -2
  29. package/scripts/setup-codex-skill.mjs +38 -0
  30. package/scripts/setup-windows-runtime.mjs +74 -0
  31. package/src/assets/agent-inspector.ico +0 -0
  32. package/src/cli/onboard.ts +175 -68
  33. package/src/cli/templates/codex-skill-onboard.ts +210 -0
  34. package/src/cli.ts +18 -2
  35. package/src/components/providers/ProviderCard.tsx +2 -27
  36. package/src/components/providers/ProvidersPanel.tsx +16 -0
  37. package/src/knowledge/openclawClient.ts +34 -5
  38. package/src/knowledge/openclawGatewayClient.ts +237 -0
  39. package/src/knowledge/openclawMarkdown.ts +146 -0
  40. package/src/lib/providerTestPrompt.ts +78 -0
  41. package/src/routes/api/providers.$providerId.test.log.ts +9 -22
  42. package/.output/public/assets/_sessionId-DsNRbnNm.js +0 -1
  43. package/.output/public/assets/index-CpWG2hFn.css +0 -1
  44. package/.output/public/assets/index-DmBV8Gve.js +0 -1
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * `agent-inspector onboard` subcommand.
3
3
  *
4
- * Generates a Claude Code skill + slash command into the user's home dir so
5
- * the user can run `/agent-inspector:onboard` to walk through a guided setup.
4
+ * Generates Agent Inspector onboarding skills into the user's home dir.
5
+ * Claude Code gets a skill + slash command. Codex gets a skill under
6
+ * ~/.codex/skills that guides the user through connecting /api/mcp.
6
7
  *
7
- * Idempotent by default: re-runs without `--force` are no-ops. `--force`
8
- * overwrites, `--dry-run` writes nothing. Designed as an explicit setup step
9
- * that can fail softly without blocking normal CLI use.
8
+ * Idempotent by default: re-runs without `--force` only write missing files.
9
+ * `--force` overwrites, `--dry-run` writes nothing. Designed as an explicit
10
+ * setup step that can fail softly without blocking normal CLI use.
10
11
  */
11
12
 
12
13
  import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
@@ -16,6 +17,10 @@ import { fileURLToPath } from "node:url";
16
17
 
17
18
  import { detectAll, detectFirst } from "./detect-tools.js";
18
19
  import { renderCommandOnboard } from "./templates/command-onboard.js";
20
+ import {
21
+ renderCodexSkillOnboard,
22
+ REQUIRED_CODEX_PHASE_HEADINGS,
23
+ } from "./templates/codex-skill-onboard.js";
19
24
  import { renderSkillOnboard, REQUIRED_PHASE_HEADINGS } from "./templates/skill-onboard.js";
20
25
 
21
26
  const __filename = fileURLToPath(import.meta.url);
@@ -37,7 +42,23 @@ export type OnboardFlags = {
37
42
  dryRun: boolean;
38
43
  skipProvider: boolean;
39
44
  skipToolWire: boolean;
45
+ codexOnly: boolean;
46
+ skipCodexSkill: boolean;
40
47
  skillDir: string | null;
48
+ codexSkillDir: string | null;
49
+ };
50
+
51
+ type OnboardTargets = {
52
+ claudeSkillFile: string;
53
+ claudeCommandFile: string;
54
+ codexSkillFile: string;
55
+ };
56
+
57
+ type PlannedFile = {
58
+ label: string;
59
+ path: string;
60
+ body: string;
61
+ enabled: boolean;
41
62
  };
42
63
 
43
64
  function parseFlags(argv: readonly string[]): OnboardFlags {
@@ -46,7 +67,10 @@ function parseFlags(argv: readonly string[]): OnboardFlags {
46
67
  dryRun: false,
47
68
  skipProvider: false,
48
69
  skipToolWire: false,
70
+ codexOnly: false,
71
+ skipCodexSkill: false,
49
72
  skillDir: null,
73
+ codexSkillDir: null,
50
74
  };
51
75
  for (let i = 0; i < argv.length; i++) {
52
76
  const arg = argv[i];
@@ -65,6 +89,12 @@ function parseFlags(argv: readonly string[]): OnboardFlags {
65
89
  case "--skip-tool-wire":
66
90
  flags.skipToolWire = true;
67
91
  break;
92
+ case "--codex-only":
93
+ flags.codexOnly = true;
94
+ break;
95
+ case "--skip-codex-skill":
96
+ flags.skipCodexSkill = true;
97
+ break;
68
98
  case "--skill-dir": {
69
99
  const next = argv[i + 1];
70
100
  if (next === undefined) {
@@ -75,6 +105,18 @@ function parseFlags(argv: readonly string[]): OnboardFlags {
75
105
  i++;
76
106
  break;
77
107
  }
108
+ case "--codex-skill-dir": {
109
+ const next = argv[i + 1];
110
+ if (next === undefined) {
111
+ process.stderr.write(
112
+ "agent-inspector onboard: --codex-skill-dir requires a path argument\n",
113
+ );
114
+ process.exit(2);
115
+ }
116
+ flags.codexSkillDir = next;
117
+ i++;
118
+ break;
119
+ }
78
120
  case "--help":
79
121
  case "-h":
80
122
  printHelp();
@@ -89,18 +131,21 @@ function parseFlags(argv: readonly string[]): OnboardFlags {
89
131
  }
90
132
 
91
133
  function printHelp(): void {
92
- process.stdout.write(`agent-inspector onboard install the agent-inspector Claude Code skill
134
+ process.stdout.write(`agent-inspector onboard - install Agent Inspector onboarding skills
93
135
 
94
136
  Usage:
95
137
  agent-inspector onboard [options]
96
138
 
97
139
  Options:
98
- --force Overwrite the existing skill and slash command if they exist
99
- --dry-run Print target paths and a template preview, write nothing
100
- --skip-provider Skip the provider-setup phase in the skill body
101
- --skip-tool-wire Skip the wire-tool phase in the skill body
102
- --skill-dir <path> Override the target skill directory (default: ~/.claude/skills)
103
- -h, --help Show this help
140
+ --force Overwrite existing generated skills and slash command
141
+ --dry-run Print target paths and template previews, write nothing
142
+ --skip-provider Skip the provider-setup phase in the Claude skill body
143
+ --skip-tool-wire Skip the wire-tool phase in the Claude skill body
144
+ --skip-codex-skill Install only the Claude Code skill and slash command
145
+ --codex-only Install only the Codex skill
146
+ --skill-dir <path> Override the target Claude root directory (default: ~/.claude)
147
+ --codex-skill-dir <path> Override the target Codex skills directory (default: ~/.codex/skills)
148
+ -h, --help Show this help
104
149
 
105
150
  Exit codes:
106
151
  0 success (or already installed)
@@ -109,16 +154,16 @@ Exit codes:
109
154
  `);
110
155
  }
111
156
 
112
- function resolveTargets(flags: OnboardFlags): {
113
- skillFile: string;
114
- commandFile: string;
115
- } {
157
+ function resolveTargets(flags: OnboardFlags): OnboardTargets {
116
158
  const claudeRoot = flags.skillDir ?? join(homedir(), ".claude");
117
- const skillDir = join(claudeRoot, "skills", SKILL_DIR_NAME);
118
- const commandsDir = join(claudeRoot, "commands");
159
+ const claudeSkillDir = join(claudeRoot, "skills", SKILL_DIR_NAME);
160
+ const claudeCommandsDir = join(claudeRoot, "commands");
161
+ const codexRoot = process.env["CODEX_HOME"] ?? join(homedir(), ".codex");
162
+ const codexSkillsDir = flags.codexSkillDir ?? join(codexRoot, "skills");
119
163
  return {
120
- skillFile: join(skillDir, SKILL_FILE_NAME),
121
- commandFile: join(commandsDir, COMMAND_FILE_NAME),
164
+ claudeSkillFile: join(claudeSkillDir, SKILL_FILE_NAME),
165
+ claudeCommandFile: join(claudeCommandsDir, COMMAND_FILE_NAME),
166
+ codexSkillFile: join(codexSkillsDir, SKILL_DIR_NAME, SKILL_FILE_NAME),
122
167
  };
123
168
  }
124
169
 
@@ -135,6 +180,67 @@ function buildDetectedSummary(): string {
135
180
  return ` - Detected: ${presentList}\n - Not detected: ${absentList}`;
136
181
  }
137
182
 
183
+ function readPackageVersion(): string {
184
+ try {
185
+ const raw: unknown = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
186
+ if (isObject(raw) && typeof raw["version"] === "string") {
187
+ return raw["version"];
188
+ }
189
+ } catch {
190
+ // best-effort: leave version as the fallback
191
+ }
192
+ return "0.0.0";
193
+ }
194
+
195
+ function buildPlannedFiles(
196
+ flags: OnboardFlags,
197
+ targets: OnboardTargets,
198
+ version: string,
199
+ ): PlannedFile[] {
200
+ const installClaude = !flags.codexOnly;
201
+ const installCodex = !flags.skipCodexSkill;
202
+ const detectedSummary = installClaude ? buildDetectedSummary() : "";
203
+ const claudeSkillBody = installClaude
204
+ ? renderSkillOnboard({
205
+ version,
206
+ port: DEFAULT_PORT,
207
+ detectedSummary,
208
+ })
209
+ : "";
210
+ const commandBody = installClaude ? renderCommandOnboard() : "";
211
+ const codexSkillBody = installCodex
212
+ ? renderCodexSkillOnboard({
213
+ version,
214
+ port: DEFAULT_PORT,
215
+ })
216
+ : "";
217
+
218
+ return [
219
+ {
220
+ label: "Claude skill",
221
+ path: targets.claudeSkillFile,
222
+ body: claudeSkillBody,
223
+ enabled: installClaude,
224
+ },
225
+ {
226
+ label: "Claude command",
227
+ path: targets.claudeCommandFile,
228
+ body: commandBody,
229
+ enabled: installClaude,
230
+ },
231
+ {
232
+ label: "Codex skill",
233
+ path: targets.codexSkillFile,
234
+ body: codexSkillBody,
235
+ enabled: installCodex,
236
+ },
237
+ ];
238
+ }
239
+
240
+ function shouldWrite(file: PlannedFile, force: boolean): boolean {
241
+ return file.enabled && (force || !existsSync(file.path));
242
+ }
243
+
138
244
  export function runOnboard(argv: readonly string[]): Promise<number> {
139
245
  try {
140
246
  return Promise.resolve(runOnboardSync(argv));
@@ -149,62 +255,57 @@ export function runOnboard(argv: readonly string[]): Promise<number> {
149
255
 
150
256
  function runOnboardSync(argv: readonly string[]): number {
151
257
  const flags = parseFlags(argv);
152
- const { skillFile, commandFile } = resolveTargets(flags);
153
-
154
- // Idempotency check only relevant for the skill file. If the slash
155
- // command file is missing but the skill is present, that's weird; treat
156
- // it the same as "already installed" so a partial state doesn't get
157
- // half-rewritten on the next run.
158
- if (!flags.force && existsSync(skillFile)) {
159
- process.stdout.write(`agent-inspector onboard: already installed at ${skillFile}\n`);
160
- process.stdout.write("Re-run with --force to refresh.\n");
161
- return 0;
162
- }
258
+ const targets = resolveTargets(flags);
259
+ const version = readPackageVersion();
260
+ const plannedFiles = buildPlannedFiles(flags, targets, version);
261
+ const enabledFiles = plannedFiles.filter((file) => file.enabled);
163
262
 
164
- // Version stamp — read from package.json so the skill and the published
165
- // package can never disagree about which version wrote them.
166
- let version = "0.0.0";
167
- try {
168
- const raw: unknown = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
169
- if (isObject(raw) && typeof raw["version"] === "string") {
170
- version = raw["version"];
171
- }
172
- } catch {
173
- // best-effort: leave version as the fallback
263
+ if (enabledFiles.length === 0) {
264
+ process.stderr.write("agent-inspector onboard: no onboarding target selected\n");
265
+ return 2;
174
266
  }
175
267
 
176
- const detectedSummary = buildDetectedSummary();
177
- const skillBody = renderSkillOnboard({
178
- version,
179
- port: DEFAULT_PORT,
180
- detectedSummary,
181
- });
182
- const commandBody = renderCommandOnboard();
183
-
184
268
  if (flags.dryRun) {
185
269
  process.stdout.write(`agent-inspector onboard --dry-run\n\n`);
186
- process.stdout.write(`Skill target: ${skillFile}\n`);
187
- process.stdout.write(`Command target: ${commandFile}\n\n`);
188
- process.stdout.write(`Skill preview (first 5 lines + headings):\n`);
189
- const previewLines = skillBody.split("\n").slice(0, 5);
190
- process.stdout.write(`${previewLines.join("\n")}\n`);
191
- process.stdout.write(`...\n`);
192
- for (const heading of REQUIRED_PHASE_HEADINGS) {
193
- process.stdout.write(` - ${heading}\n`);
270
+ for (const file of enabledFiles) {
271
+ const status = existsSync(file.path) ? "exists" : "missing";
272
+ process.stdout.write(`${file.label}: ${file.path} (${status})\n`);
273
+ }
274
+ process.stdout.write(`\nClaude skill headings:\n`);
275
+ if (!flags.codexOnly) {
276
+ for (const heading of REQUIRED_PHASE_HEADINGS) {
277
+ process.stdout.write(` - ${heading}\n`);
278
+ }
279
+ } else {
280
+ process.stdout.write(` (disabled by --codex-only)\n`);
281
+ }
282
+ process.stdout.write(`\nCodex skill headings:\n`);
283
+ if (!flags.skipCodexSkill) {
284
+ for (const heading of REQUIRED_CODEX_PHASE_HEADINGS) {
285
+ process.stdout.write(` - ${heading}\n`);
286
+ }
287
+ } else {
288
+ process.stdout.write(` (disabled by --skip-codex-skill)\n`);
289
+ }
290
+ if (!flags.codexOnly) {
291
+ process.stdout.write(`\nDetected tools:\n${buildDetectedSummary()}\n`);
194
292
  }
195
- process.stdout.write(`\nDetected tools:\n${detectedSummary}\n`);
196
293
  process.stdout.write(`\nNo files were written.\n`);
197
294
  return 0;
198
295
  }
199
296
 
200
- // Ensure target dirs exist. mkdirSync with recursive: true is a no-op if
201
- // the dir already exists; failures are caught by the outer try/catch.
202
- mkdirSync(join(skillFile, ".."), { recursive: true });
203
- mkdirSync(join(commandFile, ".."), { recursive: true });
297
+ const filesToWrite = plannedFiles.filter((file) => shouldWrite(file, flags.force));
298
+ if (filesToWrite.length === 0) {
299
+ process.stdout.write("agent-inspector onboard: all selected onboarding files already exist\n");
300
+ process.stdout.write("Re-run with --force to refresh.\n");
301
+ return 0;
302
+ }
204
303
 
205
304
  try {
206
- writeFileSync(skillFile, skillBody, "utf8");
207
- writeFileSync(commandFile, commandBody, "utf8");
305
+ for (const file of filesToWrite) {
306
+ mkdirSync(dirname(file.path), { recursive: true });
307
+ writeFileSync(file.path, file.body, "utf8");
308
+ }
208
309
  } catch (err) {
209
310
  const msg = err instanceof Error ? err.message : String(err);
210
311
  process.stderr.write(`agent-inspector onboard: failed to write files: ${msg}\n`);
@@ -214,11 +315,17 @@ function runOnboardSync(argv: readonly string[]): number {
214
315
  const firstTool = detectFirst();
215
316
  const toolHint = firstTool !== null ? firstTool.displayName : "no known tool detected";
216
317
 
217
- process.stdout.write(`Installed skill to: ${skillFile}\n`);
218
- process.stdout.write(`Installed command to: ${commandFile}\n`);
318
+ for (const file of filesToWrite) {
319
+ process.stdout.write(`Installed ${file.label} to: ${file.path}\n`);
320
+ }
219
321
  process.stdout.write(`\nNext steps:\n`);
220
- process.stdout.write(` - Open Claude Code and run: /agent-inspector:onboard\n`);
221
- process.stdout.write(` - Or refresh later: agent-inspector onboard --force\n`);
322
+ if (!flags.codexOnly) {
323
+ process.stdout.write(` - Open Claude Code and run: /agent-inspector:onboard\n`);
324
+ }
325
+ if (!flags.skipCodexSkill) {
326
+ process.stdout.write(` - Open Codex and ask to use the agent-inspector-onboard skill\n`);
327
+ }
328
+ process.stdout.write(` - Refresh later: agent-inspector onboard --force\n`);
222
329
  process.stdout.write(` - Detected primary tool: ${toolHint}\n`);
223
330
  return 0;
224
331
  }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Renders `~/.codex/skills/agent-inspector-onboard/SKILL.md` for Codex.
3
+ *
4
+ * This is intentionally separate from the Claude Code onboarding skill because
5
+ * Codex uses `~/.codex/config.toml` and supports streamable HTTP MCP servers via
6
+ * `mcp_servers.<id>.url`.
7
+ */
8
+
9
+ export type CodexSkillOnboardContext = {
10
+ /** Package version (from `package.json`), stamped into the frontmatter. */
11
+ readonly version: string;
12
+ /** The default proxy port (mirrors `DEFAULT_PORT` in `src/cli.ts`). */
13
+ readonly port: number;
14
+ };
15
+
16
+ export const REQUIRED_CODEX_PHASE_HEADINGS = [
17
+ "Phase 0: Preflight",
18
+ "Phase 1: Start Agent Inspector",
19
+ "Phase 2: Wire Codex MCP",
20
+ "Phase 3: Restart and verify Codex",
21
+ "Phase 4: First Inspector query",
22
+ "Phase 5: Capture and memory review",
23
+ ] as const;
24
+
25
+ export function renderCodexSkillOnboard(ctx: CodexSkillOnboardContext): string {
26
+ const { version, port } = ctx;
27
+ return `---
28
+ name: agent-inspector-onboard
29
+ description: Guide Codex users through connecting Agent Inspector v${version} as a local MCP server.
30
+ metadata:
31
+ author: agent-inspector
32
+ version: ${version}
33
+ ---
34
+
35
+ # Agent Inspector onboard for Codex
36
+
37
+ Use this skill when the user wants to set up Agent Inspector inside Codex, connect the Agent Inspector MCP server, inspect captured model traffic from Codex, or turn Inspector sessions into reviewable knowledge candidates.
38
+
39
+ Agent Inspector runs a local web UI, proxy, REST API, and MCP endpoint on the same port. The default MCP endpoint is:
40
+
41
+ \`\`\`text
42
+ http://localhost:${port}/api/mcp
43
+ \`\`\`
44
+
45
+ This skill is installed at \`~/.codex/skills/agent-inspector-onboard/SKILL.md\`.
46
+ Codex user-level configuration lives in \`~/.codex/config.toml\`. Codex supports streamable HTTP MCP servers with \`mcp_servers.<id>.url\`, so no extra stdio bridge package is needed.
47
+
48
+ Keep this setup local and explicit. Agent Inspector MCP tools can read captured requests and provider configuration, so only wire it into trusted local Codex environments.
49
+
50
+ ---
51
+
52
+ ## Phase 0: Preflight
53
+
54
+ Check that the package and Codex home exist without printing secrets.
55
+
56
+ \`\`\`powershell
57
+ Get-Command agent-inspector -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source
58
+ Test-Path (Join-Path $env:USERPROFILE ".codex")
59
+ Test-Path (Join-Path $env:USERPROFILE ".codex\\config.toml")
60
+ \`\`\`
61
+
62
+ \`\`\`bash
63
+ command -v agent-inspector || true
64
+ test -d "$HOME/.codex" && echo "codex home: present" || echo "codex home: missing"
65
+ test -f "$HOME/.codex/config.toml" && echo "config: present" || echo "config: missing"
66
+ \`\`\`
67
+
68
+ If \`agent-inspector\` is missing, ask the user to install it first:
69
+
70
+ \`\`\`bash
71
+ npm install -g @tonyclaw/agent-inspector
72
+ \`\`\`
73
+
74
+ Do not read \`~/.codex/auth.json\`, token files, or unrelated Codex state files.
75
+
76
+ ---
77
+
78
+ ## Phase 1: Start Agent Inspector
79
+
80
+ Start or reuse Agent Inspector before wiring MCP. Prefer the installed binary so Windows users get the branded runtime in Task Manager.
81
+
82
+ \`\`\`powershell
83
+ agent-inspector --background --no-open
84
+ Invoke-RestMethod -Uri "http://localhost:${port}/api/health" -TimeoutSec 3
85
+ \`\`\`
86
+
87
+ \`\`\`bash
88
+ agent-inspector --background --no-open
89
+ curl -fsS "http://localhost:${port}/api/health"
90
+ \`\`\`
91
+
92
+ If the health check fails, show only the relevant error and ask whether to diagnose startup before editing Codex config.
93
+
94
+ ---
95
+
96
+ ## Phase 2: Wire Codex MCP
97
+
98
+ Add an \`agent-inspector\` MCP server to \`~/.codex/config.toml\`. Preserve the user's existing config and merge the table instead of replacing the file.
99
+
100
+ Recommended user-level config:
101
+
102
+ \`\`\`toml
103
+ [mcp_servers.agent-inspector]
104
+ url = "http://localhost:${port}/api/mcp"
105
+ startup_timeout_sec = 30
106
+ tool_timeout_sec = 60
107
+ \`\`\`
108
+
109
+ If the user's Codex environment is managed by an admin allowlist, they may also need a matching identity rule in the managed requirements layer:
110
+
111
+ \`\`\`toml
112
+ [mcp_servers.agent-inspector.identity]
113
+ url = "http://localhost:${port}/api/mcp"
114
+ \`\`\`
115
+
116
+ When editing:
117
+
118
+ - Ask before changing \`~/.codex/config.toml\`.
119
+ - Keep existing \`mcp_servers.*\`, \`projects.*\`, profiles, sandbox, approval, and model settings intact.
120
+ - Do not write provider API keys into Codex config.
121
+ - Do not expose Agent Inspector MCP beyond localhost.
122
+
123
+ ---
124
+
125
+ ## Phase 3: Restart and verify Codex
126
+
127
+ Tell the user that Codex loads MCP servers at session startup. After saving \`config.toml\`, they should start a new Codex session or restart the Codex app.
128
+
129
+ Before restart, verify the HTTP endpoint itself:
130
+
131
+ \`\`\`bash
132
+ curl -sS -X POST "http://localhost:${port}/api/mcp" \\
133
+ -H "Content-Type: application/json" \\
134
+ -H "Accept: application/json, text/event-stream" \\
135
+ -d '{
136
+ "jsonrpc": "2.0",
137
+ "id": 1,
138
+ "method": "initialize",
139
+ "params": {
140
+ "protocolVersion": "2025-03-26",
141
+ "capabilities": {},
142
+ "clientInfo": { "name": "codex-onboard-check", "version": "0" }
143
+ }
144
+ }'
145
+ \`\`\`
146
+
147
+ Expected signal: a 200 response containing \`serverInfo\` with \`agent-inspector\`.
148
+
149
+ ---
150
+
151
+ ## Phase 4: First Inspector query
152
+
153
+ After Codex restarts with the MCP server configured, ask Codex to use Agent Inspector:
154
+
155
+ \`\`\`text
156
+ Use Agent Inspector to list the latest captured logs.
157
+ \`\`\`
158
+
159
+ The expected tool is \`inspector_list_logs\`. If no logs exist yet, that is still a successful MCP connection.
160
+
161
+ Useful follow-up tools:
162
+
163
+ - \`inspector_list_logs\`
164
+ - \`inspector_get_log\`
165
+ - \`inspector_get_log_chunks\`
166
+ - \`inspector_list_sessions\`
167
+ - \`inspector_test_provider\`
168
+ - \`inspector_create_session_knowledge\`
169
+ - \`inspector_list_knowledge_candidates\`
170
+ - \`inspector_search_knowledge\`
171
+ - \`inspector_get_project_context\`
172
+
173
+ Provider mutation tools expose and modify local provider configuration. Ask for explicit user confirmation before using \`inspector_add_provider\`, \`inspector_update_provider\`, or \`inspector_replay_log\`.
174
+
175
+ ---
176
+
177
+ ## Phase 5: Capture and memory review
178
+
179
+ To capture Codex traffic through Agent Inspector, route a model client through:
180
+
181
+ \`\`\`text
182
+ http://localhost:${port}/proxy
183
+ \`\`\`
184
+
185
+ For Anthropic-compatible tools, set \`ANTHROPIC_BASE_URL=http://localhost:${port}/proxy\`. For OpenAI-compatible tools, set \`OPENAI_BASE_URL=http://localhost:${port}/proxy\`.
186
+
187
+ For a Provider Test memory probe, run a provider test in the Agent Inspector UI:
188
+
189
+ 1. Open \`http://localhost:${port}\`.
190
+ 2. Go to Settings -> Providers.
191
+ 3. Add or edit a Provider.
192
+ 4. Run Test.
193
+ 5. Check the \`provider-test\` session.
194
+
195
+ Then ask Codex:
196
+
197
+ \`\`\`text
198
+ Use Agent Inspector to create knowledge candidates for the provider-test session, then list the candidates.
199
+ \`\`\`
200
+
201
+ Expected flow:
202
+
203
+ - Raw Trace stays in Agent Inspector logs.
204
+ - Session Episode is reconstructed from the session.
205
+ - Memory Candidate is generated for review.
206
+ - Promotion to OpenClaw happens only after review through Agent Inspector.
207
+
208
+ Stop after showing candidates unless the user explicitly approves promotion.
209
+ `;
210
+ }
package/src/cli.ts CHANGED
@@ -10,6 +10,9 @@ const __dirname = dirname(__filename);
10
10
 
11
11
  const DEFAULT_PORT = 25947;
12
12
  const LOCAL_PROBE_TIMEOUT_MS = 2000;
13
+ const BRANDED_WINDOWS_RUNTIME_EXE = "agent-inspector.exe";
14
+
15
+ process.title = "Agent Inspector";
13
16
 
14
17
  /**
15
18
  * Subcommand router. The legacy one-liner UX (`agent-inspector` with no args,
@@ -123,6 +126,18 @@ function waitForProcessExit(child: ChildProcess): Promise<number> {
123
126
  });
124
127
  }
125
128
 
129
+ type ServerCommand = {
130
+ command: string;
131
+ args: string[];
132
+ };
133
+
134
+ function resolveServerCommand(outputDir: string, serverPath: string): ServerCommand {
135
+ const brandedRuntime = join(outputDir, BRANDED_WINDOWS_RUNTIME_EXE);
136
+ return process.platform === "win32" && existsSync(brandedRuntime)
137
+ ? { command: brandedRuntime, args: [serverPath] }
138
+ : { command: process.execPath, args: [serverPath] };
139
+ }
140
+
126
141
  async function runStart(args: string[]): Promise<void> {
127
142
  const envPort = process.env["PORT"];
128
143
  const portDefault = envPort !== undefined ? Number(envPort) : DEFAULT_PORT;
@@ -298,8 +313,9 @@ async function runStart(args: string[]): Promise<void> {
298
313
  // Compute server path
299
314
  const outputDir = __dirname;
300
315
  const serverPath = join(outputDir, "../.output/server/index.mjs");
316
+ const serverCommand = resolveServerCommand(outputDir, serverPath);
301
317
 
302
- // Start server with node
318
+ // Start the server with the branded Windows runtime when postinstall created it.
303
319
  const serverEnv = { ...process.env };
304
320
  if (configDir !== undefined) {
305
321
  // Normalize MSYS / Git Bash paths to Windows native form.
@@ -323,7 +339,7 @@ async function runStart(args: string[]): Promise<void> {
323
339
  if (providersJson !== undefined) {
324
340
  serverEnv["AGENT_INSPECTOR_PROVIDERS_JSON"] = providersJson;
325
341
  }
326
- const serverProcess = spawn(process.execPath, [serverPath], {
342
+ const serverProcess = spawn(serverCommand.command, serverCommand.args, {
327
343
  stdio: background ? ["ignore", "ignore", "ignore"] : "inherit",
328
344
  detached: background,
329
345
  env: serverEnv,
@@ -1,4 +1,4 @@
1
- import { type JSX, type ReactNode, useCallback, useRef, useState } from "react";
1
+ import { type JSX, type ReactNode, useState } from "react";
2
2
  import { Button } from "../ui/button";
3
3
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
4
4
  import {
@@ -235,31 +235,6 @@ export function ProviderCard({
235
235
  const [showApiKey, setShowApiKey] = useState(false);
236
236
  const [copied, setCopied] = useState(false);
237
237
  const [showModelResults, setShowModelResults] = useState(false);
238
- const hasLoggedRef = useRef<string | null>(null);
239
- const lastTestedAtRef = useRef<string | undefined>(undefined);
240
-
241
- // Reset log state when new test results arrive
242
- if (testResults?.testedAt !== undefined && testResults.testedAt !== lastTestedAtRef.current) {
243
- lastTestedAtRef.current = testResults.testedAt;
244
- hasLoggedRef.current = null;
245
- }
246
-
247
- // Call log API when user expands model test results for the first time
248
- const handleToggleModelResults = useCallback(() => {
249
- setShowModelResults((v) => {
250
- const next = !v;
251
- if (next && hasLoggedRef.current === null && testResults?.models !== undefined) {
252
- hasLoggedRef.current = testResults.testedAt ?? "";
253
- // Fire-and-forget: commit test results to dashboard log
254
- void fetch(`/api/providers/${provider.id}/test/log`, {
255
- method: "POST",
256
- headers: { "Content-Type": "application/json" },
257
- body: JSON.stringify(testResults),
258
- });
259
- }
260
- return next;
261
- });
262
- }, [provider.id, testResults]);
263
238
 
264
239
  function handleCopy() {
265
240
  navigator.clipboard.writeText(provider.apiKey).catch(() => {});
@@ -382,7 +357,7 @@ export function ProviderCard({
382
357
  <div className="border-t pt-2">
383
358
  <button
384
359
  type="button"
385
- onClick={handleToggleModelResults}
360
+ onClick={() => setShowModelResults((value) => !value)}
386
361
  className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
387
362
  >
388
363
  <span className="font-mono">{showModelResults ? "▾" : "▸"}</span>
@@ -54,6 +54,21 @@ function createProviderPayload(data: ProviderFormData) {
54
54
  };
55
55
  }
56
56
 
57
+ async function persistProviderTestLog(providerId: string, results: TestResults): Promise<void> {
58
+ if (results.models === undefined) {
59
+ return;
60
+ }
61
+ try {
62
+ await fetch(`/api/providers/${providerId}/test/log`, {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/json" },
65
+ body: JSON.stringify(results),
66
+ });
67
+ } catch {
68
+ // Provider connection status is primary; log persistence is best-effort.
69
+ }
70
+ }
71
+
57
72
  type ProvidersPanelProps = {
58
73
  externalProviders?: ProviderConfig[];
59
74
  isLoading?: boolean;
@@ -227,6 +242,7 @@ export function ProvidersPanel({
227
242
  if (res.ok) {
228
243
  const results = await parseJsonResponse(res, ProviderTestResultsSchema);
229
244
  updateTestResults(providerId, results);
245
+ await persistProviderTestLog(providerId, results);
230
246
  } else {
231
247
  updateTestResults(
232
248
  providerId,