@tonyclaw/llm-inspector 1.18.2 → 1.19.0

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 (42) hide show
  1. package/.output/cli.js +776 -139
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-C-4ypEWs.js → CompareDrawer-DwayZPPO.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-iv3LVMEW.js +101 -0
  5. package/.output/public/assets/{ReplayDialog-CyBKOgba.js → ReplayDialog-CaV1elYO.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-C0IrVQ3q.js → RequestAnatomy-CSfnjK7j.js} +1 -1
  7. package/.output/public/assets/{ResponseView-MogToC4i.js → ResponseView-YkOL__xm.js} +1 -1
  8. package/.output/public/assets/{StreamingChunkSequence-ClhUhT-s.js → StreamingChunkSequence-D_p6L-oB.js} +1 -1
  9. package/.output/public/assets/_sessionId-BgCVUC6R.js +1 -0
  10. package/.output/public/assets/index-CWA4S0FO.js +1 -0
  11. package/.output/public/assets/index-DeJyypsp.css +1 -0
  12. package/.output/public/assets/{json-viewer-BicGakI5.js → json-viewer-BB-9bqnP.js} +2 -2
  13. package/.output/public/assets/{main-Be2qqUUW.js → main-COVN451W.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +93 -72
  15. package/.output/server/{_sessionId-DhKJIdQC.mjs → _sessionId-BJT5qIib.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-BGUgukJ8.mjs → CompareDrawer-DNGYdUXs.mjs} +4 -4
  17. package/.output/server/_ssr/{ProxyViewerContainer--3K3o3Sm.mjs → ProxyViewerContainer-B-zDOLYE.mjs} +354 -343
  18. package/.output/server/_ssr/{ReplayDialog-Bo86xZI4.mjs → ReplayDialog-DWeqMA4y.mjs} +4 -4
  19. package/.output/server/_ssr/{RequestAnatomy-jRU5qgwB.mjs → RequestAnatomy-TOsrMu9-.mjs} +3 -3
  20. package/.output/server/_ssr/{ResponseView-DdO_-79a.mjs → ResponseView-BuqdPrzm.mjs} +4 -4
  21. package/.output/server/_ssr/{StreamingChunkSequence-BigLwhh4.mjs → StreamingChunkSequence-DuzNZkqL.mjs} +3 -3
  22. package/.output/server/_ssr/{index-BHG6vOnr.mjs → index-1nCQUt3y.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-B4c_WjXD.mjs → json-viewer-BL8xhHbi.mjs} +9 -5
  25. package/.output/server/_ssr/{router-DVixpJO-.mjs → router-aCaUgVTW.mjs} +3 -3
  26. package/.output/server/{_tanstack-start-manifest_v-BbvWUF4v.mjs → _tanstack-start-manifest_v-cBRxvCjb.mjs} +1 -1
  27. package/.output/server/index.mjs +61 -61
  28. package/package.json +2 -1
  29. package/src/cli/detect-tools.ts +146 -0
  30. package/src/cli/onboard.ts +229 -0
  31. package/src/cli/templates/command-onboard.ts +17 -0
  32. package/src/cli/templates/skill-onboard.ts +325 -0
  33. package/src/cli.ts +185 -163
  34. package/src/components/ProxyViewer.tsx +153 -142
  35. package/src/components/proxy-viewer/LogEntry.tsx +136 -157
  36. package/src/components/proxy-viewer/LogEntryHeader.tsx +147 -66
  37. package/src/components/proxy-viewer/useCopyFeedback.ts +36 -0
  38. package/src/components/ui/json-viewer.tsx +12 -0
  39. package/.output/public/assets/ProxyViewerContainer-WRenRpeh.js +0 -101
  40. package/.output/public/assets/_sessionId-BO47oA3Z.js +0 -1
  41. package/.output/public/assets/index-BRvz6-L6.css +0 -1
  42. package/.output/public/assets/index-Btw8ec7-.js +0 -1
package/.output/cli.js CHANGED
@@ -1,154 +1,791 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
2
11
 
3
- // src/cli.ts
4
- import { spawn, execSync } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
- import { dirname, join } from "node:path";
7
- var __filename = fileURLToPath(import.meta.url);
8
- var __dirname = dirname(__filename);
9
- var DEFAULT_PORT = 25947;
10
- var envPort = process.env["PORT"];
11
- var portDefault = envPort !== void 0 ? Number(envPort) : DEFAULT_PORT;
12
- var args = process.argv.slice(2);
13
- var port = portDefault;
14
- var open = true;
15
- var configDir;
16
- var providersJson;
17
- for (let i = 0; i < args.length; i++) {
18
- const arg = args[i] ?? "";
19
- switch (arg) {
20
- case "--port":
21
- case "-p":
22
- port = Number(args[i + 1]);
23
- i++;
24
- break;
25
- case "--no-open":
26
- open = false;
27
- break;
28
- case "--open":
29
- open = true;
30
- break;
31
- case "--config-dir":
32
- configDir = args[i + 1];
33
- i++;
34
- break;
35
- case "--providers":
36
- providersJson = args[i + 1];
37
- i++;
38
- break;
39
- default:
40
- break;
41
- }
42
- }
43
- function killProcessOnPort(targetPort) {
44
- const platform = process.platform;
12
+ // src/cli/detect-tools.ts
13
+ import { execFileSync } from "node:child_process";
14
+ import { existsSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { homedir } from "node:os";
17
+ function which(bin) {
45
18
  try {
46
- let pids = [];
47
- if (platform === "win32") {
48
- const output = execSync(`netstat -ano | findstr :${targetPort}`, {
19
+ if (process.platform === "win32") {
20
+ const out2 = execFileSync("where", [bin], {
49
21
  encoding: "utf8",
50
- timeout: 5e3
22
+ timeout: 3e3,
23
+ stdio: ["ignore", "pipe", "ignore"]
51
24
  });
52
- const lines = output.trim().split("\n");
53
- for (const line of lines) {
54
- const parts = line.trim().split(/\s+/);
55
- if (parts.length >= 5) {
56
- const localAddress = parts[1] ?? "";
57
- const pidStr = parts[4] ?? "";
58
- if (localAddress !== "" && pidStr !== "" && localAddress.includes(`:${targetPort}`)) {
59
- const pid = parseInt(pidStr, 10);
60
- if (!isNaN(pid) && pid > 0) {
61
- pids.push(pid);
25
+ const first = out2.split(/\r?\n/).find((line) => line.trim().length > 0);
26
+ return first?.trim() ?? null;
27
+ }
28
+ const out = execFileSync("sh", ["-c", `command -v ${bin}`], {
29
+ encoding: "utf8",
30
+ timeout: 3e3,
31
+ stdio: ["ignore", "pipe", "ignore"]
32
+ });
33
+ return out.trim() || null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ function tryDir(path) {
39
+ return existsSync(path) ? path : null;
40
+ }
41
+ function detectClaudeCode() {
42
+ const configDir = tryDir(join(homedir(), ".claude"));
43
+ const bin = which("claude");
44
+ if (configDir === null && bin === null) return { found: false };
45
+ return { found: true, path: configDir ?? bin ?? "" };
46
+ }
47
+ function detectOpenCode() {
48
+ const configDir = tryDir(join(homedir(), ".config", "opencode"));
49
+ const bin = which("opencode");
50
+ if (configDir === null && bin === null) return { found: false };
51
+ return { found: true, path: configDir ?? bin ?? "" };
52
+ }
53
+ function detectMiMo() {
54
+ const configDir = tryDir(join(homedir(), ".mimo"));
55
+ const bin = which("mimo");
56
+ if (configDir === null && bin === null) return { found: false };
57
+ return { found: true, path: configDir ?? bin ?? "" };
58
+ }
59
+ function detectCursor() {
60
+ const configDir = tryDir(join(homedir(), ".cursor"));
61
+ const bin = which("cursor");
62
+ if (configDir === null && bin === null) return { found: false };
63
+ return { found: true, path: configDir ?? bin ?? "" };
64
+ }
65
+ function detectCody() {
66
+ const configDir = tryDir(join(homedir(), ".config", "cody"));
67
+ const bin = which("cody");
68
+ if (configDir === null && bin === null) return { found: false };
69
+ return { found: true, path: configDir ?? bin ?? "" };
70
+ }
71
+ function detectAll() {
72
+ return DETECTORS.map(({ id, displayName, detect }) => ({
73
+ id,
74
+ displayName,
75
+ result: detect()
76
+ }));
77
+ }
78
+ function detectFirst() {
79
+ for (const entry of DETECTORS) {
80
+ const result = entry.detect();
81
+ if (result.found) return { id: entry.id, displayName: entry.displayName, result };
82
+ }
83
+ return null;
84
+ }
85
+ var DETECTORS;
86
+ var init_detect_tools = __esm({
87
+ "src/cli/detect-tools.ts"() {
88
+ "use strict";
89
+ DETECTORS = [
90
+ { id: "claude-code", displayName: "Claude Code", detect: detectClaudeCode },
91
+ { id: "opencode", displayName: "OpenCode", detect: detectOpenCode },
92
+ { id: "mimo", displayName: "MiMo Code", detect: detectMiMo },
93
+ { id: "cursor", displayName: "Cursor", detect: detectCursor },
94
+ { id: "cody", displayName: "Cody", detect: detectCody }
95
+ ];
96
+ }
97
+ });
98
+
99
+ // src/cli/templates/command-onboard.ts
100
+ function renderCommandOnboard() {
101
+ return `---
102
+ description: Walk through llm-inspector setup \u2014 start the proxy, wire your AI tool, capture your first request.
103
+ ---
104
+
105
+ Invoke the \`llm-inspector-onboard\` skill and follow its phases.
106
+
107
+ The user wants to set up llm-inspector. If they have specific context (e.g. "I'm on Windows", "I already added my API key"), honor it \u2014 but otherwise just run the skill end-to-end and let the \`EXPLAIN / DO / PAUSE\` markers drive the conversation.
108
+ `;
109
+ }
110
+ var init_command_onboard = __esm({
111
+ "src/cli/templates/command-onboard.ts"() {
112
+ "use strict";
113
+ }
114
+ });
115
+
116
+ // src/cli/templates/skill-onboard.ts
117
+ function renderSkillOnboard(ctx) {
118
+ const { version, port, detectedSummary } = ctx;
119
+ return `---
120
+ name: llm-inspector-onboard
121
+ description: Guided setup for llm-inspector v${version} \u2014 start the proxy, wire your AI coding tool, capture your first request, and learn the Web UI.
122
+ metadata:
123
+ author: llm-inspector
124
+ version: ${version}
125
+ ---
126
+
127
+ # llm-inspector onboard
128
+
129
+ Guide the user from "I just installed llm-inspector" to "I can see my AI tool's traffic in the Web UI". This is a teaching experience \u2014 you'll do real work in their environment while explaining each step.
130
+
131
+ Environment detected by the installer:
132
+ ${detectedSummary || " (no known AI tool detected \u2014 the user can still use the generic curl example in Phase 4)"}
133
+
134
+ Default proxy port: \`${port}\` (override with \`PORT=<n> llm-inspector\` or \`--port <n>\`).
135
+
136
+ ---
137
+
138
+ ## Preflight
139
+
140
+ Before starting, verify the environment.
141
+
142
+ **EXPLAIN:** "Let's make sure everything we need is in place. Two quick checks."
143
+
144
+ **DO:** Run the platform-appropriate commands below. The user can copy-paste, or you can run them yourself if you have shell access.
145
+
146
+ \`\`\`bash
147
+ # Unix / macOS / WSL
148
+ node --version # expect >= 18
149
+ test -d "$HOME/.claude" && echo "claude-code: present" || echo "claude-code: not detected"
150
+ \`\`\`
151
+
152
+ \`\`\`powershell
153
+ # Windows PowerShell
154
+ node --version # expect >= 18
155
+ if (Test-Path "$env:USERPROFILE\\.claude") { Write-Host "claude-code: present" } else { Write-Host "claude-code: not detected" }
156
+ \`\`\`
157
+
158
+ **PAUSE** \u2014 if Node is older than 18, ask the user to install a newer version (https://nodejs.org) before continuing.
159
+
160
+ ---
161
+
162
+ ## Phase 1: Welcome
163
+
164
+ **EXPLAIN:**
165
+
166
+ \`\`\`
167
+ ## Welcome to llm-inspector!
168
+
169
+ llm-inspector is a transparent HTTP proxy + Web UI for AI coding tools. Point your tool at it, and you'll see every request and response \u2014 system prompts, tool definitions, message history, SSE streaming chunks, and token counts \u2014 captured live in a browser tab.
170
+
171
+ **What we'll do in the next ~10 minutes:**
172
+ 1. Add your first LLM provider (Anthropic or OpenAI key)
173
+ 2. Start the proxy
174
+ 3. Wire your AI tool to use it
175
+ 4. Capture a first request end-to-end
176
+ 5. Tour the key UI affordances
177
+
178
+ Ready? Let's start with the provider.
179
+ \`\`\`
180
+
181
+ **PAUSE** \u2014 wait for the user to confirm.
182
+
183
+ ---
184
+
185
+ ## Phase 2: Provider setup
186
+
187
+ **EXPLAIN:** "A 'provider' is an upstream LLM endpoint \u2014 Anthropic, OpenAI, MiniMax, etc. llm-inspector routes each request to the right upstream based on the model name. You need at least one provider configured for the proxy to forward traffic."
188
+
189
+ **DO:** Open (or create) \`~/.llm-inspector/config.json\` in the user's editor. If the file doesn't exist, create it with the structure below. Walk the user through filling in their API key for their provider of choice.
190
+
191
+ \`\`\`json
192
+ {
193
+ "providers": [
194
+ {
195
+ "id": "anthropic",
196
+ "type": "anthropic",
197
+ "apiKey": "sk-ant-...",
198
+ "baseUrl": "https://api.anthropic.com"
199
+ }
200
+ ]
201
+ }
202
+ \`\`\`
203
+
204
+ Alternative (avoid touching the config file): point the user at the Web UI \u2192 top-right Settings button \u2192 Providers tab, which has a form-driven flow.
205
+
206
+ **PAUSE** \u2014 wait for the user to confirm they have at least one provider with a key.
207
+
208
+ ---
209
+
210
+ ## Phase 3: Start proxy
211
+
212
+ **EXPLAIN:** "Time to start the proxy. It binds to port ${port} by default, kills any process already on that port, and prints the URL."
213
+
214
+ **DO:** Start the proxy in the background so you can keep working.
215
+
216
+ \`\`\`bash
217
+ # Unix / macOS / WSL
218
+ nohup llm-inspector --no-open > /tmp/llm-inspector.log 2>&1 &
219
+ \`\`\`
220
+
221
+ \`\`\`powershell
222
+ # Windows PowerShell
223
+ Start-Process -FilePath "llm-inspector" -ArgumentList "--no-open" -RedirectStandardOutput "$env:TEMP\\llm-inspector.log" -RedirectStandardError "$env:TEMP\\llm-inspector.err.log" -WindowStyle Hidden
224
+ \`\`\`
225
+
226
+ Then wait for the port to be ready:
227
+
228
+ \`\`\`bash
229
+ # Wait up to 10s for the port to come up
230
+ for i in $(seq 1 20); do
231
+ curl -fsS "http://localhost:${port}/api/health" >/dev/null 2>&1 && echo "ready" && break
232
+ sleep 0.5
233
+ done
234
+ \`\`\`
235
+
236
+ **DO:** Hit the health endpoint to confirm the proxy is alive:
237
+
238
+ \`\`\`bash
239
+ curl -sS "http://localhost:${port}/api/health"
240
+ \`\`\`
241
+
242
+ **PAUSE** \u2014 if the health check fails, show the user the log file (\`/tmp/llm-inspector.log\` or \`%TEMP%\\\\llm-inspector.log\`) and diagnose. Common issues: another process on the port, firewall, missing providers.
243
+
244
+ ---
245
+
246
+ ## Phase 4: Wire tool
247
+
248
+ **EXPLAIN:** "Now we tell your AI tool to send traffic through the proxy instead of directly to the upstream API. The exact env var depends on which tool you have."
249
+
250
+ **DO:** Based on the \`Environment detected\` block at the top of this skill, print the matching wiring command. Examples for each supported tool:
251
+
252
+ \`\`\`bash
253
+ # Claude Code
254
+ export ANTHROPIC_BASE_URL=http://localhost:${port}/proxy
255
+ claude
256
+
257
+ # OpenCode
258
+ export LLM_BASE_URL=http://localhost:${port}/proxy
259
+ opencode
260
+
261
+ # MiMo Code
262
+ export OPENAI_BASE_URL=http://localhost:${port}/proxy
263
+ mimo
264
+
265
+ # Cursor / Cody \u2014 set the OpenAI base URL in each tool's settings panel to http://localhost:${port}/proxy
266
+ \`\`\`
267
+
268
+ For a tool that wasn't auto-detected, fall through to the generic curl test in the next phase \u2014 the user can wire their tool later.
269
+
270
+ **PAUSE** \u2014 wait for the user to confirm they've set the env var (or that they're going to use the curl test instead).
271
+
272
+ ---
273
+
274
+ ## Phase 4.5: Wire MCP server
275
+
276
+ **EXPLAIN:** "The proxy also exposes an MCP server at \`http://localhost:${port}/api/mcp\`. Your AI agent can query logs, replay requests, and test providers through it \u2014 no need to leave the editor. We'll add an \`mcpServers\` entry so your agent picks it up automatically."
277
+
278
+ **DO:** Add (or merge) an \`mcpServers\` entry in the user's Claude Code config. The file is \`~/.claude.json\` on all platforms; the MCP spec entry uses HTTP Streamable transport:
279
+
280
+ \`\`\`json
281
+ // ~/.claude.json (merge into existing mcpServers)
282
+ {
283
+ "mcpServers": {
284
+ "llm-inspector": {
285
+ "type": "http",
286
+ "url": "http://localhost:${port}/api/mcp"
287
+ }
288
+ }
289
+ }
290
+ \`\`\`
291
+
292
+ If \`mcpServers\` already exists, add \`llm-inspector\` as a new key \u2014 don't clobber existing entries.
293
+
294
+ PowerShell one-liner that creates the file if missing, or merges if present:
295
+
296
+ \`\`\`powershell
297
+ $cfg = Join-Path $env:USERPROFILE ".claude.json"
298
+ if (Test-Path $cfg) {
299
+ $doc = Get-Content $cfg -Raw | ConvertFrom-Json
300
+ } else {
301
+ $doc = [pscustomobject]@{ mcpServers = [pscustomobject]@{} }
302
+ }
303
+ if (-not ($doc.PSObject.Properties.Name -contains "mcpServers")) {
304
+ $doc | Add-Member -NotePropertyName mcpServers -NotePropertyValue ([pscustomobject]@{})
305
+ }
306
+ $doc.mcpServers | Add-Member -NotePropertyName "llm-inspector" -NotePropertyValue ([pscustomobject]@{
307
+ type = "http"
308
+ url = "http://localhost:${port}/api/mcp"
309
+ }) -Force
310
+ $doc | ConvertTo-Json -Depth 10 | Set-Content $cfg -Encoding UTF8
311
+ \`\`\`
312
+
313
+ **DO:** Verify the handshake. The MCP \`initialize\` request should return 200 with a \`serverInfo\` payload \u2014 that proves the server is mounted and reachable:
314
+
315
+ \`\`\`bash
316
+ curl -sS -X POST "http://localhost:${port}/api/mcp" \\
317
+ -H "Content-Type: application/json" \\
318
+ -H "Accept: application/json, text/event-stream" \\
319
+ -d '{
320
+ "jsonrpc": "2.0",
321
+ "id": 1,
322
+ "method": "initialize",
323
+ "params": {
324
+ "protocolVersion": "2025-03-26",
325
+ "capabilities": {},
326
+ "clientInfo": { "name": "onboard-check", "version": "0" }
327
+ }
328
+ }'
329
+ # expect: HTTP 200, body contains "result" with serverInfo.name = "llm-inspector"
330
+ \`\`\`
331
+
332
+ After the handshake, issue a \`tools/list\` to confirm the tool catalog is reachable:
333
+
334
+ \`\`\`bash
335
+ SESSION=<session-id-from-initialize-response>
336
+ curl -sS -X POST "http://localhost:${port}/api/mcp" \\
337
+ -H "Content-Type: application/json" \\
338
+ -H "Accept: application/json, text/event-stream" \\
339
+ -H "mcp-session-id: $SESSION" \\
340
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
341
+ # expect: a result.tools array with at least 1 entry
342
+ \`\`\`
343
+
344
+ **PAUSE** \u2014 if \`initialize\` returns non-200, show the user the proxy log and re-check the JSON syntax. If it returns 200 but \`tools/list\` fails, the server is up but the session wasn't carried over \u2014 re-use the \`mcp-session-id\` header from the first response.
345
+
346
+ ---
347
+
348
+ ## Phase 5: First capture
349
+
350
+ **EXPLAIN:** "Let's prove the proxy works end-to-end. We'll send one real request through it and confirm the log shows up in the API. A 401/403 from the upstream is fine \u2014 the point is that the *request* reaches the proxy."
351
+
352
+ **DO:** Fire a minimal Anthropic-format request through the proxy. This works regardless of which tool the user wired up:
353
+
354
+ \`\`\`bash
355
+ curl -sS -X POST "http://localhost:${port}/proxy/v1/messages" \\
356
+ -H "Content-Type: application/json" \\
357
+ -H "anthropic-version: 2023-06-01" \\
358
+ -H "x-api-key: \${LLM_INSPECTOR_API_KEY:-sk-no-key-needed-for-routing}" \\
359
+ -d '{"model":"claude-3-5-sonnet-20241022","max_tokens":1,"messages":[{"role":"user","content":"ping"}]}'
360
+ \`\`\`
361
+
362
+ **DO:** Poll the logs API for up to 5 seconds. A 200 with at least one entry means success:
363
+
364
+ \`\`\`bash
365
+ for i in $(seq 1 10); do
366
+ resp=$(curl -sS "http://localhost:${port}/api/logs?limit=1")
367
+ count=$(echo "$resp" | grep -o '"total":[0-9]*' | head -1 | grep -o '[0-9]*$')
368
+ if [ "\${count:-0}" -ge 1 ]; then
369
+ echo "captured"
370
+ break
371
+ fi
372
+ sleep 0.5
373
+ done
374
+ \`\`\`
375
+
376
+ **PAUSE** \u2014 show the user the captured log entry (id, status, model, elapsed). If the count never reached 1, the proxy didn't see the request \u2014 re-check the env var and the proxy log.
377
+
378
+ ---
379
+
380
+ ## Phase 6: Tour & wrap
381
+
382
+ **EXPLAIN:** "Everything's working. Here's the cheat sheet for the Web UI and the supporting surfaces:"
383
+
384
+ - **Web UI**: \`http://localhost:${port}/\` \u2014 collapsible log rows, per-tab Copy/Expand in the log header, Diff with Raw (request body), Diff with Previous (compare adjacent requests), Replay (re-send a request), Export (JSON ZIP).
385
+ - **MCP server**: \`http://localhost:${port}/api/mcp\` \u2014 connect from your coding agent to query logs, replay, and test providers without leaving the editor. Look for the "MCP Ready" badge in the Web UI header.
386
+ - **REST API**: \`/api/logs\`, \`/api/sessions\`, \`/api/providers\` \u2014 for scripting and shell-based inspection.
387
+ - **Stop the proxy**:
388
+
389
+ \`\`\`bash
390
+ # Unix / macOS
391
+ lsof -ti:${port} | xargs -r kill -9
392
+
393
+ # Windows PowerShell
394
+ Get-NetTCPConnection -LocalPort ${port} | ForEach-Object { Stop-Process -Id \\$_.OwningProcess -Force }
395
+ \`\`\`
396
+
397
+ - **Re-run onboard**: \`llm-inspector onboard --force\` refreshes this skill.
398
+ - **Full docs**: see the project README (linked from the Web UI footer).
399
+
400
+ **PAUSE** \u2014 let the user know they can come back to this skill at any time via \`/llm-inspector:onboard\` if they want a refresher, and call out that \`/llm-inspector:onboard --skip-tool-wire\` is the way to re-run later phases without re-detecting the tool.
401
+
402
+ You're done. Happy inspecting.
403
+ `;
404
+ }
405
+ var REQUIRED_PHASE_HEADINGS;
406
+ var init_skill_onboard = __esm({
407
+ "src/cli/templates/skill-onboard.ts"() {
408
+ "use strict";
409
+ REQUIRED_PHASE_HEADINGS = [
410
+ "Preflight",
411
+ "Phase 1: Welcome",
412
+ "Phase 2: Provider setup",
413
+ "Phase 3: Start proxy",
414
+ "Phase 4: Wire tool",
415
+ "Phase 4.5: Wire MCP server",
416
+ "Phase 5: First capture",
417
+ "Phase 6: Tour & wrap"
418
+ ];
419
+ }
420
+ });
421
+
422
+ // src/cli/onboard.ts
423
+ var onboard_exports = {};
424
+ __export(onboard_exports, {
425
+ runOnboard: () => runOnboard
426
+ });
427
+ import { mkdirSync, writeFileSync, existsSync as existsSync2, readFileSync } from "node:fs";
428
+ import { homedir as homedir2 } from "node:os";
429
+ import { dirname, join as join2 } from "node:path";
430
+ import { fileURLToPath } from "node:url";
431
+ function parseFlags(argv) {
432
+ const flags = {
433
+ force: false,
434
+ dryRun: false,
435
+ skipProvider: false,
436
+ skipToolWire: false,
437
+ skillDir: null
438
+ };
439
+ for (let i = 0; i < argv.length; i++) {
440
+ const arg = argv[i];
441
+ switch (arg) {
442
+ case void 0:
443
+ continue;
444
+ case "--force":
445
+ flags.force = true;
446
+ break;
447
+ case "--dry-run":
448
+ flags.dryRun = true;
449
+ break;
450
+ case "--skip-provider":
451
+ flags.skipProvider = true;
452
+ break;
453
+ case "--skip-tool-wire":
454
+ flags.skipToolWire = true;
455
+ break;
456
+ case "--skill-dir": {
457
+ const next = argv[i + 1];
458
+ if (next === void 0) {
459
+ process.stderr.write("llm-inspector onboard: --skill-dir requires a path argument\n");
460
+ process.exit(2);
461
+ }
462
+ flags.skillDir = next;
463
+ i++;
464
+ break;
465
+ }
466
+ case "--help":
467
+ case "-h":
468
+ printHelp();
469
+ process.exit(0);
470
+ break;
471
+ default:
472
+ process.stderr.write(`llm-inspector onboard: unknown flag: ${arg}
473
+ `);
474
+ process.exit(2);
475
+ }
476
+ }
477
+ return flags;
478
+ }
479
+ function printHelp() {
480
+ process.stdout.write(`llm-inspector onboard \u2014 install the llm-inspector Claude Code skill
481
+
482
+ Usage:
483
+ llm-inspector onboard [options]
484
+
485
+ Options:
486
+ --force Overwrite the existing skill and slash command if they exist
487
+ --dry-run Print target paths and a template preview, write nothing
488
+ --skip-provider Skip the provider-setup phase in the skill body
489
+ --skip-tool-wire Skip the wire-tool phase in the skill body
490
+ --skill-dir <path> Override the target skill directory (default: ~/.claude/skills)
491
+ -h, --help Show this help
492
+
493
+ Exit codes:
494
+ 0 success (or already installed)
495
+ 2 invalid arguments
496
+ 1 write failed
497
+ `);
498
+ }
499
+ function resolveTargets(flags) {
500
+ const claudeRoot = flags.skillDir ?? join2(homedir2(), ".claude");
501
+ const skillDir = join2(claudeRoot, "skills", SKILL_DIR_NAME);
502
+ const commandsDir = join2(claudeRoot, "commands");
503
+ return {
504
+ skillFile: join2(skillDir, SKILL_FILE_NAME),
505
+ commandFile: join2(commandsDir, COMMAND_FILE_NAME)
506
+ };
507
+ }
508
+ function isObject(value) {
509
+ return typeof value === "object" && value !== null;
510
+ }
511
+ function buildDetectedSummary() {
512
+ const entries = detectAll();
513
+ const present = entries.filter((e) => e.result.found);
514
+ const absent = entries.filter((e) => !e.result.found);
515
+ const presentList = present.map((e) => e.displayName).join(", ") || "(none)";
516
+ const absentList = absent.map((e) => e.displayName).join(", ") || "(none)";
517
+ return ` - Detected: ${presentList}
518
+ - Not detected: ${absentList}`;
519
+ }
520
+ function runOnboard(argv) {
521
+ try {
522
+ return Promise.resolve(runOnboardSync(argv));
523
+ } catch (err) {
524
+ const msg = err instanceof Error ? err.message : String(err);
525
+ process.stderr.write(`llm-inspector onboard: ${msg}
526
+ `);
527
+ process.stderr.write(
528
+ "(postinstall skill install skipped \u2014 run `llm-inspector onboard` later)\n"
529
+ );
530
+ return Promise.resolve(0);
531
+ }
532
+ }
533
+ function runOnboardSync(argv) {
534
+ const flags = parseFlags(argv);
535
+ const { skillFile, commandFile } = resolveTargets(flags);
536
+ if (!flags.force && existsSync2(skillFile)) {
537
+ process.stdout.write(`llm-inspector onboard: already installed at ${skillFile}
538
+ `);
539
+ process.stdout.write("Re-run with --force to refresh.\n");
540
+ return 0;
541
+ }
542
+ let version = "0.0.0";
543
+ try {
544
+ const raw = JSON.parse(readFileSync(join2(__dirname, "..", "package.json"), "utf8"));
545
+ if (isObject(raw) && typeof raw["version"] === "string") {
546
+ version = raw["version"];
547
+ }
548
+ } catch {
549
+ }
550
+ const detectedSummary = buildDetectedSummary();
551
+ const skillBody = renderSkillOnboard({
552
+ version,
553
+ port: DEFAULT_PORT,
554
+ detectedSummary
555
+ });
556
+ const commandBody = renderCommandOnboard();
557
+ if (flags.dryRun) {
558
+ process.stdout.write(`llm-inspector onboard --dry-run
559
+
560
+ `);
561
+ process.stdout.write(`Skill target: ${skillFile}
562
+ `);
563
+ process.stdout.write(`Command target: ${commandFile}
564
+
565
+ `);
566
+ process.stdout.write(`Skill preview (first 5 lines + headings):
567
+ `);
568
+ const previewLines = skillBody.split("\n").slice(0, 5);
569
+ process.stdout.write(`${previewLines.join("\n")}
570
+ `);
571
+ process.stdout.write(`...
572
+ `);
573
+ for (const heading of REQUIRED_PHASE_HEADINGS) {
574
+ process.stdout.write(` - ${heading}
575
+ `);
576
+ }
577
+ process.stdout.write(`
578
+ Detected tools:
579
+ ${detectedSummary}
580
+ `);
581
+ process.stdout.write(`
582
+ No files were written.
583
+ `);
584
+ return 0;
585
+ }
586
+ mkdirSync(join2(skillFile, ".."), { recursive: true });
587
+ mkdirSync(join2(commandFile, ".."), { recursive: true });
588
+ try {
589
+ writeFileSync(skillFile, skillBody, "utf8");
590
+ writeFileSync(commandFile, commandBody, "utf8");
591
+ } catch (err) {
592
+ const msg = err instanceof Error ? err.message : String(err);
593
+ process.stderr.write(`llm-inspector onboard: failed to write files: ${msg}
594
+ `);
595
+ return 1;
596
+ }
597
+ const firstTool = detectFirst();
598
+ const toolHint = firstTool !== null ? firstTool.displayName : "no known tool detected";
599
+ process.stdout.write(`Installed skill to: ${skillFile}
600
+ `);
601
+ process.stdout.write(`Installed command to: ${commandFile}
602
+ `);
603
+ process.stdout.write(`
604
+ Next steps:
605
+ `);
606
+ process.stdout.write(` - Open Claude Code and run: /llm-inspector:onboard
607
+ `);
608
+ process.stdout.write(` - Or refresh later: llm-inspector onboard --force
609
+ `);
610
+ process.stdout.write(` - Detected primary tool: ${toolHint}
611
+ `);
612
+ return 0;
613
+ }
614
+ var __filename, __dirname, DEFAULT_PORT, SKILL_DIR_NAME, SKILL_FILE_NAME, COMMAND_FILE_NAME;
615
+ var init_onboard = __esm({
616
+ "src/cli/onboard.ts"() {
617
+ "use strict";
618
+ init_detect_tools();
619
+ init_command_onboard();
620
+ init_skill_onboard();
621
+ __filename = fileURLToPath(import.meta.url);
622
+ __dirname = dirname(__filename);
623
+ DEFAULT_PORT = 25947;
624
+ SKILL_DIR_NAME = "llm-inspector-onboard";
625
+ SKILL_FILE_NAME = "SKILL.md";
626
+ COMMAND_FILE_NAME = process.platform === "win32" ? "llm-inspector-onboard.md" : "llm-inspector:onboard.md";
627
+ }
628
+ });
629
+
630
+ // src/cli.ts
631
+ import { spawn, execSync } from "node:child_process";
632
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
633
+ import { dirname as dirname2, join as join3 } from "node:path";
634
+ var __filename2 = fileURLToPath2(import.meta.url);
635
+ var __dirname2 = dirname2(__filename2);
636
+ var DEFAULT_PORT2 = 25947;
637
+ var subcommand = process.argv[2];
638
+ if (subcommand === "onboard") {
639
+ const { runOnboard: runOnboard2 } = await Promise.resolve().then(() => (init_onboard(), onboard_exports));
640
+ const code = await runOnboard2(process.argv.slice(3));
641
+ process.exit(code);
642
+ }
643
+ runStart(process.argv.slice(2));
644
+ function runStart(args) {
645
+ const envPort = process.env["PORT"];
646
+ const portDefault = envPort !== void 0 ? Number(envPort) : DEFAULT_PORT2;
647
+ let port = portDefault;
648
+ let open = true;
649
+ let configDir;
650
+ let providersJson;
651
+ for (let i = 0; i < args.length; i++) {
652
+ const arg = args[i] ?? "";
653
+ switch (arg) {
654
+ case "--port":
655
+ case "-p":
656
+ port = Number(args[i + 1]);
657
+ i++;
658
+ break;
659
+ case "--no-open":
660
+ open = false;
661
+ break;
662
+ case "--open":
663
+ open = true;
664
+ break;
665
+ case "--config-dir":
666
+ configDir = args[i + 1];
667
+ i++;
668
+ break;
669
+ case "--providers":
670
+ providersJson = args[i + 1];
671
+ i++;
672
+ break;
673
+ default:
674
+ break;
675
+ }
676
+ }
677
+ function killProcessOnPort(targetPort) {
678
+ const platform = process.platform;
679
+ try {
680
+ let pids = [];
681
+ if (platform === "win32") {
682
+ const output = execSync(`netstat -ano | findstr :${targetPort}`, {
683
+ encoding: "utf8",
684
+ timeout: 5e3
685
+ });
686
+ const lines = output.trim().split("\n");
687
+ for (const line of lines) {
688
+ const parts = line.trim().split(/\s+/);
689
+ if (parts.length >= 5) {
690
+ const localAddress = parts[1] ?? "";
691
+ const pidStr = parts[4] ?? "";
692
+ if (localAddress !== "" && pidStr !== "" && localAddress.includes(`:${targetPort}`)) {
693
+ const pid = parseInt(pidStr, 10);
694
+ if (!isNaN(pid) && pid > 0) {
695
+ pids.push(pid);
696
+ }
62
697
  }
63
698
  }
64
699
  }
65
- }
66
- pids = [...new Set(pids)];
67
- for (const pid of pids) {
68
- try {
69
- console.log(`Killing process ${pid} on port ${port}...`);
70
- execSync(`taskkill /PID ${pid} /F`, { encoding: "utf8", timeout: 5e3 });
71
- } catch {
700
+ pids = [...new Set(pids)];
701
+ for (const pid of pids) {
702
+ try {
703
+ console.log(`Killing process ${pid} on port ${port}...`);
704
+ execSync(`taskkill /PID ${pid} /F`, { encoding: "utf8", timeout: 5e3 });
705
+ } catch {
706
+ }
72
707
  }
73
- }
74
- } else {
75
- const output = execSync(`lsof -ti:${targetPort}`, { encoding: "utf8", timeout: 5e3 });
76
- const lines = output.trim().split("\n");
77
- for (const line of lines) {
78
- const pid = parseInt(line.trim(), 10);
79
- if (!isNaN(pid) && pid > 0) {
80
- pids.push(pid);
708
+ } else {
709
+ const output = execSync(`lsof -ti:${targetPort}`, { encoding: "utf8", timeout: 5e3 });
710
+ const lines = output.trim().split("\n");
711
+ for (const line of lines) {
712
+ const pid = parseInt(line.trim(), 10);
713
+ if (!isNaN(pid) && pid > 0) {
714
+ pids.push(pid);
715
+ }
81
716
  }
82
- }
83
- pids = [...new Set(pids)];
84
- for (const pid of pids) {
85
- try {
86
- console.log(`Killing process ${pid} on port ${targetPort}...`);
87
- execSync(`kill -9 ${pid}`, { encoding: "utf8", timeout: 5e3 });
88
- } catch {
717
+ pids = [...new Set(pids)];
718
+ for (const pid of pids) {
719
+ try {
720
+ console.log(`Killing process ${pid} on port ${port}...`);
721
+ execSync(`kill -9 ${pid}`, { encoding: "utf8", timeout: 5e3 });
722
+ } catch {
723
+ }
89
724
  }
90
725
  }
726
+ } catch {
91
727
  }
92
- } catch {
93
728
  }
729
+ process.env["PORT"] = String(port);
730
+ killProcessOnPort(port);
731
+ const url = `http://localhost:${port}`;
732
+ console.log(`Server running at ${url}`);
733
+ console.log(` Proxy: ${url}/proxy`);
734
+ console.log(``);
735
+ console.log(`Route AI coding tools through the proxy:`);
736
+ console.log(` Claude Code: ANTHROPIC_BASE_URL=${url}/proxy claude`);
737
+ console.log(` OpenCode: LLM_BASE_URL=${url}/proxy opencode`);
738
+ console.log(` MiMo Code: OPENAI_BASE_URL=${url}/proxy mimo`);
739
+ console.log(
740
+ ` Direct HTTP: curl ${url}/proxy/v1/messages -d '{"model":"...","messages":[...]}'`
741
+ );
742
+ console.log(``);
743
+ console.log(`Routing environment variables:`);
744
+ console.log(` ROUTES JSON map of model prefix -> upstream URL`);
745
+ console.log(` DEFAULT_UPSTREAM Fallback upstream for unmatched models`);
746
+ console.log(
747
+ ` Example: ROUTES='{"claude-":"https://api.anthropic.com","MiniMax":"https://api.minimaxi.com/anthropic"}'`
748
+ );
749
+ const openBrowser = (targetUrl) => {
750
+ let command;
751
+ switch (process.platform) {
752
+ case "darwin":
753
+ command = ["open", targetUrl];
754
+ break;
755
+ case "linux":
756
+ command = ["xdg-open", targetUrl];
757
+ break;
758
+ case "win32":
759
+ command = ["cmd", "/c", "start", targetUrl];
760
+ break;
761
+ default:
762
+ break;
763
+ }
764
+ if (command === void 0) return;
765
+ const [bin, ...cmdArgs] = command;
766
+ if (bin === void 0) return;
767
+ spawn(bin, cmdArgs, { stdio: "ignore", detached: true });
768
+ };
769
+ if (open) {
770
+ openBrowser(url);
771
+ }
772
+ const outputDir = __dirname2;
773
+ const serverPath = join3(outputDir, "../.output/server/index.mjs");
774
+ const serverEnv = { ...process.env };
775
+ if (configDir !== void 0) {
776
+ let resolvedPath = join3(configDir, "config.json");
777
+ if (resolvedPath.startsWith("\\c\\")) {
778
+ resolvedPath = "C:" + resolvedPath;
779
+ }
780
+ serverEnv["LLM_INSPECTOR_CONFIG_PATH"] = resolvedPath;
781
+ }
782
+ if (providersJson !== void 0) {
783
+ serverEnv["LLM_INSPECTOR_PROVIDERS_JSON"] = providersJson;
784
+ }
785
+ const serverProcess = spawn(process.execPath, [serverPath], {
786
+ stdio: ["ignore", "inherit", "inherit"],
787
+ detached: true,
788
+ env: serverEnv
789
+ });
790
+ serverProcess.unref();
94
791
  }
95
- process.env["PORT"] = String(port);
96
- killProcessOnPort(port);
97
- var url = `http://localhost:${port}`;
98
- console.log(`Server running at ${url}`);
99
- console.log(` Proxy: ${url}/proxy`);
100
- console.log(``);
101
- console.log(`Route AI coding tools through the proxy:`);
102
- console.log(` Claude Code: ANTHROPIC_BASE_URL=${url}/proxy claude`);
103
- console.log(` OpenCode: LLM_BASE_URL=${url}/proxy opencode`);
104
- console.log(` MiMo Code: OPENAI_BASE_URL=${url}/proxy mimo`);
105
- console.log(` Direct HTTP: curl ${url}/proxy/v1/messages -d '{"model":"...","messages":[...]}'`);
106
- console.log(``);
107
- console.log(`Routing environment variables:`);
108
- console.log(` ROUTES JSON map of model prefix -> upstream URL`);
109
- console.log(` DEFAULT_UPSTREAM Fallback upstream for unmatched models`);
110
- console.log(
111
- ` Example: ROUTES='{"claude-":"https://api.anthropic.com","MiniMax":"https://api.minimaxi.com/anthropic"}'`
112
- );
113
- var openBrowser = (targetUrl) => {
114
- let command;
115
- switch (process.platform) {
116
- case "darwin":
117
- command = ["open", targetUrl];
118
- break;
119
- case "linux":
120
- command = ["xdg-open", targetUrl];
121
- break;
122
- case "win32":
123
- command = ["cmd", "/c", "start", targetUrl];
124
- break;
125
- default:
126
- break;
127
- }
128
- if (command === void 0) return;
129
- const [bin, ...cmdArgs] = command;
130
- if (bin === void 0) return;
131
- spawn(bin, cmdArgs, { stdio: "ignore", detached: true });
132
- };
133
- if (open) {
134
- openBrowser(url);
135
- }
136
- var outputDir = __dirname;
137
- var serverPath = join(outputDir, "../.output/server/index.mjs");
138
- var serverEnv = { ...process.env };
139
- if (configDir !== void 0) {
140
- let resolvedPath = join(configDir, "config.json");
141
- if (resolvedPath.startsWith("\\c\\")) {
142
- resolvedPath = "C:" + resolvedPath;
143
- }
144
- serverEnv["LLM_INSPECTOR_CONFIG_PATH"] = resolvedPath;
145
- }
146
- if (providersJson !== void 0) {
147
- serverEnv["LLM_INSPECTOR_PROVIDERS_JSON"] = providersJson;
148
- }
149
- var serverProcess = spawn(process.execPath, [serverPath], {
150
- stdio: ["ignore", "inherit", "inherit"],
151
- detached: true,
152
- env: serverEnv
153
- });
154
- serverProcess.unref();