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 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
  [![Security](https://github.com/verivus-oss/llm-cli-gateway/actions/workflows/security.yml/badge.svg?branch=main)](https://github.com/verivus-oss/llm-cli-gateway/actions/workflows/security.yml)
5
5
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/verivus-oss/llm-cli-gateway/badge)](https://scorecard.dev/viewer/?uri=github.com/verivus-oss/llm-cli-gateway)
6
6
  [![npm](https://img.shields.io/npm/v/llm-cli-gateway.svg)](https://www.npmjs.com/package/llm-cli-gateway)
7
- [![npm monthly downloads](https://img.shields.io/npm/dm/llm-cli-gateway.svg)](https://www.npmjs.com/package/llm-cli-gateway)
8
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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:** crossed 5k monthly npm downloads in May 2026; live npm downloads are shown above. 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.
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
  [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13025/badge)](https://www.bestpractices.dev/projects/13025)
58
- [![npm weekly downloads](https://img.shields.io/npm/dw/llm-cli-gateway.svg)](https://www.npmjs.com/package/llm-cli-gateway)
59
- [![GitHub release downloads](https://img.shields.io/github/downloads/verivus-oss/llm-cli-gateway/total.svg)](https://github.com/verivus-oss/llm-cli-gateway/releases)
60
57
  [![Releases: Sigstore signed](https://img.shields.io/badge/releases-Sigstore%20signed-2e7d32.svg)](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-compose.personal.yml](docker-compose.personal.yml) + [Dockerfile.personal](Dockerfile.personal) for users who already manage containers.
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-compose.personal.yml up -d
152
- docker compose -f docker-compose.personal.yml run --rm doctor
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
- npm install -g grok-build
248
- grok login # OAuth flow, or set GROK_CODE_XAI_API_KEY
249
- # Docs: https://docs.x.ai/build/cli
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 three behavioural alerts and some dependency-ownership alerts. They are accurate descriptions of what the package does and what it depends on; we've left them visible (not silenced in `socket.yml`) so you don't have to take our word for it. Here's the context for each:
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
 
@@ -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;
@@ -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
- for (let i = 1; i < arr.length; i++) {
239
- creationAfterFirstSum += arr[i].cache_creation_tokens;
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(): void;
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";