llm-cli-gateway 1.17.0 → 1.17.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/CHANGELOG.md +28 -0
- package/README.md +16 -19
- package/dist/cache-stats.d.ts +47 -0
- package/dist/cache-stats.js +85 -2
- package/dist/config.js +1 -1
- package/dist/doctor.d.ts +22 -1
- package/dist/doctor.js +35 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +123 -39
- package/dist/process-monitor.d.ts +1 -2
- package/dist/process-monitor.js +7 -7
- package/dist/prompt-parts.d.ts +1 -1
- package/dist/prompt-parts.js +1 -1
- package/dist/provider-login-guidance.js +5 -5
- package/dist/provider-status.js +0 -4
- package/dist/request-helpers.d.ts +28 -26
- package/dist/request-helpers.js +50 -43
- package/dist/session-manager.js +1 -1
- package/dist/stream-json-parser.js +30 -15
- package/dist/upstream-contracts.d.ts +24 -0
- package/dist/upstream-contracts.js +213 -18
- package/dist/validation-tools.js +1 -1
- package/package.json +11 -8
- package/setup/status.schema.json +31 -0
- package/socket.yml +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@ All notable changes to the llm-cli-gateway project.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [1.17.2] - 2026-05-31 — upstream contract compatibility
|
|
8
|
+
|
|
9
|
+
Patch release that keeps the gateway aligned with current provider CLI surfaces
|
|
10
|
+
and fixes the reviewed outstanding-work blockers.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Updated `doctor --json` schema coverage for the top-level upstream contract
|
|
15
|
+
report.
|
|
16
|
+
- Stopped emitting removed Codex CLI flags such as `--ask-for-approval`,
|
|
17
|
+
`--full-auto`, `--search`, and resume-mode `--profile`.
|
|
18
|
+
- Made `upstream:scan -- --probe-installed` compare installed CLI help surfaces
|
|
19
|
+
in offline mode.
|
|
20
|
+
- Updated Grok Build contract metadata, install guidance, and public auth copy
|
|
21
|
+
for current xAI docs.
|
|
22
|
+
|
|
23
|
+
## [1.17.1] - 2026-05-30 — Socket shell-access suppression
|
|
24
|
+
|
|
25
|
+
Patch release updating the package's Socket policy for the reviewed gateway
|
|
26
|
+
process-launching capability.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Suppressed Socket's `shellAccess` alert in `socket.yml` now that the
|
|
31
|
+
child-process surface is documented and release-audited.
|
|
32
|
+
- Updated README Socket-alert wording so reviewers still get the bounded
|
|
33
|
+
shell-access rationale without seeing the same package alert on every release.
|
|
34
|
+
|
|
7
35
|
## [1.17.0] - 2026-05-30 — upstream provider tracking
|
|
8
36
|
|
|
9
37
|
Feature release adding repeatable upstream-provider contract tracking for the
|
package/README.md
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
[](https://github.com/verivus-oss/llm-cli-gateway/actions/workflows/security.yml)
|
|
5
5
|
[](https://scorecard.dev/viewer/?uri=github.com/verivus-oss/llm-cli-gateway)
|
|
6
6
|
[](https://www.npmjs.com/package/llm-cli-gateway)
|
|
7
|
-
[](https://www.npmjs.com/package/llm-cli-gateway)
|
|
8
7
|
[](LICENSE)
|
|
9
8
|
|
|
10
9
|
> _"Without consultation, plans are frustrated, but with many counselors they succeed."_
|
|
@@ -14,7 +13,7 @@ A Model Context Protocol (MCP) gateway for running Claude Code, Codex, Gemini, G
|
|
|
14
13
|
|
|
15
14
|
**Why developers try it:** one local MCP endpoint for cross-LLM validation, multi-agent coding workflows, and repeatable assistant-led setup across five provider CLIs.
|
|
16
15
|
|
|
17
|
-
**Current signals:**
|
|
16
|
+
**Current signals:** CI and security workflows pass on `main`, OpenSSF Scorecard is published, OpenSSF Best Practices is passing, releases use Sigstore signing, and the package is MIT licensed.
|
|
18
17
|
|
|
19
18
|
## Quick Start
|
|
20
19
|
|
|
@@ -55,8 +54,6 @@ The next documentation focus is provider-specific skill and DAG-TOML pairs for e
|
|
|
55
54
|
## Trust & Supply Chain
|
|
56
55
|
|
|
57
56
|
[](https://www.bestpractices.dev/projects/13025)
|
|
58
|
-
[](https://www.npmjs.com/package/llm-cli-gateway)
|
|
59
|
-
[](https://github.com/verivus-oss/llm-cli-gateway/releases)
|
|
60
57
|
[](SECURITY.md#release-signing)
|
|
61
58
|
|
|
62
59
|
- CI runs build, lint, format, tests, package checks, and npm audit.
|
|
@@ -80,7 +77,7 @@ Current personal-appliance artifacts include:
|
|
|
80
77
|
- Machine-readable diagnostics: `npm run doctor`
|
|
81
78
|
- Go bootstrapper: `installer/` with `setup`, `doctor --json`, `start`, `stop`, `status`, `repair`, `upgrade`, `uninstall`, `print-client-config`, and verified bundle download commands.
|
|
82
79
|
- Release packaging: the release workflow builds Linux binaries on the local self-hosted runner, builds Windows/macOS binaries on GitHub-hosted runners, then publishes checksummed platform bundles with the gateway, production dependencies, and a managed Node runtime; see [installer/packaging/README.md](installer/packaging/README.md).
|
|
83
|
-
- Docker Compose fallback: [docker
|
|
80
|
+
- Docker Compose fallback: [docker/personal.compose.yml](docker/personal.compose.yml) + [docker/Dockerfile.personal](docker/Dockerfile.personal) for users who already manage containers.
|
|
84
81
|
- Local setup UI artifact: [setup/ui/index.html](setup/ui/index.html)
|
|
85
82
|
- Provider setup snippets: [setup/providers/](setup/providers/)
|
|
86
83
|
- Cross-validation tools: `validate_with_models`, `second_opinion`, `compare_answers`, `red_team_review`, `consensus_check`, `ask_model`, `synthesize_validation`, `job_status`, and `job_result`.
|
|
@@ -148,8 +145,8 @@ Docker fallback:
|
|
|
148
145
|
|
|
149
146
|
```bash
|
|
150
147
|
LLM_GATEWAY_AUTH_TOKEN=$(openssl rand -hex 32) \
|
|
151
|
-
docker compose -f docker
|
|
152
|
-
docker compose -f docker
|
|
148
|
+
docker compose -f docker/personal.compose.yml up -d
|
|
149
|
+
docker compose -f docker/personal.compose.yml run --rm doctor
|
|
153
150
|
```
|
|
154
151
|
|
|
155
152
|
## Features
|
|
@@ -241,12 +238,12 @@ npm install -g @google/gemini-cli
|
|
|
241
238
|
# Or: https://github.com/google-gemini/gemini-cli
|
|
242
239
|
```
|
|
243
240
|
|
|
244
|
-
### Grok CLI (xAI)
|
|
241
|
+
### Grok Build CLI (xAI)
|
|
245
242
|
|
|
246
243
|
```bash
|
|
247
|
-
|
|
248
|
-
grok login # OAuth flow,
|
|
249
|
-
# Docs: https://docs.x.ai/build/
|
|
244
|
+
curl -fsSL https://x.ai/cli/install.sh | bash
|
|
245
|
+
grok login # OAuth flow; for headless auth, set XAI_API_KEY
|
|
246
|
+
# Docs: https://docs.x.ai/build/overview
|
|
250
247
|
```
|
|
251
248
|
|
|
252
249
|
### Mistral Vibe CLI
|
|
@@ -1176,15 +1173,15 @@ The gateway supports concurrent requests across different CLIs. Each request spa
|
|
|
1176
1173
|
|
|
1177
1174
|
### Socket alerts — context for reviewers
|
|
1178
1175
|
|
|
1179
|
-
If you're vetting `llm-cli-gateway` through [Socket](https://socket.dev/npm/package/llm-cli-gateway) or a similar supply-chain scanner, you'll see
|
|
1176
|
+
If you're vetting `llm-cli-gateway` through [Socket](https://socket.dev/npm/package/llm-cli-gateway) or a similar supply-chain scanner, you'll see behavioural alerts and some dependency-ownership alerts. They are accurate descriptions of what the package does and what it depends on. The reviewed `shellAccess` capability is suppressed in `socket.yml` to avoid a repeat finding on every release; the rationale remains documented here and in the package.
|
|
1180
1177
|
|
|
1181
|
-
| Alert | Where | Why it's bounded
|
|
1182
|
-
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1183
|
-
| **Network access** | `src/http-transport.ts` opens an HTTP MCP transport when started via `npm run start:http`. `src/endpoint-exposure.ts` issues a HEAD probe to verify configured public/tunnel URLs. | The transport binds to `127.0.0.1` by default and requires `LLM_GATEWAY_AUTH_TOKEN` to be set. The default stdio MCP entry point (`npm start`) opens no sockets.
|
|
1184
|
-
| **Shell access** | `src/executor.ts` uses `child_process.spawn(cmd, args, …)` to invoke the underlying LLM CLIs. | `spawn` is called with an argument array and **never** `shell: true`, so there is no shell interpolation path for caller input. The command name is restricted to an allow-list of known CLI binaries (`claude`, `codex`, `gemini`, `grok`, `vibe`).
|
|
1185
|
-
| **Uses eval** | None in our source. Transitive: `@modelcontextprotocol/sdk` → `ajv@8` uses `new Function(...)` in `ajv/dist/compile/index.js` to compile JSON Schema validators. | This is ajv's standard codegen path. Only known schemas (defined in our source and the MCP SDK) flow into it; no caller-supplied data ever reaches the compiled function body.
|
|
1186
|
-
| **better-sqlite3 PRAGMA helper** | Transitive: `better-sqlite3/lib/methods/pragma.js` interpolates its caller-provided `source` into a `PRAGMA ${source}` statement. | We do not call `db.pragma()` from production source. Internal SQLite setup uses fixed literal `db.exec("PRAGMA ...")` statements, and `npm run security:audit` fails the release if production code reintroduces `.pragma()` calls.
|
|
1187
|
-
| **Dependency ownership** | A handful of small transitive packages (e.g. `bindings` via `better-sqlite3`, `media-typer` via `@modelcontextprotocol/sdk`) trip Socket's "unstable ownership" or "obfuscated code" heuristics. | These are pinned, well-known micro-deps in the Node ecosystem with no known issues. We pin direct override versions of `content-type` and `type-is` in `package.json#overrides`. Our previous direct dependency on `toml@3.0.0` (also single-maintainer, last released 2020) was replaced with the actively-maintained `smol-toml` to reduce inherited risk.
|
|
1178
|
+
| Alert | Where | Why it's bounded |
|
|
1179
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1180
|
+
| **Network access** | `src/http-transport.ts` opens an HTTP MCP transport when started via `npm run start:http`. `src/endpoint-exposure.ts` issues a HEAD probe to verify configured public/tunnel URLs. | The transport binds to `127.0.0.1` by default and requires `LLM_GATEWAY_AUTH_TOKEN` to be set. The default stdio MCP entry point (`npm start`) opens no sockets. |
|
|
1181
|
+
| **Shell access** | `src/executor.ts` uses `child_process.spawn(cmd, args, …)` to invoke the underlying LLM CLIs. | `spawn` is called with an argument array and **never** `shell: true`, so there is no shell interpolation path for caller input. The command name is restricted to an allow-list of known CLI binaries (`claude`, `codex`, `gemini`, `grok`, `vibe`). |
|
|
1182
|
+
| **Uses eval** | None in our source. Transitive: `@modelcontextprotocol/sdk` → `ajv@8` uses `new Function(...)` in `ajv/dist/compile/index.js` to compile JSON Schema validators. | This is ajv's standard codegen path. Only known schemas (defined in our source and the MCP SDK) flow into it; no caller-supplied data ever reaches the compiled function body. |
|
|
1183
|
+
| **better-sqlite3 PRAGMA helper** | Transitive: `better-sqlite3/lib/methods/pragma.js` interpolates its caller-provided `source` into a `PRAGMA ${source}` statement. | We do not call `db.pragma()` from production source. Internal SQLite setup uses fixed literal `db.exec("PRAGMA ...")` statements, and `npm run security:audit` fails the release if production code reintroduces `.pragma()` calls. |
|
|
1184
|
+
| **Dependency ownership** | A handful of small transitive packages (e.g. `bindings` via `better-sqlite3`, `media-typer` via `@modelcontextprotocol/sdk`) trip Socket's "unstable ownership" or "obfuscated code" heuristics. | These are pinned, well-known micro-deps in the Node ecosystem with no known issues. We pin direct override versions of `content-type` and `type-is` in `package.json#overrides`. Our previous direct dependency on `toml@3.0.0` (also single-maintainer, last released 2020) was replaced with the actively-maintained `smol-toml` to reduce inherited risk. |
|
|
1188
1185
|
|
|
1189
1186
|
See [`socket.yml`](./socket.yml) for the same context in machine-readable form.
|
|
1190
1187
|
|
package/dist/cache-stats.d.ts
CHANGED
|
@@ -136,3 +136,50 @@ export interface GlobalCacheStatsOpts {
|
|
|
136
136
|
lastNHours?: number;
|
|
137
137
|
}
|
|
138
138
|
export declare function computeGlobalCacheStats(db: FlightRecorderQuery, opts?: GlobalCacheStatsOpts): GlobalCacheStats;
|
|
139
|
+
/** Default response truncation budget, matching llm_job_result's maxChars. */
|
|
140
|
+
export declare const PERSISTED_REQUEST_DEFAULT_MAX_CHARS = 200000;
|
|
141
|
+
export interface PersistedRequestRecord {
|
|
142
|
+
correlationId: string;
|
|
143
|
+
cli: string;
|
|
144
|
+
model: string;
|
|
145
|
+
sessionId: string | null;
|
|
146
|
+
datetimeUtc: string;
|
|
147
|
+
durationMs: number | null;
|
|
148
|
+
status: string | null;
|
|
149
|
+
exitCode: number | null;
|
|
150
|
+
errorMessage: string | null;
|
|
151
|
+
retryCount: number | null;
|
|
152
|
+
circuitBreakerState: string | null;
|
|
153
|
+
costUsd: number | null;
|
|
154
|
+
/** NULL for sync requests; the async job UUID for *_request_async rows. */
|
|
155
|
+
asyncJobId: string | null;
|
|
156
|
+
inputTokens: number | null;
|
|
157
|
+
outputTokens: number | null;
|
|
158
|
+
cacheReadTokens: number | null;
|
|
159
|
+
cacheCreationTokens: number | null;
|
|
160
|
+
/** Full character length of the persisted prompt (always reported). */
|
|
161
|
+
promptChars: number;
|
|
162
|
+
/** Full character length of the persisted response (pre-truncation). */
|
|
163
|
+
responseChars: number;
|
|
164
|
+
/** True when `response` was clipped to `maxChars`. */
|
|
165
|
+
responseTruncated: boolean;
|
|
166
|
+
/** Persisted response text, truncated to maxChars. NULL if the row never completed. */
|
|
167
|
+
response: string | null;
|
|
168
|
+
/** Only present when includePrompt = true. */
|
|
169
|
+
prompt?: string;
|
|
170
|
+
/** Parsed thinking blocks (claude), or null. */
|
|
171
|
+
thinkingBlocks: string[] | null;
|
|
172
|
+
}
|
|
173
|
+
export interface ReadPersistedRequestOptions {
|
|
174
|
+
/** Truncate the returned response to this many characters. Default 200000. */
|
|
175
|
+
maxChars?: number;
|
|
176
|
+
/** Include the full persisted prompt text in the result. Default false. */
|
|
177
|
+
includePrompt?: boolean;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Fetch a single persisted request by correlation id from the flight recorder.
|
|
181
|
+
* Returns null when no row matches (including a NoopFlightRecorder, which
|
|
182
|
+
* yields no rows — i.e. flight recording disabled). The response is truncated
|
|
183
|
+
* to `maxChars`; the full pre-truncation length is reported via responseChars.
|
|
184
|
+
*/
|
|
185
|
+
export declare function readPersistedRequest(db: FlightRecorderQuery, correlationId: string, opts?: ReadPersistedRequestOptions): PersistedRequestRecord | null;
|
package/dist/cache-stats.js
CHANGED
|
@@ -235,8 +235,11 @@ export function computeGlobalCacheStats(db, opts = {}) {
|
|
|
235
235
|
continue;
|
|
236
236
|
stablePrefixReuseCount += 1;
|
|
237
237
|
arr.sort((a, b) => a.datetime_utc < b.datetime_utc ? -1 : a.datetime_utc > b.datetime_utc ? 1 : 0);
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
// Every row after the first-by-time in this prefix group (the reuse
|
|
239
|
+
// calls). Iterate the tail directly rather than index-walking `arr`.
|
|
240
|
+
const [, ...afterFirst] = arr;
|
|
241
|
+
for (const entry of afterFirst) {
|
|
242
|
+
creationAfterFirstSum += entry.cache_creation_tokens;
|
|
240
243
|
creationAfterFirstCount += 1;
|
|
241
244
|
}
|
|
242
245
|
}
|
|
@@ -266,3 +269,83 @@ export function computeGlobalCacheStats(db, opts = {}) {
|
|
|
266
269
|
avgCacheCreationAfterFirstCall,
|
|
267
270
|
};
|
|
268
271
|
}
|
|
272
|
+
//──────────────────────────────────────────────────────────────────────────────
|
|
273
|
+
// Read-back of a single persisted request by correlation id.
|
|
274
|
+
//
|
|
275
|
+
// The flight recorder already persists every request's `response` column on
|
|
276
|
+
// logComplete (flight-recorder.ts), regardless of sync vs async. But the only
|
|
277
|
+
// MCP read-back surface — llm_job_result — is keyed on an async job id and
|
|
278
|
+
// reads the AsyncJobManager, not the recorder. So a *sync* response (which has
|
|
279
|
+
// async_job_id = NULL and is handed back inline exactly once) has no retrieval
|
|
280
|
+
// path after the fact. This helper closes that gap: given the correlationId
|
|
281
|
+
// that every sync/async response echoes in `structuredContent.correlationId`,
|
|
282
|
+
// it returns the persisted row from the recorder. Pure read-only — uses the
|
|
283
|
+
// same FlightRecorderQuery surface as the cache aggregates above.
|
|
284
|
+
//──────────────────────────────────────────────────────────────────────────────
|
|
285
|
+
/** Default response truncation budget, matching llm_job_result's maxChars. */
|
|
286
|
+
export const PERSISTED_REQUEST_DEFAULT_MAX_CHARS = 200_000;
|
|
287
|
+
function parseThinkingBlocks(raw) {
|
|
288
|
+
if (!raw)
|
|
289
|
+
return null;
|
|
290
|
+
try {
|
|
291
|
+
const parsed = JSON.parse(raw);
|
|
292
|
+
return Array.isArray(parsed) ? parsed.filter((b) => typeof b === "string") : null;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Fetch a single persisted request by correlation id from the flight recorder.
|
|
300
|
+
* Returns null when no row matches (including a NoopFlightRecorder, which
|
|
301
|
+
* yields no rows — i.e. flight recording disabled). The response is truncated
|
|
302
|
+
* to `maxChars`; the full pre-truncation length is reported via responseChars.
|
|
303
|
+
*/
|
|
304
|
+
export function readPersistedRequest(db, correlationId, opts = {}) {
|
|
305
|
+
const maxChars = opts.maxChars ?? PERSISTED_REQUEST_DEFAULT_MAX_CHARS;
|
|
306
|
+
const rows = db.queryRequests(`SELECT r.id, r.cli, r.model, r.prompt, r.response, r.session_id,
|
|
307
|
+
r.datetime_utc, r.duration_ms, r.input_tokens, r.output_tokens,
|
|
308
|
+
r.cache_read_tokens, r.cache_creation_tokens,
|
|
309
|
+
m.retry_count, m.circuit_breaker_state, m.cost_usd,
|
|
310
|
+
m.exit_code, m.error_message, m.async_job_id, m.status,
|
|
311
|
+
m.thinking_blocks
|
|
312
|
+
FROM requests r
|
|
313
|
+
LEFT JOIN gateway_metadata m ON m.request_id = r.id
|
|
314
|
+
WHERE r.id = ?
|
|
315
|
+
LIMIT 1`, correlationId);
|
|
316
|
+
const [row] = rows;
|
|
317
|
+
if (!row)
|
|
318
|
+
return null;
|
|
319
|
+
const fullResponse = row.response;
|
|
320
|
+
const responseChars = fullResponse ? fullResponse.length : 0;
|
|
321
|
+
const responseTruncated = fullResponse != null && responseChars > maxChars;
|
|
322
|
+
const response = fullResponse == null ? null : fullResponse.slice(0, maxChars);
|
|
323
|
+
const record = {
|
|
324
|
+
correlationId: row.id,
|
|
325
|
+
cli: row.cli,
|
|
326
|
+
model: row.model,
|
|
327
|
+
sessionId: row.session_id,
|
|
328
|
+
datetimeUtc: row.datetime_utc,
|
|
329
|
+
durationMs: row.duration_ms,
|
|
330
|
+
status: row.status,
|
|
331
|
+
exitCode: row.exit_code,
|
|
332
|
+
errorMessage: row.error_message,
|
|
333
|
+
retryCount: row.retry_count,
|
|
334
|
+
circuitBreakerState: row.circuit_breaker_state,
|
|
335
|
+
costUsd: row.cost_usd,
|
|
336
|
+
asyncJobId: row.async_job_id,
|
|
337
|
+
inputTokens: row.input_tokens,
|
|
338
|
+
outputTokens: row.output_tokens,
|
|
339
|
+
cacheReadTokens: row.cache_read_tokens,
|
|
340
|
+
cacheCreationTokens: row.cache_creation_tokens,
|
|
341
|
+
promptChars: row.prompt ? row.prompt.length : 0,
|
|
342
|
+
responseChars,
|
|
343
|
+
responseTruncated,
|
|
344
|
+
response,
|
|
345
|
+
thinkingBlocks: parseThinkingBlocks(row.thinking_blocks),
|
|
346
|
+
};
|
|
347
|
+
if (opts.includePrompt) {
|
|
348
|
+
record.prompt = row.prompt ?? "";
|
|
349
|
+
}
|
|
350
|
+
return record;
|
|
351
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "fs";
|
|
|
2
2
|
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { createRequire } from "module";
|
|
5
|
-
import { z } from "zod";
|
|
5
|
+
import { z } from "zod/v3";
|
|
6
6
|
import { logWarn, noopLogger } from "./logger.js";
|
|
7
7
|
// Zod schemas for configuration validation
|
|
8
8
|
const DatabaseUrlSchema = z
|
package/dist/doctor.d.ts
CHANGED
|
@@ -132,6 +132,19 @@ export interface DoctorReport {
|
|
|
132
132
|
vibe_session_logging: VibeSessionLoggingStatus;
|
|
133
133
|
};
|
|
134
134
|
cache_awareness: CacheAwarenessReport;
|
|
135
|
+
upstream: {
|
|
136
|
+
note: string;
|
|
137
|
+
recommendation: string;
|
|
138
|
+
how_to_check: string;
|
|
139
|
+
/** Whether the expensive installed binary probe was performed (requires --probe-upstream). */
|
|
140
|
+
probed: boolean;
|
|
141
|
+
/** Cheap installed versions (always present when CLIs are detected). */
|
|
142
|
+
installed_versions: Partial<Record<CliType, string | null>>;
|
|
143
|
+
/** Lightweight declared contracts (always present, no spawning). */
|
|
144
|
+
contracts: ReturnType<typeof import("./upstream-contracts.js").buildUpstreamContractReport>;
|
|
145
|
+
/** Full probed report only when --probe-upstream was used. */
|
|
146
|
+
probe_report?: ReturnType<typeof import("./upstream-contracts.js").buildUpstreamContractReport>;
|
|
147
|
+
};
|
|
135
148
|
next_actions: string[];
|
|
136
149
|
}
|
|
137
150
|
export interface CreateDoctorReportOptions {
|
|
@@ -147,6 +160,14 @@ export interface CreateDoctorReportOptions {
|
|
|
147
160
|
* absent, `enabled_features` is empty (all behaviour considered off).
|
|
148
161
|
*/
|
|
149
162
|
cacheAwareness?: CacheAwarenessConfig;
|
|
163
|
+
/**
|
|
164
|
+
* When true, perform the (potentially slow) installed CLI --help probe
|
|
165
|
+
* for upstream contract drift detection. This is opt-in because it
|
|
166
|
+
* spawns the real provider CLIs.
|
|
167
|
+
*/
|
|
168
|
+
probeUpstream?: boolean;
|
|
150
169
|
}
|
|
151
170
|
export declare function createDoctorReport(envOrOptions?: NodeJS.ProcessEnv | CreateDoctorReportOptions): DoctorReport;
|
|
152
|
-
export declare function printDoctorJson(
|
|
171
|
+
export declare function printDoctorJson(opts?: {
|
|
172
|
+
probeUpstream?: boolean;
|
|
173
|
+
}): void;
|
package/dist/doctor.js
CHANGED
|
@@ -9,6 +9,7 @@ import { CLAUDE_MCP_SERVER_NAMES } from "./claude-mcp-config.js";
|
|
|
9
9
|
import { loadCacheAwarenessConfig } from "./config.js";
|
|
10
10
|
import { computeGlobalCacheStats } from "./cache-stats.js";
|
|
11
11
|
import { FlightRecorder, resolveFlightRecorderDbPath } from "./flight-recorder.js";
|
|
12
|
+
import { buildUpstreamContractReport } from "./upstream-contracts.js";
|
|
12
13
|
/**
|
|
13
14
|
* Probe ~/.vibe/config.toml to see whether session_logging is enabled. Current
|
|
14
15
|
* Mistral Vibe defaults session logging to enabled; an explicit
|
|
@@ -274,6 +275,25 @@ export function createDoctorReport(envOrOptions = process.env) {
|
|
|
274
275
|
const publicUrl = redactDiagnosticUrl(rawPublicUrl);
|
|
275
276
|
const endpointExposure = createEndpointExposureReport(env, publicUrl);
|
|
276
277
|
const providerStatuses = listProviderRuntimeStatuses();
|
|
278
|
+
const installedVersions = {};
|
|
279
|
+
for (const [name, status] of Object.entries(providerStatuses)) {
|
|
280
|
+
installedVersions[name] = status.version;
|
|
281
|
+
}
|
|
282
|
+
const lightweightContracts = buildUpstreamContractReport({ probeInstalled: false });
|
|
283
|
+
const probeReport = opts.probeUpstream
|
|
284
|
+
? buildUpstreamContractReport({ probeInstalled: true })
|
|
285
|
+
: undefined;
|
|
286
|
+
const upstream = {
|
|
287
|
+
note: "The gateway declares strict contracts for what flags, output modes, permission modes, and session/resume behaviour each provider CLI is expected to support.",
|
|
288
|
+
recommendation: "After upgrading any provider CLI (especially fast-moving vendor binaries like grok), run the installed binary probe to detect drift between what the gateway expects and what your installed CLI actually advertises.",
|
|
289
|
+
how_to_check: "llm-cli-gateway contracts --json --probe-installed (or with --cli=grok etc.)",
|
|
290
|
+
probed: !!opts.probeUpstream,
|
|
291
|
+
installed_versions: installedVersions,
|
|
292
|
+
contracts: lightweightContracts,
|
|
293
|
+
};
|
|
294
|
+
if (probeReport) {
|
|
295
|
+
upstream.probe_report = probeReport;
|
|
296
|
+
}
|
|
277
297
|
const report = {
|
|
278
298
|
schema_version: "1.0",
|
|
279
299
|
ok: true,
|
|
@@ -315,6 +335,7 @@ export function createDoctorReport(envOrOptions = process.env) {
|
|
|
315
335
|
endpoint_exposure: endpointExposure,
|
|
316
336
|
client_config: clientConfigStatus(),
|
|
317
337
|
cache_awareness: buildCacheAwarenessReport(opts),
|
|
338
|
+
upstream,
|
|
318
339
|
next_actions: [],
|
|
319
340
|
};
|
|
320
341
|
if (transport === "http" && auth.required && !auth.tokenConfigured) {
|
|
@@ -346,9 +367,21 @@ export function createDoctorReport(envOrOptions = process.env) {
|
|
|
346
367
|
if (report.next_actions.length === 0) {
|
|
347
368
|
report.next_actions.push("Run a client setup guide and verify with doctor --json after each step.");
|
|
348
369
|
}
|
|
370
|
+
// Upstream drift detection recommendation — surfaced for habitual use after provider upgrades.
|
|
371
|
+
const hasAnyCli = Object.values(report.providers).some(p => p.cli_available);
|
|
372
|
+
if (hasAnyCli) {
|
|
373
|
+
if (report.upstream.probed) {
|
|
374
|
+
report.next_actions.push("Upstream probe was run (see upstream.probe_report for installed vs declared drift).");
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
report.next_actions.push("After upgrading provider CLIs, check for contract drift: " +
|
|
378
|
+
report.upstream.how_to_check +
|
|
379
|
+
" (add --probe-upstream to this doctor command for one-shot probing)");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
349
382
|
return report;
|
|
350
383
|
}
|
|
351
|
-
export function printDoctorJson() {
|
|
384
|
+
export function printDoctorJson(opts = {}) {
|
|
352
385
|
// Load cache-awareness config + open the flight recorder so the doctor
|
|
353
386
|
// command can populate cache_awareness.last_24h. Both are best-effort —
|
|
354
387
|
// failures degrade to the zeroed block (buildCacheAwarenessReport
|
|
@@ -373,6 +406,7 @@ export function printDoctorJson() {
|
|
|
373
406
|
env: process.env,
|
|
374
407
|
cacheAwareness,
|
|
375
408
|
flightRecorder,
|
|
409
|
+
probeUpstream: opts.probeUpstream,
|
|
376
410
|
});
|
|
377
411
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
378
412
|
if (flightRecorder) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { z } from "zod";
|
|
3
|
+
import { z } from "zod/v3";
|
|
4
4
|
import { ISessionManager } from "./session-manager.js";
|
|
5
5
|
import { ResourceProvider } from "./resources.js";
|
|
6
6
|
import { PerformanceMetrics } from "./metrics.js";
|