@tonyclaw/llm-inspector 1.19.0 → 1.19.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.
- package/.output/cli.js +338 -102
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-DwayZPPO.js → CompareDrawer-BzTsEelr.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-iv3LVMEW.js → ProxyViewerContainer-BHm-n-_W.js} +9 -9
- package/.output/public/assets/{ReplayDialog-CaV1elYO.js → ReplayDialog-Dxxo80xO.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-CSfnjK7j.js → RequestAnatomy-D-swiaii.js} +1 -1
- package/.output/public/assets/{ResponseView-YkOL__xm.js → ResponseView-DvdH2bGk.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-D_p6L-oB.js → StreamingChunkSequence-D_RzgyKq.js} +1 -1
- package/.output/public/assets/_sessionId-DdODJCYY.js +1 -0
- package/.output/public/assets/{index-DeJyypsp.css → index-Bqi9RAGS.css} +1 -1
- package/.output/public/assets/index-EvnsNPOK.js +1 -0
- package/.output/public/assets/{json-viewer-BB-9bqnP.js → json-viewer-DIHZbEId.js} +1 -1
- package/.output/public/assets/{main-COVN451W.js → main-Br2EjrqZ.js} +2 -2
- package/.output/server/{_sessionId-BJT5qIib.mjs → _sessionId-CPkCxTP8.mjs} +4 -3
- package/.output/server/_ssr/{CompareDrawer-DNGYdUXs.mjs → CompareDrawer-DKHgXC5-.mjs} +4 -4
- package/.output/server/_ssr/{ProxyViewerContainer-B-zDOLYE.mjs → ProxyViewerContainer-B41D-2Eo.mjs} +57 -9
- package/.output/server/_ssr/{ReplayDialog-DWeqMA4y.mjs → ReplayDialog-D2piRWb0.mjs} +5 -5
- package/.output/server/_ssr/{RequestAnatomy-TOsrMu9-.mjs → RequestAnatomy-Ce7QdQNP.mjs} +4 -3
- package/.output/server/_ssr/{ResponseView-BuqdPrzm.mjs → ResponseView-D50UPv-r.mjs} +5 -5
- package/.output/server/_ssr/{StreamingChunkSequence-DuzNZkqL.mjs → StreamingChunkSequence-CDlNFS3Z.mjs} +4 -4
- package/.output/server/_ssr/{index-1nCQUt3y.mjs → index-DhAQxjnZ.mjs} +4 -3
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-BL8xhHbi.mjs → json-viewer-BZRjG_f7.mjs} +4 -4
- package/.output/server/_ssr/{router-aCaUgVTW.mjs → router-yP98-Gq-.mjs} +126 -105
- package/.output/server/{_tanstack-start-manifest_v-cBRxvCjb.mjs → _tanstack-start-manifest_v-d4a4xlOi.mjs} +1 -1
- package/.output/server/index.mjs +64 -64
- package/README.md +22 -0
- package/package.json +3 -1
- package/src/cli/detect-tools.ts +1 -0
- package/src/cli/templates/skill-onboard.ts +204 -71
- package/src/cli.ts +164 -39
- package/src/components/ProxyViewerContainer.tsx +52 -0
- package/src/components/proxy-viewer/LogEntryHeader.tsx +1 -0
- package/src/proxy/logFinalizer.ts +7 -3
- package/src/proxy/sessionProcess.ts +14 -7
- package/src/proxy/sessionSupervisor.ts +3 -2
- package/src/proxy/socketTracker.ts +19 -7
- package/styles/globals.css +14 -7
- package/.output/public/assets/_sessionId-BgCVUC6R.js +0 -1
- package/.output/public/assets/index-CWA4S0FO.js +0 -1
package/.output/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ function which(bin) {
|
|
|
20
20
|
const out2 = execFileSync("where", [bin], {
|
|
21
21
|
encoding: "utf8",
|
|
22
22
|
timeout: 3e3,
|
|
23
|
+
windowsHide: true,
|
|
23
24
|
stdio: ["ignore", "pipe", "ignore"]
|
|
24
25
|
});
|
|
25
26
|
const first = out2.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
@@ -133,29 +134,117 @@ ${detectedSummary || " (no known AI tool detected \u2014 the user can still use
|
|
|
133
134
|
|
|
134
135
|
Default proxy port: \`${port}\` (override with \`PORT=<n> llm-inspector\` or \`--port <n>\`).
|
|
135
136
|
|
|
137
|
+
> **PAUSE protocol.** Every \`**PAUSE**\` marker in this skill is a real stop.
|
|
138
|
+
> Use the \`AskUserQuestion\` tool to actually wait for the user before
|
|
139
|
+
> continuing. Do not stream past a PAUSE based on context \u2014 the user has
|
|
140
|
+
> not seen your output yet. Each PAUSE in the body below includes a sample
|
|
141
|
+
> question you can adapt.
|
|
142
|
+
|
|
136
143
|
---
|
|
137
144
|
|
|
138
|
-
##
|
|
145
|
+
## Phase 0: Idempotency check
|
|
146
|
+
|
|
147
|
+
**EXPLAIN:** "Before we do anything, let me see what's already set up. If some of the steps are already done, we can skip them."
|
|
148
|
+
|
|
149
|
+
**DO:** Probe the three pieces of state this skill touches. Use targeted checks \u2014 do **not** read large JSON files into the conversation.
|
|
139
150
|
|
|
140
|
-
|
|
151
|
+
\`\`\`bash
|
|
152
|
+
# 1. Is the proxy already up?
|
|
153
|
+
curl -fsS "http://localhost:${port}/api/health" 2>/dev/null && echo "PROXY: up" || echo "PROXY: down"
|
|
154
|
+
|
|
155
|
+
# 2. Does the config have a real provider key?
|
|
156
|
+
CFG="$HOME/.llm-inspector/config.json"
|
|
157
|
+
if [ -f "$CFG" ]; then
|
|
158
|
+
if grep -qE '"apiKey"[[:space:]]*:[[:space:]]*"(sk-[^"]+|REPLACE|REPLACE_)' "$CFG" 2>/dev/null; then
|
|
159
|
+
echo "CONFIG: has key (no REPLACE placeholder)"
|
|
160
|
+
else
|
|
161
|
+
echo "CONFIG: missing or has placeholder key"
|
|
162
|
+
fi
|
|
163
|
+
else
|
|
164
|
+
echo "CONFIG: file does not exist"
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# 3. Is the MCP server already wired? (project .mcp.json wins)
|
|
168
|
+
PROJ_MCP=".mcp.json"
|
|
169
|
+
HOME_MCP="$HOME/.claude.json"
|
|
170
|
+
if [ -f "$PROJ_MCP" ] && grep -q '"llm-inspector"' "$PROJ_MCP"; then
|
|
171
|
+
echo "MCP: wired in $PROJ_MCP"
|
|
172
|
+
elif [ -f "$HOME_MCP" ] && grep -q '"llm-inspector"' "$HOME_MCP"; then
|
|
173
|
+
echo "MCP: wired in $HOME_MCP"
|
|
174
|
+
else
|
|
175
|
+
echo "MCP: not wired"
|
|
176
|
+
fi
|
|
177
|
+
\`\`\`
|
|
178
|
+
|
|
179
|
+
\`\`\`powershell
|
|
180
|
+
# Windows PowerShell \u2014 single-quoted so $env: expands correctly
|
|
181
|
+
$port = ${port}
|
|
182
|
+
$cfg = Join-Path $env:USERPROFILE '.llm-inspector/config.json'
|
|
183
|
+
|
|
184
|
+
# 1. Is the proxy already up?
|
|
185
|
+
try {
|
|
186
|
+
$null = Invoke-RestMethod -Uri "http://localhost:$port/api/health" -TimeoutSec 2 -ErrorAction Stop
|
|
187
|
+
Write-Host 'PROXY: up'
|
|
188
|
+
} catch {
|
|
189
|
+
Write-Host 'PROXY: down'
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# 2. Does the config have a real provider key?
|
|
193
|
+
if (Test-Path $cfg) {
|
|
194
|
+
$content = Get-Content $cfg -Raw
|
|
195
|
+
if ($content -match '"apiKey"[[:space:]]*:[[:space:]]*"(sk-[^"]+)"') {
|
|
196
|
+
Write-Host 'CONFIG: has key'
|
|
197
|
+
} elseif ($content -match 'REPLACE') {
|
|
198
|
+
Write-Host 'CONFIG: has placeholder key'
|
|
199
|
+
} else {
|
|
200
|
+
Write-Host 'CONFIG: missing key'
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
Write-Host 'CONFIG: file does not exist'
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# 3. Is the MCP server already wired? (project .mcp.json wins)
|
|
207
|
+
$projMcp = Join-Path (Get-Location) '.mcp.json'
|
|
208
|
+
$homeMcp = Join-Path $env:USERPROFILE '.claude.json'
|
|
209
|
+
if ((Test-Path $projMcp) -and (Select-String -Path $projMcp -Pattern 'llm-inspector' -Quiet)) {
|
|
210
|
+
Write-Host "MCP: wired in $projMcp"
|
|
211
|
+
} elseif ((Test-Path $homeMcp) -and (Select-String -Path $homeMcp -Pattern 'llm-inspector' -Quiet)) {
|
|
212
|
+
Write-Host "MCP: wired in $homeMcp"
|
|
213
|
+
} else {
|
|
214
|
+
Write-Host 'MCP: not wired'
|
|
215
|
+
}
|
|
216
|
+
\`\`\`
|
|
217
|
+
|
|
218
|
+
**DO:** Summarize the three checks in one line, then use \`AskUserQuestion\` to ask whether to skip the corresponding phases.
|
|
219
|
+
|
|
220
|
+
> **PAUSE** \u2014 call \`AskUserQuestion\` with:
|
|
221
|
+
> - header: \`Skip done\`
|
|
222
|
+
> - question: \`Proxy/CONFIG/MCP state: <summary>. Skip the phases that are already done?\`
|
|
223
|
+
> - options: \`["Yes, skip what's done", "No, walk me through everything again"]\`
|
|
224
|
+
> Wait for the answer before moving to Preflight.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Preflight
|
|
141
229
|
|
|
142
|
-
**EXPLAIN:** "
|
|
230
|
+
**EXPLAIN:** "Quick env sanity check \u2014 make sure Node and Claude Code are present."
|
|
143
231
|
|
|
144
|
-
**DO:** Run the platform-appropriate commands below.
|
|
232
|
+
**DO:** Run the platform-appropriate commands below.
|
|
145
233
|
|
|
146
234
|
\`\`\`bash
|
|
147
235
|
# Unix / macOS / WSL
|
|
148
|
-
node --version
|
|
149
|
-
|
|
236
|
+
node --version # expect >= 18
|
|
237
|
+
command -v claude >/dev/null && echo "claude-code: present" || echo "claude-code: not detected"
|
|
150
238
|
\`\`\`
|
|
151
239
|
|
|
152
240
|
\`\`\`powershell
|
|
153
|
-
# Windows PowerShell
|
|
154
|
-
node --version
|
|
155
|
-
|
|
241
|
+
# Windows PowerShell \u2014 single-quoted so $env: expands correctly
|
|
242
|
+
node --version # expect >= 18
|
|
243
|
+
$null = Get-Command claude -ErrorAction SilentlyContinue
|
|
244
|
+
if ($null) { Write-Host 'claude-code: present' } else { Write-Host 'claude-code: not detected' }
|
|
156
245
|
\`\`\`
|
|
157
246
|
|
|
158
|
-
**PAUSE** \u2014 if Node is older than 18, ask the user to install a newer version (https://nodejs.org) before continuing.
|
|
247
|
+
> **PAUSE** \u2014 if Node is older than 18, ask the user to install a newer version (https://nodejs.org) before continuing. Use \`AskUserQuestion\` with header \`Node version\`.
|
|
159
248
|
|
|
160
249
|
---
|
|
161
250
|
|
|
@@ -178,7 +267,7 @@ llm-inspector is a transparent HTTP proxy + Web UI for AI coding tools. Point yo
|
|
|
178
267
|
Ready? Let's start with the provider.
|
|
179
268
|
\`\`\`
|
|
180
269
|
|
|
181
|
-
**PAUSE** \u2014
|
|
270
|
+
> **PAUSE** \u2014 use \`AskUserQuestion\` with header \`Ready?\` and options \`["Yes, let's go", "Wait, I have a question"]\`. Wait for the user before continuing.
|
|
182
271
|
|
|
183
272
|
---
|
|
184
273
|
|
|
@@ -186,47 +275,94 @@ Ready? Let's start with the provider.
|
|
|
186
275
|
|
|
187
276
|
**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
277
|
|
|
189
|
-
**DO:**
|
|
278
|
+
**DO:** First, re-check whether the config already has a real key (Phase 0 may have raced with a manual edit). Use the same \`grep -qE '"apiKey":"sk-' "$HOME/.llm-inspector/config.json"\` (bash) or \`Select-String\` (PowerShell) check from Phase 0. If a real key is present, skip the rest of this phase.
|
|
190
279
|
|
|
191
|
-
|
|
280
|
+
**DO:** If no real key, ask the user for the provider type and API key via \`AskUserQuestion\`. The question should be a free-form text field (no fixed options) \u2014 the API key is a secret, so don't echo it back in the question UI.
|
|
281
|
+
|
|
282
|
+
\`\`\`bash
|
|
283
|
+
# After collecting the key, write the config
|
|
284
|
+
mkdir -p "$HOME/.llm-inspector"
|
|
285
|
+
cat > "$HOME/.llm-inspector/config.json" <<'JSON'
|
|
192
286
|
{
|
|
193
287
|
"providers": [
|
|
194
288
|
{
|
|
195
289
|
"id": "anthropic",
|
|
196
290
|
"type": "anthropic",
|
|
197
|
-
"apiKey": "
|
|
291
|
+
"apiKey": "REPLACE_ME_BEFORE_WRITING",
|
|
198
292
|
"baseUrl": "https://api.anthropic.com"
|
|
199
293
|
}
|
|
200
294
|
]
|
|
201
295
|
}
|
|
296
|
+
JSON
|
|
297
|
+
# Then patch the apiKey with the user-provided value (use jq if available)
|
|
202
298
|
\`\`\`
|
|
203
299
|
|
|
204
|
-
|
|
300
|
+
\`\`\`powershell
|
|
301
|
+
# Windows PowerShell \u2014 single-quoted so $env: expands correctly
|
|
302
|
+
$dir = Join-Path $env:USERPROFILE '.llm-inspector'
|
|
303
|
+
$file = Join-Path $dir 'config.json'
|
|
304
|
+
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
|
305
|
+
@'
|
|
306
|
+
{
|
|
307
|
+
"providers": [
|
|
308
|
+
{
|
|
309
|
+
"id": "anthropic",
|
|
310
|
+
"type": "anthropic",
|
|
311
|
+
"apiKey": "REPLACE_ME_BEFORE_WRITING",
|
|
312
|
+
"baseUrl": "https://api.anthropic.com"
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
'@ | Set-Content -Path $file -Encoding UTF8
|
|
317
|
+
# Then patch the apiKey with the user-provided value
|
|
318
|
+
\`\`\`
|
|
319
|
+
|
|
320
|
+
**DO:** Patch the placeholder with the actual key using \`jq\` (preferred) or a simple \`sed\`. Then read the file back to confirm the key is no longer \`REPLACE_ME_BEFORE_WRITING\`.
|
|
321
|
+
|
|
322
|
+
**DO:** If the user declines to provide a key in the AskUserQuestion (selects "Skip for now"), do **not** write a placeholder config. Tell the user that Phase 5 (First capture) will be skipped, and that they can re-run the skill after adding a key.
|
|
205
323
|
|
|
206
|
-
**PAUSE** \u2014
|
|
324
|
+
> **PAUSE** \u2014 use \`AskUserQuestion\` with header \`Provider key\` and options \`["Key is in, continue", "Skip for now, I'll add it later"]\`. Wait for the answer.
|
|
207
325
|
|
|
208
326
|
---
|
|
209
327
|
|
|
210
328
|
## Phase 3: Start proxy
|
|
211
329
|
|
|
212
|
-
**EXPLAIN:** "Time to start the proxy. It binds to port ${port} by default,
|
|
330
|
+
**EXPLAIN:** "Time to start the proxy. It binds to port ${port} by default, reuses an already-running healthy llm-inspector, and prints the URL. Use \`--force-restart\` only when you intentionally want to replace the existing process."
|
|
213
331
|
|
|
214
|
-
**DO:**
|
|
332
|
+
**DO:** Skip this phase entirely if the Phase 0 health check already reported \`PROXY: up\` and the user opted to skip done phases.
|
|
333
|
+
|
|
334
|
+
**DO:** Otherwise, start the proxy with the explicit \`--background --no-open\` flags. On Windows, resolve the npm shim first and launch it through \`Start-Process -WindowStyle Hidden\` so setup does not flash a command window.
|
|
215
335
|
|
|
216
336
|
\`\`\`bash
|
|
217
337
|
# Unix / macOS / WSL
|
|
218
|
-
|
|
338
|
+
llm-inspector --background --no-open > /tmp/llm-inspector.log 2>&1
|
|
219
339
|
\`\`\`
|
|
220
340
|
|
|
221
341
|
\`\`\`powershell
|
|
222
|
-
# Windows PowerShell
|
|
223
|
-
|
|
342
|
+
# Windows PowerShell \u2014 single-quoted so $env: expands correctly.
|
|
343
|
+
# Locate the binary on PATH first (works for npm, pnpm, yarn, volta, fnm).
|
|
344
|
+
# If not on PATH, fall back to the common npm global shim at $env:APPDATA.
|
|
345
|
+
# As a last resort, let cmd /c resolve it through PATHEXT.
|
|
346
|
+
$log = Join-Path $env:TEMP 'llm-inspector.log'
|
|
347
|
+
$err = Join-Path $env:TEMP 'llm-inspector.err.log'
|
|
348
|
+
$found = Get-Command llm-inspector -ErrorAction SilentlyContinue
|
|
349
|
+
if ($found) {
|
|
350
|
+
$shim = $found.Source
|
|
351
|
+
$args = '--background','--no-open'
|
|
352
|
+
} elseif (Test-Path (Join-Path $env:APPDATA 'npm/llm-inspector.cmd')) {
|
|
353
|
+
$shim = Join-Path $env:APPDATA 'npm/llm-inspector.cmd'
|
|
354
|
+
$args = '--background','--no-open'
|
|
355
|
+
} else {
|
|
356
|
+
# bin not on PATH and not at the default npm prefix \u2014 let cmd resolve it
|
|
357
|
+
$shim = 'cmd.exe'
|
|
358
|
+
$args = '/c','llm-inspector','--background','--no-open'
|
|
359
|
+
}
|
|
360
|
+
Start-Process -FilePath $shim -ArgumentList $args -RedirectStandardOutput $log -RedirectStandardError $err -WindowStyle Hidden
|
|
224
361
|
\`\`\`
|
|
225
362
|
|
|
226
363
|
Then wait for the port to be ready:
|
|
227
364
|
|
|
228
365
|
\`\`\`bash
|
|
229
|
-
# Wait up to 10s for the port to come up
|
|
230
366
|
for i in $(seq 1 20); do
|
|
231
367
|
curl -fsS "http://localhost:${port}/api/health" >/dev/null 2>&1 && echo "ready" && break
|
|
232
368
|
sleep 0.5
|
|
@@ -239,7 +375,7 @@ done
|
|
|
239
375
|
curl -sS "http://localhost:${port}/api/health"
|
|
240
376
|
\`\`\`
|
|
241
377
|
|
|
242
|
-
**PAUSE** \u2014 if the health check fails, show the user the log file (\`/tmp/llm-inspector.log\` or \`%TEMP
|
|
378
|
+
> **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. Use \`AskUserQuestion\` with header \`Proxy up?\` and options \`["Yes, proxy is up", "No, I see an error in the log"]\`. Wait for the answer.
|
|
243
379
|
|
|
244
380
|
---
|
|
245
381
|
|
|
@@ -267,18 +403,22 @@ mimo
|
|
|
267
403
|
|
|
268
404
|
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
405
|
|
|
270
|
-
**PAUSE** \u2014
|
|
406
|
+
> **PAUSE** \u2014 use \`AskUserQuestion\` with header \`Tool wired?\` and options \`["Yes, env var is set, claude is running", "No, I'm going to use the curl test instead"]\`. Wait for the answer.
|
|
271
407
|
|
|
272
408
|
---
|
|
273
409
|
|
|
274
410
|
## Phase 4.5: Wire MCP server
|
|
275
411
|
|
|
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.
|
|
412
|
+
**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."
|
|
277
413
|
|
|
278
|
-
**DO:**
|
|
414
|
+
**DO:** Skip this phase if Phase 0 reported \`MCP: wired in <path>\` and the user opted to skip done phases.
|
|
415
|
+
|
|
416
|
+
**DO:** Otherwise, check the project-level \`.mcp.json\` first (preferred \u2014 modern Claude Code convention), then fall back to \`~/.claude.json\`. Use the \`Read\` tool to inspect; do **not** \`cat\` a 40 KB file into the conversation.
|
|
417
|
+
|
|
418
|
+
If neither has an \`llm-inspector\` entry, add one. The simplest path is to write to project \`.mcp.json\` (create it if missing):
|
|
279
419
|
|
|
280
420
|
\`\`\`json
|
|
281
|
-
//
|
|
421
|
+
// .mcp.json (project root)
|
|
282
422
|
{
|
|
283
423
|
"mcpServers": {
|
|
284
424
|
"llm-inspector": {
|
|
@@ -289,26 +429,7 @@ For a tool that wasn't auto-detected, fall through to the generic curl test in t
|
|
|
289
429
|
}
|
|
290
430
|
\`\`\`
|
|
291
431
|
|
|
292
|
-
If \`mcpServers\` already exists
|
|
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
|
-
\`\`\`
|
|
432
|
+
If \`mcpServers\` already exists in \`.mcp.json\`, merge the \`llm-inspector\` key into it via the \`Edit\` tool \u2014 do not overwrite other entries. If you can't create a project \`.mcp.json\` (no project root, permission, etc.), fall back to merging into \`~/.claude.json\` using the same \`Read\`/\`Edit\` pattern.
|
|
312
433
|
|
|
313
434
|
**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
435
|
|
|
@@ -325,41 +446,33 @@ curl -sS -X POST "http://localhost:${port}/api/mcp" \\
|
|
|
325
446
|
"capabilities": {},
|
|
326
447
|
"clientInfo": { "name": "onboard-check", "version": "0" }
|
|
327
448
|
}
|
|
328
|
-
}'
|
|
329
|
-
# expect: HTTP 200, body contains "result" with serverInfo.name = "llm-inspector"
|
|
449
|
+
}' | grep -o '"name":"llm-inspector"' && echo "handshake OK"
|
|
330
450
|
\`\`\`
|
|
331
451
|
|
|
332
|
-
|
|
452
|
+
The \`grep -o '"name":"llm-inspector"'\` extracts only the serverInfo name \u2014 do not dump the full response. If the server returns session IDs, store the \`mcp-session-id\` header from the first response and use it for the follow-up \`tools/list\` call.
|
|
333
453
|
|
|
334
|
-
|
|
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.
|
|
454
|
+
> **PAUSE** \u2014 use \`AskUserQuestion\` with header \`MCP OK?\` and options \`["Yes, handshake returned 200", "No, the call failed"]\`. Wait for the answer.
|
|
345
455
|
|
|
346
456
|
---
|
|
347
457
|
|
|
348
458
|
## Phase 5: First capture
|
|
349
459
|
|
|
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.
|
|
460
|
+
**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."
|
|
351
461
|
|
|
352
|
-
**DO:**
|
|
462
|
+
**DO:** First, re-check the config. If the \`apiKey\` is still a \`REPLACE_ME_BEFORE_WRITING\` placeholder (user opted out in Phase 2), **skip the capture test** and tell the user to fill in their key and re-run the skill. A 401 from the upstream is fine if they did provide a real key \u2014 the proxy will still log the request.
|
|
463
|
+
|
|
464
|
+
Fire a minimal Anthropic-format request through the proxy:
|
|
353
465
|
|
|
354
466
|
\`\`\`bash
|
|
355
467
|
curl -sS -X POST "http://localhost:${port}/proxy/v1/messages" \\
|
|
356
468
|
-H "Content-Type: application/json" \\
|
|
357
469
|
-H "anthropic-version: 2023-06-01" \\
|
|
358
470
|
-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"}]}'
|
|
471
|
+
-d '{"model":"claude-3-5-sonnet-20241022","max_tokens":1,"messages":[{"role":"user","content":"ping"}]}' \\
|
|
472
|
+
-o /tmp/llm-inspector-capture.json -w 'STATUS:%{http_code}\\n'
|
|
360
473
|
\`\`\`
|
|
361
474
|
|
|
362
|
-
**DO:** Poll the logs API for up to 5 seconds. A 200 with at least one entry means
|
|
475
|
+
**DO:** Poll the logs API for up to 5 seconds. A 200 with at least one entry means the request reached the proxy:
|
|
363
476
|
|
|
364
477
|
\`\`\`bash
|
|
365
478
|
for i in $(seq 1 10); do
|
|
@@ -367,13 +480,24 @@ for i in $(seq 1 10); do
|
|
|
367
480
|
count=$(echo "$resp" | grep -o '"total":[0-9]*' | head -1 | grep -o '[0-9]*$')
|
|
368
481
|
if [ "\${count:-0}" -ge 1 ]; then
|
|
369
482
|
echo "captured"
|
|
483
|
+
echo "$resp" | head -c 400
|
|
370
484
|
break
|
|
371
485
|
fi
|
|
372
486
|
sleep 0.5
|
|
373
487
|
done
|
|
374
488
|
\`\`\`
|
|
375
489
|
|
|
376
|
-
**
|
|
490
|
+
**DO:** Diagnose the response based on the actual status and body. **Do not** default to "auth failure" for every 4xx.
|
|
491
|
+
|
|
492
|
+
| Status | Body hint | Meaning |
|
|
493
|
+
|--------|-----------|---------|
|
|
494
|
+
| 200 | normal | Real success \u2014 the upstream returned data |
|
|
495
|
+
| 401 | \`"unauthorized"\` or similar | Upstream rejected the key (expected with a test key) |
|
|
496
|
+
| 403 | \`"Request not allowed"\` | **Proxy's allowlist** \u2014 not an auth failure, the proxy rejected the model/config. Show the user the proxy log. |
|
|
497
|
+
| 403 | other text | Could be upstream ACL \u2014 different problem |
|
|
498
|
+
| 5xx | anything | Upstream network error |
|
|
499
|
+
|
|
500
|
+
> **PAUSE** \u2014 use \`AskUserQuestion\` with header \`Captured?\` and options matching the diagnosis above. Wait for the answer.
|
|
377
501
|
|
|
378
502
|
---
|
|
379
503
|
|
|
@@ -390,14 +514,14 @@ done
|
|
|
390
514
|
# Unix / macOS
|
|
391
515
|
lsof -ti:${port} | xargs -r kill -9
|
|
392
516
|
|
|
393
|
-
# Windows PowerShell
|
|
394
|
-
Get-NetTCPConnection -LocalPort $
|
|
517
|
+
# Windows PowerShell \u2014 single-quoted so $env: expands correctly
|
|
518
|
+
Get-NetTCPConnection -LocalPort $port | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }
|
|
395
519
|
\`\`\`
|
|
396
520
|
|
|
397
521
|
- **Re-run onboard**: \`llm-inspector onboard --force\` refreshes this skill.
|
|
398
522
|
- **Full docs**: see the project README (linked from the Web UI footer).
|
|
399
523
|
|
|
400
|
-
**PAUSE** \u2014
|
|
524
|
+
> **PAUSE** \u2014 use \`AskUserQuestion\` with header \`All set?\` and options \`["All set, I'm done", "Wait, I want to revisit a phase"]\`. Wait for the answer.
|
|
401
525
|
|
|
402
526
|
You're done. Happy inspecting.
|
|
403
527
|
`;
|
|
@@ -407,6 +531,7 @@ var init_skill_onboard = __esm({
|
|
|
407
531
|
"src/cli/templates/skill-onboard.ts"() {
|
|
408
532
|
"use strict";
|
|
409
533
|
REQUIRED_PHASE_HEADINGS = [
|
|
534
|
+
"Phase 0: Idempotency check",
|
|
410
535
|
"Preflight",
|
|
411
536
|
"Phase 1: Welcome",
|
|
412
537
|
"Phase 2: Provider setup",
|
|
@@ -629,23 +754,105 @@ var init_onboard = __esm({
|
|
|
629
754
|
|
|
630
755
|
// src/cli.ts
|
|
631
756
|
import { spawn, execSync } from "node:child_process";
|
|
757
|
+
import { createConnection } from "node:net";
|
|
632
758
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
633
759
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
634
760
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
635
761
|
var __dirname2 = dirname2(__filename2);
|
|
636
762
|
var DEFAULT_PORT2 = 25947;
|
|
763
|
+
var LOCAL_PROBE_TIMEOUT_MS = 2e3;
|
|
637
764
|
var subcommand = process.argv[2];
|
|
638
765
|
if (subcommand === "onboard") {
|
|
639
766
|
const { runOnboard: runOnboard2 } = await Promise.resolve().then(() => (init_onboard(), onboard_exports));
|
|
640
767
|
const code = await runOnboard2(process.argv.slice(3));
|
|
641
768
|
process.exit(code);
|
|
642
769
|
}
|
|
643
|
-
runStart(process.argv.slice(2));
|
|
644
|
-
function
|
|
770
|
+
await runStart(process.argv.slice(2));
|
|
771
|
+
async function isInspectorHealthy(port) {
|
|
772
|
+
const controller = new AbortController();
|
|
773
|
+
const timeout = setTimeout(() => controller.abort(), LOCAL_PROBE_TIMEOUT_MS);
|
|
774
|
+
try {
|
|
775
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
776
|
+
cache: "no-store",
|
|
777
|
+
signal: controller.signal
|
|
778
|
+
});
|
|
779
|
+
return response.ok;
|
|
780
|
+
} catch {
|
|
781
|
+
return false;
|
|
782
|
+
} finally {
|
|
783
|
+
clearTimeout(timeout);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function isPortAcceptingConnections(port) {
|
|
787
|
+
return new Promise((resolve) => {
|
|
788
|
+
const socket = createConnection({ host: "127.0.0.1", port });
|
|
789
|
+
const finish = (value) => {
|
|
790
|
+
socket.removeAllListeners();
|
|
791
|
+
socket.destroy();
|
|
792
|
+
resolve(value);
|
|
793
|
+
};
|
|
794
|
+
socket.setTimeout(LOCAL_PROBE_TIMEOUT_MS);
|
|
795
|
+
socket.once("connect", () => finish(true));
|
|
796
|
+
socket.once("timeout", () => finish(false));
|
|
797
|
+
socket.once("error", () => finish(false));
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
function sleep(ms) {
|
|
801
|
+
return new Promise((resolve) => {
|
|
802
|
+
setTimeout(resolve, ms);
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
async function waitForInspectorHealthy(port, timeoutMs) {
|
|
806
|
+
const start = Date.now();
|
|
807
|
+
while (Date.now() - start < timeoutMs) {
|
|
808
|
+
if (await isInspectorHealthy(port)) return true;
|
|
809
|
+
await sleep(250);
|
|
810
|
+
}
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
function openBrowser(targetUrl) {
|
|
814
|
+
let command;
|
|
815
|
+
switch (process.platform) {
|
|
816
|
+
case "darwin":
|
|
817
|
+
command = ["open", targetUrl];
|
|
818
|
+
break;
|
|
819
|
+
case "linux":
|
|
820
|
+
command = ["xdg-open", targetUrl];
|
|
821
|
+
break;
|
|
822
|
+
case "win32":
|
|
823
|
+
command = ["cmd", "/c", "start", "", targetUrl];
|
|
824
|
+
break;
|
|
825
|
+
default:
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
if (command === void 0) return;
|
|
829
|
+
const [bin, ...cmdArgs] = command;
|
|
830
|
+
if (bin === void 0) return;
|
|
831
|
+
const browserProcess = spawn(bin, cmdArgs, {
|
|
832
|
+
stdio: "ignore",
|
|
833
|
+
detached: true,
|
|
834
|
+
windowsHide: true
|
|
835
|
+
});
|
|
836
|
+
browserProcess.unref();
|
|
837
|
+
}
|
|
838
|
+
function waitForProcessExit(child) {
|
|
839
|
+
return new Promise((resolve) => {
|
|
840
|
+
child.once("exit", (code) => {
|
|
841
|
+
resolve(code ?? 1);
|
|
842
|
+
});
|
|
843
|
+
child.once("error", () => {
|
|
844
|
+
resolve(1);
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
async function runStart(args) {
|
|
645
849
|
const envPort = process.env["PORT"];
|
|
646
850
|
const portDefault = envPort !== void 0 ? Number(envPort) : DEFAULT_PORT2;
|
|
647
851
|
let port = portDefault;
|
|
648
852
|
let open = true;
|
|
853
|
+
let openWasSpecified = false;
|
|
854
|
+
let background = false;
|
|
855
|
+
let forceRestart = false;
|
|
649
856
|
let configDir;
|
|
650
857
|
let providersJson;
|
|
651
858
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -658,9 +865,18 @@ function runStart(args) {
|
|
|
658
865
|
break;
|
|
659
866
|
case "--no-open":
|
|
660
867
|
open = false;
|
|
868
|
+
openWasSpecified = true;
|
|
661
869
|
break;
|
|
662
870
|
case "--open":
|
|
663
871
|
open = true;
|
|
872
|
+
openWasSpecified = true;
|
|
873
|
+
break;
|
|
874
|
+
case "--force-restart":
|
|
875
|
+
case "--restart":
|
|
876
|
+
forceRestart = true;
|
|
877
|
+
break;
|
|
878
|
+
case "--background":
|
|
879
|
+
background = true;
|
|
664
880
|
break;
|
|
665
881
|
case "--config-dir":
|
|
666
882
|
configDir = args[i + 1];
|
|
@@ -674,6 +890,11 @@ function runStart(args) {
|
|
|
674
890
|
break;
|
|
675
891
|
}
|
|
676
892
|
}
|
|
893
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
894
|
+
console.error(`Invalid port: ${String(port)}. Use --port <1-65535>.`);
|
|
895
|
+
process.exitCode = 1;
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
677
898
|
function killProcessOnPort(targetPort) {
|
|
678
899
|
const platform = process.platform;
|
|
679
900
|
try {
|
|
@@ -681,7 +902,8 @@ function runStart(args) {
|
|
|
681
902
|
if (platform === "win32") {
|
|
682
903
|
const output = execSync(`netstat -ano | findstr :${targetPort}`, {
|
|
683
904
|
encoding: "utf8",
|
|
684
|
-
timeout: 5e3
|
|
905
|
+
timeout: 5e3,
|
|
906
|
+
windowsHide: true
|
|
685
907
|
});
|
|
686
908
|
const lines = output.trim().split("\n");
|
|
687
909
|
for (const line of lines) {
|
|
@@ -700,8 +922,12 @@ function runStart(args) {
|
|
|
700
922
|
pids = [...new Set(pids)];
|
|
701
923
|
for (const pid of pids) {
|
|
702
924
|
try {
|
|
703
|
-
console.log(`Killing process ${pid} on port ${
|
|
704
|
-
execSync(`taskkill /PID ${pid} /F`, {
|
|
925
|
+
console.log(`Killing process ${pid} on port ${targetPort}...`);
|
|
926
|
+
execSync(`taskkill /PID ${pid} /F`, {
|
|
927
|
+
encoding: "utf8",
|
|
928
|
+
timeout: 5e3,
|
|
929
|
+
windowsHide: true
|
|
930
|
+
});
|
|
705
931
|
} catch {
|
|
706
932
|
}
|
|
707
933
|
}
|
|
@@ -717,7 +943,7 @@ function runStart(args) {
|
|
|
717
943
|
pids = [...new Set(pids)];
|
|
718
944
|
for (const pid of pids) {
|
|
719
945
|
try {
|
|
720
|
-
console.log(`Killing process ${pid} on port ${
|
|
946
|
+
console.log(`Killing process ${pid} on port ${targetPort}...`);
|
|
721
947
|
execSync(`kill -9 ${pid}`, { encoding: "utf8", timeout: 5e3 });
|
|
722
948
|
} catch {
|
|
723
949
|
}
|
|
@@ -727,8 +953,24 @@ function runStart(args) {
|
|
|
727
953
|
}
|
|
728
954
|
}
|
|
729
955
|
process.env["PORT"] = String(port);
|
|
730
|
-
killProcessOnPort(port);
|
|
731
956
|
const url = `http://localhost:${port}`;
|
|
957
|
+
if (!forceRestart && await isInspectorHealthy(port)) {
|
|
958
|
+
console.log(`llm-inspector is already running at ${url}`);
|
|
959
|
+
console.log(`Use --force-restart to restart the existing instance.`);
|
|
960
|
+
if (open && openWasSpecified) {
|
|
961
|
+
openBrowser(url);
|
|
962
|
+
}
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (!forceRestart && await isPortAcceptingConnections(port)) {
|
|
966
|
+
console.error(`Port ${port} is already in use, but it is not a healthy llm-inspector.`);
|
|
967
|
+
console.error(`Stop that process, choose --port <n>, or re-run with --force-restart.`);
|
|
968
|
+
process.exitCode = 1;
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (forceRestart) {
|
|
972
|
+
killProcessOnPort(port);
|
|
973
|
+
}
|
|
732
974
|
console.log(`Server running at ${url}`);
|
|
733
975
|
console.log(` Proxy: ${url}/proxy`);
|
|
734
976
|
console.log(``);
|
|
@@ -746,26 +988,6 @@ function runStart(args) {
|
|
|
746
988
|
console.log(
|
|
747
989
|
` Example: ROUTES='{"claude-":"https://api.anthropic.com","MiniMax":"https://api.minimaxi.com/anthropic"}'`
|
|
748
990
|
);
|
|
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
991
|
if (open) {
|
|
770
992
|
openBrowser(url);
|
|
771
993
|
}
|
|
@@ -774,8 +996,11 @@ function runStart(args) {
|
|
|
774
996
|
const serverEnv = { ...process.env };
|
|
775
997
|
if (configDir !== void 0) {
|
|
776
998
|
let resolvedPath = join3(configDir, "config.json");
|
|
777
|
-
|
|
778
|
-
|
|
999
|
+
const msysMatch = /^\\([a-z])\\(.*)$/i.exec(resolvedPath);
|
|
1000
|
+
if (msysMatch !== null) {
|
|
1001
|
+
const drive = (msysMatch[1] ?? "").toUpperCase();
|
|
1002
|
+
const rest = msysMatch[2] ?? "";
|
|
1003
|
+
resolvedPath = `${drive}:\\${rest}`;
|
|
779
1004
|
}
|
|
780
1005
|
serverEnv["LLM_INSPECTOR_CONFIG_PATH"] = resolvedPath;
|
|
781
1006
|
}
|
|
@@ -783,9 +1008,20 @@ function runStart(args) {
|
|
|
783
1008
|
serverEnv["LLM_INSPECTOR_PROVIDERS_JSON"] = providersJson;
|
|
784
1009
|
}
|
|
785
1010
|
const serverProcess = spawn(process.execPath, [serverPath], {
|
|
786
|
-
stdio: ["ignore", "
|
|
787
|
-
detached:
|
|
788
|
-
env: serverEnv
|
|
1011
|
+
stdio: background ? ["ignore", "ignore", "ignore"] : "inherit",
|
|
1012
|
+
detached: background,
|
|
1013
|
+
env: serverEnv,
|
|
1014
|
+
windowsHide: background
|
|
789
1015
|
});
|
|
790
|
-
|
|
1016
|
+
if (background) {
|
|
1017
|
+
serverProcess.unref();
|
|
1018
|
+
if (await waitForInspectorHealthy(port, 5e3)) {
|
|
1019
|
+
console.log(`llm-inspector background server is ready at ${url}`);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
console.error(`llm-inspector background server did not become ready at ${url}.`);
|
|
1023
|
+
process.exitCode = 1;
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
process.exitCode = await waitForProcessExit(serverProcess);
|
|
791
1027
|
}
|