llm-cli-gateway 2.12.1 → 2.13.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 (40) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +177 -19
  3. package/dist/acp/provider-registry.js +13 -0
  4. package/dist/api-provider.d.ts +1 -0
  5. package/dist/api-provider.js +13 -0
  6. package/dist/approval-manager.d.ts +1 -1
  7. package/dist/async-job-manager.d.ts +35 -3
  8. package/dist/async-job-manager.js +344 -46
  9. package/dist/cli-updater.d.ts +1 -1
  10. package/dist/cli-updater.js +17 -9
  11. package/dist/config.d.ts +34 -0
  12. package/dist/config.js +93 -8
  13. package/dist/doctor.d.ts +32 -4
  14. package/dist/doctor.js +80 -13
  15. package/dist/endpoint-exposure.js +15 -3
  16. package/dist/executor.js +2 -0
  17. package/dist/http-transport.d.ts +3 -0
  18. package/dist/http-transport.js +138 -8
  19. package/dist/index.d.ts +62 -5
  20. package/dist/index.js +774 -83
  21. package/dist/model-registry.d.ts +1 -1
  22. package/dist/model-registry.js +12 -16
  23. package/dist/provider-login-guidance.d.ts +12 -0
  24. package/dist/provider-login-guidance.js +46 -0
  25. package/dist/provider-status.d.ts +13 -1
  26. package/dist/provider-status.js +22 -13
  27. package/dist/provider-tool-capabilities.d.ts +4 -1
  28. package/dist/provider-tool-capabilities.js +310 -11
  29. package/dist/provider-types.d.ts +4 -0
  30. package/dist/provider-types.js +10 -0
  31. package/dist/resources.d.ts +6 -2
  32. package/dist/resources.js +72 -8
  33. package/dist/session-manager.d.ts +3 -5
  34. package/dist/session-manager.js +4 -2
  35. package/dist/upstream-contracts.js +169 -0
  36. package/dist/validation-normalizer.js +5 -4
  37. package/dist/validation-orchestrator.js +4 -0
  38. package/npm-shrinkwrap.json +2 -2
  39. package/package.json +1 -1
  40. package/setup/status.schema.json +68 -3
package/dist/config.d.ts CHANGED
@@ -36,6 +36,38 @@ export interface PersistenceConfigSources {
36
36
  }
37
37
  export declare function defaultGatewayConfigPath(): string;
38
38
  export declare function loadPersistenceConfig(logger?: Logger): PersistenceConfig;
39
+ export declare const DEFAULT_HTTP_MAX_SESSIONS = 100;
40
+ export declare const DEFAULT_HTTP_SESSION_IDLE_TTL_MS: number;
41
+ export declare const DEFAULT_HTTP_SESSION_REAPER_INTERVAL_MS: number;
42
+ export declare const DEFAULT_MAX_RUNNING_JOBS = 32;
43
+ export declare const DEFAULT_MAX_RUNNING_JOBS_PER_PROVIDER = 16;
44
+ export declare const DEFAULT_MAX_QUEUED_JOBS = 128;
45
+ export declare const DEFAULT_QUEUE_TIMEOUT_MS = 120000;
46
+ export declare const DEFAULT_COMPLETED_JOB_MEMORY_TTL_MS: number;
47
+ export declare const DEFAULT_MAX_JOB_OUTPUT_BYTES: number;
48
+ export interface HttpSessionLimitsConfig {
49
+ maxSessions: number;
50
+ sessionIdleTtlMs: number;
51
+ sessionReaperIntervalMs: number;
52
+ }
53
+ export interface JobLimitsConfig {
54
+ maxRunningJobs: number;
55
+ maxRunningJobsPerProvider: number;
56
+ maxQueuedJobs: number;
57
+ queueTimeoutMs: number;
58
+ completedJobMemoryTtlMs: number;
59
+ maxJobOutputBytes: number;
60
+ }
61
+ export interface GatewayLimitsConfig {
62
+ http: HttpSessionLimitsConfig;
63
+ jobs: JobLimitsConfig;
64
+ sources: {
65
+ configFile: string | null;
66
+ };
67
+ }
68
+ export declare const DEFAULT_HTTP_SESSION_LIMITS: HttpSessionLimitsConfig;
69
+ export declare const DEFAULT_JOB_LIMITS: JobLimitsConfig;
70
+ export declare function loadLimitsConfig(logger?: Logger): GatewayLimitsConfig;
39
71
  export declare const ANTHROPIC_TTL_SECONDS_VALUES: readonly [300, 3600];
40
72
  export type AnthropicTtlSeconds = (typeof ANTHROPIC_TTL_SECONDS_VALUES)[number];
41
73
  export declare const DEFAULT_MIN_STABLE_TOKENS_FOR_CACHE_CONTROL: {
@@ -81,6 +113,7 @@ export interface ApiProviderConfig {
81
113
  export interface ApiProviderRuntime {
82
114
  name: string;
83
115
  kind: ApiProviderKind;
116
+ apiKeyEnv: string | null;
84
117
  baseUrl: string;
85
118
  defaultModel: string;
86
119
  models?: string[];
@@ -96,6 +129,7 @@ export interface ProvidersConfig {
96
129
  }
97
130
  export declare function loadProvidersConfig(logger?: Logger): ProvidersConfig;
98
131
  export declare function isApiProviderEnabled(provider: ApiProviderConfig, env?: NodeJS.ProcessEnv): boolean;
132
+ export declare function apiProviderKeyPresent(provider: ApiProviderConfig, env?: NodeJS.ProcessEnv): boolean;
99
133
  export declare function enabledApiProviders(config: ProvidersConfig, env?: NodeJS.ProcessEnv): ApiProviderRuntime[];
100
134
  export declare function isXaiProviderEnabled(config: ProvidersConfig, env?: NodeJS.ProcessEnv): boolean;
101
135
  export declare const ACP_TRANSPORTS: readonly ["cli", "acp"];
package/dist/config.js CHANGED
@@ -6,6 +6,7 @@ import { z } from "zod/v3";
6
6
  import { logWarn, noopLogger } from "./logger.js";
7
7
  import { hashSecret, isSecretHash } from "./oauth.js";
8
8
  import { isHttpsOrLoopbackUrl, isLoopbackUrl } from "./api-http.js";
9
+ import { CLI_TYPES } from "./provider-types.js";
9
10
  const DatabaseUrlSchema = z
10
11
  .string()
11
12
  .url()
@@ -185,6 +186,93 @@ export function loadPersistenceConfig(logger = noopLogger) {
185
186
  sources,
186
187
  };
187
188
  }
189
+ export const DEFAULT_HTTP_MAX_SESSIONS = 100;
190
+ export const DEFAULT_HTTP_SESSION_IDLE_TTL_MS = 30 * 60 * 1000;
191
+ export const DEFAULT_HTTP_SESSION_REAPER_INTERVAL_MS = 60 * 1000;
192
+ export const DEFAULT_MAX_RUNNING_JOBS = 32;
193
+ export const DEFAULT_MAX_RUNNING_JOBS_PER_PROVIDER = 16;
194
+ export const DEFAULT_MAX_QUEUED_JOBS = 128;
195
+ export const DEFAULT_QUEUE_TIMEOUT_MS = 120000;
196
+ export const DEFAULT_COMPLETED_JOB_MEMORY_TTL_MS = 60 * 60 * 1000;
197
+ export const DEFAULT_MAX_JOB_OUTPUT_BYTES = 50 * 1024 * 1024;
198
+ const HttpSessionLimitsSchema = z
199
+ .object({
200
+ max_sessions: z.number().int().positive().default(DEFAULT_HTTP_MAX_SESSIONS),
201
+ session_idle_ttl_ms: z.number().int().positive().default(DEFAULT_HTTP_SESSION_IDLE_TTL_MS),
202
+ session_reaper_interval_ms: z
203
+ .number()
204
+ .int()
205
+ .positive()
206
+ .default(DEFAULT_HTTP_SESSION_REAPER_INTERVAL_MS),
207
+ })
208
+ .passthrough();
209
+ const JobLimitsSchema = z
210
+ .object({
211
+ max_running_jobs: z.number().int().positive().default(DEFAULT_MAX_RUNNING_JOBS),
212
+ max_running_jobs_per_provider: z
213
+ .number()
214
+ .int()
215
+ .positive()
216
+ .default(DEFAULT_MAX_RUNNING_JOBS_PER_PROVIDER),
217
+ max_queued_jobs: z.number().int().positive().default(DEFAULT_MAX_QUEUED_JOBS),
218
+ queue_timeout_ms: z.number().int().positive().default(DEFAULT_QUEUE_TIMEOUT_MS),
219
+ completed_job_memory_ttl_ms: z
220
+ .number()
221
+ .int()
222
+ .positive()
223
+ .default(DEFAULT_COMPLETED_JOB_MEMORY_TTL_MS),
224
+ max_job_output_bytes: z.number().int().positive().default(DEFAULT_MAX_JOB_OUTPUT_BYTES),
225
+ })
226
+ .strict();
227
+ export const DEFAULT_HTTP_SESSION_LIMITS = {
228
+ maxSessions: DEFAULT_HTTP_MAX_SESSIONS,
229
+ sessionIdleTtlMs: DEFAULT_HTTP_SESSION_IDLE_TTL_MS,
230
+ sessionReaperIntervalMs: DEFAULT_HTTP_SESSION_REAPER_INTERVAL_MS,
231
+ };
232
+ export const DEFAULT_JOB_LIMITS = {
233
+ maxRunningJobs: DEFAULT_MAX_RUNNING_JOBS,
234
+ maxRunningJobsPerProvider: DEFAULT_MAX_RUNNING_JOBS_PER_PROVIDER,
235
+ maxQueuedJobs: DEFAULT_MAX_QUEUED_JOBS,
236
+ queueTimeoutMs: DEFAULT_QUEUE_TIMEOUT_MS,
237
+ completedJobMemoryTtlMs: DEFAULT_COMPLETED_JOB_MEMORY_TTL_MS,
238
+ maxJobOutputBytes: DEFAULT_MAX_JOB_OUTPUT_BYTES,
239
+ };
240
+ export function loadLimitsConfig(logger = noopLogger) {
241
+ const configPath = defaultGatewayConfigPath();
242
+ const { parsed, sourcePath } = readGatewayTomlFile(configPath, logger, "limits");
243
+ const rawHttp = parsed?.http ?? {};
244
+ const rawLimits = parsed?.limits ?? {};
245
+ let httpParsed;
246
+ try {
247
+ httpParsed = HttpSessionLimitsSchema.parse(rawHttp);
248
+ }
249
+ catch (err) {
250
+ throw new Error(`Invalid [http] session-limit config: ${err instanceof Error ? err.message : String(err)}`);
251
+ }
252
+ let limitsParsed;
253
+ try {
254
+ limitsParsed = JobLimitsSchema.parse(rawLimits);
255
+ }
256
+ catch (err) {
257
+ throw new Error(`Invalid [limits] config: ${err instanceof Error ? err.message : String(err)}`);
258
+ }
259
+ return {
260
+ http: {
261
+ maxSessions: httpParsed.max_sessions,
262
+ sessionIdleTtlMs: httpParsed.session_idle_ttl_ms,
263
+ sessionReaperIntervalMs: httpParsed.session_reaper_interval_ms,
264
+ },
265
+ jobs: {
266
+ maxRunningJobs: limitsParsed.max_running_jobs,
267
+ maxRunningJobsPerProvider: limitsParsed.max_running_jobs_per_provider,
268
+ maxQueuedJobs: limitsParsed.max_queued_jobs,
269
+ queueTimeoutMs: limitsParsed.queue_timeout_ms,
270
+ completedJobMemoryTtlMs: limitsParsed.completed_job_memory_ttl_ms,
271
+ maxJobOutputBytes: limitsParsed.max_job_output_bytes,
272
+ },
273
+ sources: { configFile: sourcePath },
274
+ };
275
+ }
188
276
  export const ANTHROPIC_TTL_SECONDS_VALUES = [300, 3600];
189
277
  export const DEFAULT_MIN_STABLE_TOKENS_FOR_CACHE_CONTROL = {
190
278
  sonnet: 1024,
@@ -268,14 +356,7 @@ export function minStableTokensForModel(config, modelName) {
268
356
  return table.haiku;
269
357
  return table.default;
270
358
  }
271
- const RESERVED_CLI_PROVIDER_NAMES = [
272
- "claude",
273
- "codex",
274
- "gemini",
275
- "grok",
276
- "mistral",
277
- "devin",
278
- ];
359
+ const RESERVED_CLI_PROVIDER_NAMES = CLI_TYPES;
279
360
  export const DEFAULT_XAI_API_KEY_ENV = "XAI_API_KEY";
280
361
  export const DEFAULT_XAI_BASE_URL = "https://api.x.ai/v1";
281
362
  export const DEFAULT_XAI_MODEL = "grok-build-0.1";
@@ -386,6 +467,9 @@ export function isApiProviderEnabled(provider, env = process.env) {
386
467
  return true;
387
468
  return provider.kind === "openai-compatible" && isLoopbackUrl(provider.baseUrl);
388
469
  }
470
+ export function apiProviderKeyPresent(provider, env = process.env) {
471
+ return resolveProviderKey(provider.apiKeyEnv, env) !== null;
472
+ }
389
473
  export function enabledApiProviders(config, env = process.env) {
390
474
  const runtimes = [];
391
475
  for (const provider of Object.values(config.providers ?? {})) {
@@ -394,6 +478,7 @@ export function enabledApiProviders(config, env = process.env) {
394
478
  runtimes.push({
395
479
  name: provider.name,
396
480
  kind: provider.kind,
481
+ apiKeyEnv: provider.apiKeyEnv,
397
482
  baseUrl: provider.baseUrl,
398
483
  defaultModel: provider.defaultModel,
399
484
  models: provider.models,
package/dist/doctor.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { type EndpointExposureReport } from "./endpoint-exposure.js";
2
2
  import { type ProviderLoginStatus } from "./provider-status.js";
3
+ import { type ApiProviderLoginGuidance } from "./provider-login-guidance.js";
3
4
  import type { FlightRecorderQuery } from "./flight-recorder.js";
4
- import { type CacheAwarenessConfig } from "./config.js";
5
+ import { type CacheAwarenessConfig, type ProvidersConfig } from "./config.js";
5
6
  import { type ProviderCapabilityId, type ProviderKind } from "./provider-tool-capabilities.js";
6
- export type CliType = "claude" | "codex" | "gemini" | "grok" | "mistral" | "devin";
7
+ import { type CliType } from "./session-manager.js";
7
8
  export interface CacheAwarenessReport {
8
9
  enabled_features: Array<"anthropic_cache_control" | "ttl_warnings">;
9
10
  last_24h: {
@@ -38,6 +39,22 @@ export interface ProviderCapabilitySummaryReport {
38
39
  warnings: string[];
39
40
  }>;
40
41
  }
42
+ export interface ApiProviderHealthEntry {
43
+ name: string;
44
+ kind: "openai-compatible" | "anthropic" | "xai-responses";
45
+ base_url: string;
46
+ default_model: string;
47
+ models: string[] | null;
48
+ api_key_env: string | null;
49
+ api_key_present: boolean;
50
+ reachable: boolean | null;
51
+ reachability_error?: string;
52
+ login_guidance: ApiProviderLoginGuidance;
53
+ }
54
+ export interface ApiProviderHealthReport {
55
+ enabled_count: number;
56
+ providers: Record<string, ApiProviderHealthEntry>;
57
+ }
41
58
  export interface VibeSessionLoggingStatus {
42
59
  config_path: string;
43
60
  config_present: boolean;
@@ -106,7 +123,7 @@ export interface DoctorReport {
106
123
  allowed_root_count: number;
107
124
  gateway_app_dir_is_workspace: boolean;
108
125
  };
109
- providers: Record<"claude" | "codex" | "gemini" | "grok" | "mistral", {
126
+ providers: Record<CliType, {
110
127
  cli_available: boolean;
111
128
  version: string | null;
112
129
  login_status: ProviderLoginStatus;
@@ -138,6 +155,7 @@ export interface DoctorReport {
138
155
  };
139
156
  cache_awareness: CacheAwarenessReport;
140
157
  provider_capabilities: ProviderCapabilitySummaryReport;
158
+ api_providers?: ApiProviderHealthReport;
141
159
  upstream: {
142
160
  note: string;
143
161
  recommendation: string;
@@ -154,8 +172,18 @@ export interface CreateDoctorReportOptions {
154
172
  flightRecorder?: FlightRecorderQuery;
155
173
  cacheAwareness?: CacheAwarenessConfig;
156
174
  probeUpstream?: boolean;
175
+ providersConfig?: ProvidersConfig;
176
+ apiReachability?: Record<string, {
177
+ reachable: boolean;
178
+ error?: string;
179
+ }>;
157
180
  }
158
181
  export declare function createDoctorReport(envOrOptions?: NodeJS.ProcessEnv | CreateDoctorReportOptions): DoctorReport;
159
182
  export declare function printDoctorJson(opts?: {
160
183
  probeUpstream?: boolean;
161
- }): void;
184
+ probeApiProviders?: boolean;
185
+ }): Promise<void>;
186
+ export declare function probeApiProviderReachability(baseUrl: string, timeoutMs?: number): Promise<{
187
+ reachable: boolean;
188
+ error?: string;
189
+ }>;
package/dist/doctor.js CHANGED
@@ -4,14 +4,16 @@ import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { loadAuthConfig } from "./auth.js";
6
6
  import { createEndpointExposureReport, redactDiagnosticUrl, } from "./endpoint-exposure.js";
7
- import { listProviderRuntimeStatuses, } from "./provider-status.js";
7
+ import { getApiProviderStatus, listProviderRuntimeStatuses, } from "./provider-status.js";
8
+ import { getApiProviderLoginGuidance, } from "./provider-login-guidance.js";
8
9
  import { CLAUDE_MCP_SERVER_NAMES } from "./claude-mcp-config.js";
9
- import { loadCacheAwarenessConfig, loadRemoteOAuthConfig, } from "./config.js";
10
+ import { enabledApiProviders, loadCacheAwarenessConfig, loadProvidersConfig, loadRemoteOAuthConfig, } from "./config.js";
10
11
  import { loadWorkspaceRegistry } from "./workspace-registry.js";
11
12
  import { computeGlobalCacheStats } from "./cache-stats.js";
12
13
  import { FlightRecorder, resolveFlightRecorderDbPath } from "./flight-recorder.js";
13
14
  import { buildUpstreamContractReport } from "./upstream-contracts.js";
14
- import { getProviderToolCapabilities, providerCapabilityIds, } from "./provider-tool-capabilities.js";
15
+ import { getProviderToolCapabilities, knownProviderCapabilityIds, } from "./provider-tool-capabilities.js";
16
+ import { CLI_TYPES } from "./session-manager.js";
15
17
  export function checkVibeSessionLogging(home = homedir()) {
16
18
  const configPath = join(home, ".vibe", "config.toml");
17
19
  if (!existsSync(configPath)) {
@@ -172,6 +174,34 @@ function chatGPTConnectorUrl(env, rawPublicUrl) {
172
174
  return null;
173
175
  return "<redacted>";
174
176
  }
177
+ function buildApiProviderHealthReport(opts, env) {
178
+ const config = opts.providersConfig;
179
+ if (!config)
180
+ return undefined;
181
+ const providers = {};
182
+ for (const providerConfig of Object.values(config.providers)) {
183
+ const status = getApiProviderStatus(providerConfig, env);
184
+ if (!status.enabled)
185
+ continue;
186
+ const probe = opts.apiReachability?.[status.provider];
187
+ providers[status.provider] = {
188
+ name: status.provider,
189
+ kind: status.kind,
190
+ base_url: status.baseUrl,
191
+ default_model: status.defaultModel,
192
+ models: status.models,
193
+ api_key_env: status.apiKeyEnv,
194
+ api_key_present: status.apiKeyPresent,
195
+ reachable: probe ? probe.reachable : null,
196
+ ...(probe?.error ? { reachability_error: probe.error } : {}),
197
+ login_guidance: getApiProviderLoginGuidance(providerConfig),
198
+ };
199
+ }
200
+ const names = Object.keys(providers);
201
+ if (names.length === 0)
202
+ return undefined;
203
+ return { enabled_count: names.length, providers };
204
+ }
175
205
  function buildCacheAwarenessReport(opts) {
176
206
  const enabled = [];
177
207
  if (opts.cacheAwareness?.emitAnthropicCacheControl) {
@@ -234,7 +264,7 @@ function buildProviderCapabilitySummary(providerStatuses) {
234
264
  includeUnsupported: true,
235
265
  includePaths: false,
236
266
  });
237
- const providers = Object.fromEntries(providerCapabilityIds().map(provider => {
267
+ const providers = Object.fromEntries(knownProviderCapabilityIds().map(provider => {
238
268
  const capability = capabilities[provider];
239
269
  if (!capability) {
240
270
  throw new Error(`Missing provider capability record for ${provider}`);
@@ -264,7 +294,7 @@ function buildProviderCapabilitySummary(providerStatuses) {
264
294
  tool: "provider_tool_capabilities",
265
295
  resources: {
266
296
  catalog: "provider-tools://catalog",
267
- providers: Object.fromEntries(providerCapabilityIds().map(provider => [provider, `provider-tools://${provider}`])),
297
+ providers: Object.fromEntries(knownProviderCapabilityIds().map(provider => [provider, `provider-tools://${provider}`])),
268
298
  },
269
299
  cache_ttl_ms: 60_000,
270
300
  providers,
@@ -350,13 +380,7 @@ export function createDoctorReport(envOrOptions = process.env) {
350
380
  allowed_root_count: workspaceRegistry.allowedRoots.length,
351
381
  gateway_app_dir_is_workspace: workspaceRegistry.repos.some(repo => repo.path === join(homedir(), ".llm-cli-gateway")),
352
382
  },
353
- providers: {
354
- claude: doctorProviderStatus(providerStatuses.claude),
355
- codex: doctorProviderStatus(providerStatuses.codex),
356
- gemini: doctorProviderStatus(providerStatuses.gemini),
357
- grok: doctorProviderStatus(providerStatuses.grok),
358
- mistral: doctorProviderStatus(providerStatuses.mistral),
359
- },
383
+ providers: Object.fromEntries(CLI_TYPES.map(provider => [provider, doctorProviderStatus(providerStatuses[provider])])),
360
384
  endpoint_exposure: endpointExposure,
361
385
  client_config: clientConfigStatus(),
362
386
  cache_awareness: buildCacheAwarenessReport(opts),
@@ -364,6 +388,10 @@ export function createDoctorReport(envOrOptions = process.env) {
364
388
  upstream,
365
389
  next_actions: [],
366
390
  };
391
+ const apiProviders = buildApiProviderHealthReport(opts, env);
392
+ if (apiProviders) {
393
+ report.api_providers = apiProviders;
394
+ }
367
395
  if (transport === "http" && auth.required && !auth.tokenConfigured) {
368
396
  report.ok = false;
369
397
  report.next_actions.push("Set LLM_GATEWAY_AUTH_TOKEN before starting HTTP transport.");
@@ -402,9 +430,10 @@ export function createDoctorReport(envOrOptions = process.env) {
402
430
  }
403
431
  return report;
404
432
  }
405
- export function printDoctorJson(opts = {}) {
433
+ export async function printDoctorJson(opts = {}) {
406
434
  let cacheAwareness;
407
435
  let flightRecorder;
436
+ let providersConfig;
408
437
  try {
409
438
  cacheAwareness = loadCacheAwarenessConfig();
410
439
  }
@@ -417,11 +446,25 @@ export function printDoctorJson(opts = {}) {
417
446
  }
418
447
  catch {
419
448
  }
449
+ try {
450
+ providersConfig = loadProvidersConfig();
451
+ }
452
+ catch {
453
+ }
454
+ let apiReachability;
455
+ if (opts.probeApiProviders && providersConfig) {
456
+ apiReachability = {};
457
+ for (const runtime of enabledApiProviders(providersConfig)) {
458
+ apiReachability[runtime.name] = await probeApiProviderReachability(runtime.baseUrl);
459
+ }
460
+ }
420
461
  const report = createDoctorReport({
421
462
  env: process.env,
422
463
  cacheAwareness,
423
464
  flightRecorder,
424
465
  probeUpstream: opts.probeUpstream,
466
+ providersConfig,
467
+ apiReachability,
425
468
  });
426
469
  process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
427
470
  if (flightRecorder) {
@@ -432,6 +475,30 @@ export function printDoctorJson(opts = {}) {
432
475
  }
433
476
  }
434
477
  }
478
+ export async function probeApiProviderReachability(baseUrl, timeoutMs = 5_000) {
479
+ let url;
480
+ try {
481
+ url = new URL(baseUrl);
482
+ }
483
+ catch {
484
+ return { reachable: false, error: "invalid base_url" };
485
+ }
486
+ const transport = url.protocol === "http:" ? await import("node:http") : await import("node:https");
487
+ return new Promise(resolve => {
488
+ const request = transport.request(url, { method: "GET", timeout: timeoutMs }, response => {
489
+ response.resume();
490
+ resolve({ reachable: true });
491
+ });
492
+ request.on("timeout", () => {
493
+ request.destroy();
494
+ resolve({ reachable: false, error: `timed out after ${timeoutMs}ms` });
495
+ });
496
+ request.on("error", err => {
497
+ resolve({ reachable: false, error: err instanceof Error ? err.message : String(err) });
498
+ });
499
+ request.end();
500
+ });
501
+ }
435
502
  function isCreateDoctorReportOptions(value) {
436
503
  if (value === null || typeof value !== "object")
437
504
  return false;
@@ -68,16 +68,28 @@ export function redactDiagnosticUrl(rawUrl) {
68
68
  const redactSensitivePairs = (value) => value.replace(new RegExp(`((${sensitiveKeyPattern.source})=)[^&\\s#]+`, "gi"), "$1<redacted>");
69
69
  try {
70
70
  const url = new URL(rawUrl);
71
- if (url.username)
71
+ let redacted = false;
72
+ if (url.username) {
72
73
  url.username = "<redacted>";
73
- if (url.password)
74
+ redacted = true;
75
+ }
76
+ if (url.password) {
74
77
  url.password = "<redacted>";
78
+ redacted = true;
79
+ }
75
80
  for (const key of Array.from(url.searchParams.keys())) {
76
81
  if (sensitiveKeyPattern.test(key)) {
77
82
  url.searchParams.set(key, "<redacted>");
83
+ redacted = true;
78
84
  }
79
85
  }
80
- url.hash = redactSensitivePairs(url.hash);
86
+ const redactedHash = redactSensitivePairs(url.hash);
87
+ if (redactedHash !== url.hash) {
88
+ url.hash = redactedHash;
89
+ redacted = true;
90
+ }
91
+ if (!redacted)
92
+ return rawUrl;
81
93
  return url.toString().replace(/%3Credacted%3E/gi, "<redacted>");
82
94
  }
83
95
  catch {
package/dist/executor.js CHANGED
@@ -8,6 +8,8 @@ export function providerCommandName(command) {
8
8
  return "agy";
9
9
  if (command === "mistral")
10
10
  return "vibe";
11
+ if (command === "cursor")
12
+ return "cursor-agent";
11
13
  return command;
12
14
  }
13
15
  const MAX_OUTPUT_SIZE = 50 * 1024 * 1024;
@@ -1,12 +1,14 @@
1
1
  import { type Server } from "node:http";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import type { GatewayServerDeps } from "./index.js";
4
+ import { type HttpSessionLimitsConfig } from "./config.js";
4
5
  export interface HttpTransportOptions {
5
6
  host?: string;
6
7
  port?: number;
7
8
  path?: string;
8
9
  deps?: GatewayServerDeps;
9
10
  createGatewayServer: (deps?: GatewayServerDeps) => McpServer;
11
+ httpLimits?: HttpSessionLimitsConfig;
10
12
  logger?: {
11
13
  info: (...args: any[]) => void;
12
14
  error: (...args: any[]) => void;
@@ -18,5 +20,6 @@ export interface HttpGatewayHandle {
18
20
  url: string;
19
21
  close: () => Promise<void>;
20
22
  sessionCount: () => number;
23
+ sessionHealth: () => Record<string, unknown>;
21
24
  }
22
25
  export declare function startHttpGateway(options: HttpTransportOptions): Promise<HttpGatewayHandle>;