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
@@ -1,4 +1,4 @@
1
- import type { CliType } from "./session-manager.js";
1
+ import { type CliType } from "./provider-types.js";
2
2
  type ModelSource = "fallback" | "observed" | "config" | "env";
3
3
  type ModelConfidence = "low" | "medium" | "high";
4
4
  export interface ModelMetadata {
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
2
2
  import { homedir } from "os";
3
3
  import path from "path";
4
4
  import { parse as parseToml } from "smol-toml";
5
+ import { CLI_TYPES } from "./provider-types.js";
5
6
  const FALLBACK_INFO = {
6
7
  claude: {
7
8
  description: "Anthropic's Claude Code CLI - best for code generation, analysis, and agentic coding tasks",
@@ -56,6 +57,15 @@ const FALLBACK_INFO = {
56
57
  },
57
58
  modelOrder: ["opus", "gpt-5.5", "swe-1.6"],
58
59
  },
60
+ cursor: {
61
+ description: "Cursor Agent CLI - agentic coding and review in Cursor's headless terminal agent",
62
+ models: {
63
+ "gpt-5": "OpenAI GPT-5 model accepted by Cursor Agent examples.",
64
+ "sonnet-4-thinking": "Claude Sonnet thinking model accepted by Cursor Agent examples.",
65
+ "claude-opus-4-8": "Parameterized Claude Opus model family accepted by Cursor Agent examples.",
66
+ },
67
+ modelOrder: ["gpt-5", "sonnet-4-thinking", "claude-opus-4-8"],
68
+ },
59
69
  };
60
70
  const MODEL_CACHE_TTL_MS = 2 * 60 * 1000;
61
71
  const MAX_GEMINI_HISTORY_FILES = 200;
@@ -77,14 +87,7 @@ export function getCliInfo(forceRefresh = false) {
77
87
  }
78
88
  export function getAvailableCliInfo(forceRefresh = false) {
79
89
  const info = getCliInfo(forceRefresh);
80
- return {
81
- claude: filterUnverifiedModelHints(info.claude),
82
- codex: filterUnverifiedModelHints(info.codex),
83
- gemini: filterUnverifiedModelHints(info.gemini),
84
- grok: filterUnverifiedModelHints(info.grok),
85
- mistral: filterUnverifiedModelHints(info.mistral),
86
- devin: filterUnverifiedModelHints(info.devin),
87
- };
90
+ return Object.fromEntries(CLI_TYPES.map(cli => [cli, filterUnverifiedModelHints(info[cli])]));
88
91
  }
89
92
  export function clearModelRegistryCache() {
90
93
  cachedInfo = null;
@@ -115,14 +118,7 @@ export function resolveModelAlias(cli, model, info) {
115
118
  return trimmed;
116
119
  }
117
120
  function buildCliInfo() {
118
- const info = {
119
- claude: cloneInfo(FALLBACK_INFO.claude),
120
- codex: cloneInfo(FALLBACK_INFO.codex),
121
- gemini: cloneInfo(FALLBACK_INFO.gemini),
122
- grok: cloneInfo(FALLBACK_INFO.grok),
123
- mistral: cloneInfo(FALLBACK_INFO.mistral),
124
- devin: cloneInfo(FALLBACK_INFO.devin),
125
- };
121
+ const info = Object.fromEntries(CLI_TYPES.map(cli => [cli, cloneInfo(FALLBACK_INFO[cli])]));
126
122
  applyClaudeOverrides(info.claude);
127
123
  applyCodexOverrides(info.codex);
128
124
  applyGeminiOverrides(info.gemini);
@@ -1,4 +1,5 @@
1
1
  import type { CliType } from "./session-manager.js";
2
+ import type { ApiProviderConfig } from "./config.js";
2
3
  export interface ProviderLoginGuidance {
3
4
  provider: CliType;
4
5
  displayName: string;
@@ -19,3 +20,14 @@ export interface ProviderLoginGuidance {
19
20
  }
20
21
  export declare function getProviderLoginGuidance(provider: CliType): ProviderLoginGuidance;
21
22
  export declare function getAllProviderLoginGuidance(): Record<CliType, ProviderLoginGuidance>;
23
+ export interface ApiProviderLoginGuidance {
24
+ provider: string;
25
+ displayName: string;
26
+ kind: ApiProviderConfig["kind"];
27
+ baseUrl: string;
28
+ apiKeyEnv: string | null;
29
+ summary: string;
30
+ steps: string[];
31
+ credentialHandling: string;
32
+ }
33
+ export declare function getApiProviderLoginGuidance(provider: ApiProviderConfig): ApiProviderLoginGuidance;
@@ -1,3 +1,4 @@
1
+ import { redactDiagnosticUrl } from "./endpoint-exposure.js";
1
2
  const GUIDANCE = {
2
3
  claude: {
3
4
  provider: "claude",
@@ -115,6 +116,24 @@ const GUIDANCE = {
115
116
  expected: "CLI is installed and `devin auth status` reports an authenticated session",
116
117
  },
117
118
  },
119
+ cursor: {
120
+ provider: "cursor",
121
+ displayName: "Cursor Agent CLI",
122
+ install: {
123
+ summary: "Install Cursor Agent CLI using Cursor's current official installer.",
124
+ commands: ["cursor-agent update", "cursor-agent --version"],
125
+ documentationUrl: "https://cursor.com/cli",
126
+ },
127
+ login: {
128
+ summary: "Sign in through Cursor Agent's official login flow, or set CURSOR_API_KEY for headless automation.",
129
+ commands: ["cursor-agent login", "cursor-agent status"],
130
+ credentialHandling: "Let Cursor store credentials via `cursor-agent login`, or provide CURSOR_API_KEY in the process environment. Do not paste Cursor API keys into prompts or remote chats.",
131
+ },
132
+ verification: {
133
+ command: "cursor-agent status",
134
+ expected: "CLI is installed and `cursor-agent status` reports an authenticated account",
135
+ },
136
+ },
118
137
  };
119
138
  export function getProviderLoginGuidance(provider) {
120
139
  return GUIDANCE[provider];
@@ -122,3 +141,30 @@ export function getProviderLoginGuidance(provider) {
122
141
  export function getAllProviderLoginGuidance() {
123
142
  return { ...GUIDANCE };
124
143
  }
144
+ export function getApiProviderLoginGuidance(provider) {
145
+ const keyless = provider.apiKeyEnv === null;
146
+ const baseUrl = redactDiagnosticUrl(provider.baseUrl) ?? provider.baseUrl;
147
+ const summary = keyless
148
+ ? `Keyless-local API provider "${provider.name}" (kind: ${provider.kind}); no credential is required for its loopback endpoint.`
149
+ : `API provider "${provider.name}" (kind: ${provider.kind}) authenticates with a key read from the ${provider.apiKeyEnv} environment variable.`;
150
+ const steps = keyless
151
+ ? [
152
+ `Ensure the local endpoint at ${baseUrl} is running (e.g. Ollama or llama.cpp).`,
153
+ `No API key is needed; the provider is enabled as soon as the loopback endpoint is reachable.`,
154
+ ]
155
+ : [
156
+ `Obtain an API key from the provider that serves ${baseUrl}.`,
157
+ `Export it as ${provider.apiKeyEnv} in the gateway's environment (the value is read only at request time).`,
158
+ `Confirm [providers.${provider.name}] in the gateway config points at the intended base_url and default_model.`,
159
+ ];
160
+ return {
161
+ provider: provider.name,
162
+ displayName: provider.name,
163
+ kind: provider.kind,
164
+ baseUrl,
165
+ apiKeyEnv: provider.apiKeyEnv,
166
+ summary,
167
+ steps,
168
+ credentialHandling: "Set the API key only via the named environment variable. Do not paste the key into the gateway config, prompts, or a remote chat.",
169
+ };
170
+ }
@@ -1,5 +1,6 @@
1
- import type { CliType } from "./session-manager.js";
1
+ import { type CliType } from "./provider-types.js";
2
2
  import { type ProviderLoginGuidance } from "./provider-login-guidance.js";
3
+ import { type ApiProviderConfig } from "./config.js";
3
4
  export type ProviderLoginStatus = "authenticated" | "not_authenticated" | "unknown" | "not_checked";
4
5
  export interface ProviderRuntimeStatus {
5
6
  provider: CliType;
@@ -17,6 +18,17 @@ export interface ProviderRuntimeStatus {
17
18
  };
18
19
  guidance: ProviderLoginGuidance;
19
20
  }
21
+ export interface ApiProviderRuntimeStatus {
22
+ provider: string;
23
+ kind: ApiProviderConfig["kind"];
24
+ baseUrl: string;
25
+ defaultModel: string;
26
+ models: string[] | null;
27
+ apiKeyEnv: string | null;
28
+ apiKeyPresent: boolean;
29
+ enabled: boolean;
30
+ }
31
+ export declare function getApiProviderStatus(provider: ApiProviderConfig, env?: NodeJS.ProcessEnv): ApiProviderRuntimeStatus;
20
32
  export declare const PROVIDER_COMMANDS: Record<CliType, string>;
21
33
  export declare function listProviderRuntimeStatuses(): Record<CliType, ProviderRuntimeStatus>;
22
34
  export declare function getProviderRuntimeStatus(provider: CliType): ProviderRuntimeStatus;
@@ -2,24 +2,32 @@ import { existsSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
+ import { CLI_TYPES } from "./provider-types.js";
5
6
  import { getProviderLoginGuidance } from "./provider-login-guidance.js";
7
+ import { apiProviderKeyPresent, isApiProviderEnabled } from "./config.js";
8
+ import { redactDiagnosticUrl } from "./endpoint-exposure.js";
6
9
  import { envWithExtendedPath, getExtendedPath, providerCommandName, resolveCommandForSpawn, } from "./executor.js";
7
- const PROVIDERS = ["claude", "codex", "gemini", "grok", "mistral", "devin"];
8
- const VERSION_ARGS = {
9
- claude: ["--version"],
10
- codex: ["--version"],
11
- gemini: ["--version"],
12
- grok: ["--version"],
13
- mistral: ["--version"],
14
- devin: ["--version"],
15
- };
10
+ export function getApiProviderStatus(provider, env = process.env) {
11
+ return {
12
+ provider: provider.name,
13
+ kind: provider.kind,
14
+ baseUrl: redactDiagnosticUrl(provider.baseUrl) ?? provider.baseUrl,
15
+ defaultModel: provider.defaultModel,
16
+ models: provider.models ?? null,
17
+ apiKeyEnv: provider.apiKeyEnv,
18
+ apiKeyPresent: apiProviderKeyPresent(provider, env),
19
+ enabled: isApiProviderEnabled(provider, env),
20
+ };
21
+ }
22
+ const VERSION_ARGS = Object.fromEntries(CLI_TYPES.map(provider => [provider, ["--version"]]));
16
23
  export const PROVIDER_COMMANDS = {
17
- claude: "claude",
18
- codex: "codex",
24
+ claude: providerCommandName("claude"),
25
+ codex: providerCommandName("codex"),
19
26
  gemini: providerCommandName("gemini"),
20
- grok: "grok",
27
+ grok: providerCommandName("grok"),
21
28
  mistral: providerCommandName("mistral"),
22
29
  devin: providerCommandName("devin"),
30
+ cursor: providerCommandName("cursor"),
23
31
  };
24
32
  const LOGIN_CHECKS = {
25
33
  claude: ["auth", "status", "--json"],
@@ -27,9 +35,10 @@ const LOGIN_CHECKS = {
27
35
  grok: ["inspect", "--json"],
28
36
  mistral: ["auth", "status"],
29
37
  devin: ["auth", "status"],
38
+ cursor: ["status"],
30
39
  };
31
40
  export function listProviderRuntimeStatuses() {
32
- return Object.fromEntries(PROVIDERS.map(provider => [provider, getProviderRuntimeStatus(provider)]));
41
+ return Object.fromEntries(CLI_TYPES.map(provider => [provider, getProviderRuntimeStatus(provider)]));
33
42
  }
34
43
  export function getProviderRuntimeStatus(provider) {
35
44
  const guidance = getProviderLoginGuidance(provider);
@@ -1,5 +1,6 @@
1
1
  import { type CliInfo } from "./model-registry.js";
2
2
  import { type CliType } from "./session-manager.js";
3
+ import { type ProvidersConfig } from "./config.js";
3
4
  export interface ProviderToolControl {
4
5
  name?: string;
5
6
  supported: boolean;
@@ -128,6 +129,7 @@ export interface ProviderCapabilityQuery {
128
129
  includeUnsupported?: boolean;
129
130
  includePaths?: boolean;
130
131
  refresh?: boolean;
132
+ providersConfig?: ProvidersConfig;
131
133
  }
132
134
  export type ProviderToolCapabilitiesMap = Partial<Record<ProviderCapabilityId, ProviderToolCapabilities>>;
133
135
  export declare function getProviderToolCapabilities(queryOrCli?: ProviderCapabilityQuery | ProviderCapabilityId): ProviderToolCapabilitiesMap;
@@ -137,4 +139,5 @@ export declare function _getCapabilityCacheForTest(): Map<string, {
137
139
  loadedAt: number;
138
140
  value: ProviderToolCapabilities;
139
141
  }>;
140
- export declare function providerCapabilityIds(): readonly KnownProviderCapabilityId[];
142
+ export declare function providerCapabilityIds(providersConfig?: ProvidersConfig): readonly ProviderCapabilityId[];
143
+ export declare function knownProviderCapabilityIds(): readonly KnownProviderCapabilityId[];
@@ -5,7 +5,8 @@ import { parse as parseToml } from "smol-toml";
5
5
  import { CLAUDE_MCP_SERVER_NAMES } from "./claude-mcp-config.js";
6
6
  import { getAvailableCliInfo } from "./model-registry.js";
7
7
  import { CLI_TYPES } from "./session-manager.js";
8
- import { isXaiProviderEnabled, loadProvidersConfig } from "./config.js";
8
+ import { enabledApiProviders, isXaiProviderEnabled, loadProvidersConfig, } from "./config.js";
9
+ import { apiContinuityForKind } from "./api-provider.js";
9
10
  const MAX_SKILLS_PER_DIR = 100;
10
11
  const MAX_SKILL_BYTES = 64 * 1024;
11
12
  const MAX_CONFIG_BYTES = 128 * 1024;
@@ -59,6 +60,10 @@ export const ACP_CONTRACT = {
59
60
  classification: "native_candidate",
60
61
  summary: "Cognition Devin CLI exposes a native ACP server via `devin acp` (stdio); Slice D1 initialize + session/new smoke passed (protocolVersion 1, third runtime pilot). Runtime routing stays config-gated.",
61
62
  },
63
+ cursor: {
64
+ classification: "native_candidate",
65
+ summary: "Cursor Agent CLI exposes a hidden native ACP stdio entrypoint via `cursor-agent acp`; initialize + session/new smoke passed locally (protocolVersion 1, session created).",
66
+ },
62
67
  },
63
68
  };
64
69
  const ACP_DOCS_REFERENCE = "docs/plans/first-class-acp-gateway-extension.dag.toml";
@@ -160,6 +165,20 @@ const ACP_CAPABILITIES = {
160
165
  ],
161
166
  docs: ACP_DOCS_REFERENCE,
162
167
  },
168
+ cursor: {
169
+ status: "native_smoke_passed",
170
+ mediation: "native",
171
+ targetVersion: "cursor-agent 2026.06.29-2ad2186",
172
+ entrypoint: { command: "cursor-agent", args: ["acp"] },
173
+ runtimeEnabled: false,
174
+ smokeSupported: true,
175
+ smokeStatus: "passed",
176
+ caveats: [
177
+ "Native ACP via hidden `cursor-agent acp` stdio JSON-RPC entrypoint; manual initialize + session/new smoke passed locally (protocolVersion 1, session created; no agentInfo returned).",
178
+ "Runtime routing stays disabled until ACP is enabled in gateway config.",
179
+ ],
180
+ docs: ACP_DOCS_REFERENCE,
181
+ },
163
182
  };
164
183
  function cloneAcpCapability(acp) {
165
184
  return {
@@ -805,11 +824,76 @@ const TOOL_CONTROLS = {
805
824
  },
806
825
  ],
807
826
  },
827
+ cursor: {
828
+ providerKind: "cli",
829
+ gatewayRequestTools: ["cursor_request", "cursor_request_async"],
830
+ summary: "Cursor Agent CLI runs a headless agentic coding/review session; the gateway passes prompt, model, mode, sandbox/trust controls, workspace roots, and session resume on the CLI transport, with ACP transport gated and fail-closed.",
831
+ controls: {
832
+ allowlist: {
833
+ supported: false,
834
+ behavior: "Cursor Agent exposes force/auto-review modes, not per-request allow lists.",
835
+ },
836
+ denylist: {
837
+ supported: false,
838
+ behavior: "Cursor Agent has no per-request deny-list flag on the tracked surface.",
839
+ },
840
+ mcpServers: {
841
+ supported: false,
842
+ requestField: "mcpServers",
843
+ behavior: "Cursor manages its own MCP configuration via `cursor-agent mcp`; the gateway does not mutate Cursor MCP config.",
844
+ },
845
+ nativeSkills: {
846
+ supported: false,
847
+ behavior: "Cursor owns rules/plugins; the gateway does not discover Cursor-native rules as skills.",
848
+ },
849
+ permissionMode: {
850
+ supported: true,
851
+ requestField: "mode/force/autoReview/sandbox/trust/approvalStrategy/approvalPolicy",
852
+ cliFlag: "--mode/--force/--auto-review/--sandbox/--trust",
853
+ behavior: 'Cursor supports read-only plan/ask modes, Smart Auto-review, force/yolo, sandbox overrides, and workspace trust in headless mode; under approvalStrategy:"mcp_managed", high-impact force/trust/sandbox-disabled requests are gated by the gateway approval manager.',
854
+ },
855
+ promptControl: {
856
+ supported: true,
857
+ requestField: "prompt",
858
+ behavior: "Prompt is passed as the positional prompt to `cursor-agent --print`.",
859
+ },
860
+ session: {
861
+ supported: true,
862
+ requestField: "sessionId/resumeLatest/createNewSession",
863
+ cliFlag: "--resume/--continue",
864
+ behavior: "Resumes a Cursor chat/session (--resume <id>) or the most recent chat (--continue); gateway-created gw-* session ids are tracking ids and are not resumable Cursor chat ids.",
865
+ },
866
+ workspace: {
867
+ supported: true,
868
+ requestField: "workspace/addDir",
869
+ cliFlag: "--workspace/--add-dir",
870
+ behavior: "Sets the Cursor workspace and additional workspace roots; remote HTTP/OAuth callers must use registered workspace aliases/roots rather than raw paths.",
871
+ },
872
+ },
873
+ features: baseFeatures({
874
+ sessionContinuity: true,
875
+ approvalAndSandboxControls: true,
876
+ promptControl: true,
877
+ workspaceControls: true,
878
+ }),
879
+ unsupportedInputs: [
880
+ {
881
+ input: "mcpServers",
882
+ behavior: "not_supported",
883
+ details: "Cursor owns MCP config via `cursor-agent mcp`; gateway request-time MCP server injection is not implemented.",
884
+ },
885
+ {
886
+ input: 'transport:"acp" with CLI-only options',
887
+ behavior: "reject",
888
+ details: "Cursor ACP routing currently accepts prompt/model/session inputs only; mode, outputFormat, workspace, addDir, force, autoReview, sandbox, trust, and prompt/response optimization are rejected instead of silently ignored.",
889
+ },
890
+ ],
891
+ },
808
892
  };
809
893
  const CAPABILITY_CACHE = new Map();
810
894
  export function getProviderToolCapabilities(queryOrCli = {}) {
811
895
  const query = normalizeQuery(queryOrCli);
812
- const providers = query.cli ? [query.cli] : PROVIDER_CAPABILITY_IDS;
896
+ const providers = query.cli ? [query.cli] : allProviderCapabilityIds(query.providersConfig);
813
897
  const entries = providers.map(provider => [
814
898
  provider,
815
899
  getOneProviderToolCapabilities(provider, query),
@@ -833,14 +917,35 @@ export function clearProviderToolCapabilitiesCache() {
833
917
  export function _getCapabilityCacheForTest() {
834
918
  return CAPABILITY_CACHE;
835
919
  }
836
- export function providerCapabilityIds() {
920
+ function providersConfigForQuery(query) {
921
+ return query.providersConfig ?? loadProvidersConfig();
922
+ }
923
+ function enabledApiCapabilityIds(providersConfig) {
924
+ return enabledApiProviders(providersConfig ?? loadProvidersConfig()).map(provider => provider.name);
925
+ }
926
+ function allProviderCapabilityIds(providersConfig) {
927
+ return [
928
+ ...new Set([
929
+ ...PROVIDER_CAPABILITY_IDS,
930
+ ...enabledApiCapabilityIds(providersConfig),
931
+ ]),
932
+ ];
933
+ }
934
+ export function providerCapabilityIds(providersConfig) {
935
+ return allProviderCapabilityIds(providersConfig);
936
+ }
937
+ export function knownProviderCapabilityIds() {
837
938
  return PROVIDER_CAPABILITY_IDS;
838
939
  }
839
940
  function buildOneProviderToolCapabilities(cli, query) {
840
941
  const warnings = [];
841
942
  if (!isKnownProviderCapabilityId(cli)) {
842
- throw new Error(`No tool-capability metadata for provider "${cli}". ` +
843
- `Known providers: ${PROVIDER_CAPABILITY_IDS.join(", ")}.`);
943
+ const runtime = enabledApiProviders(providersConfigForQuery(query)).find(provider => provider.name === cli);
944
+ if (!runtime) {
945
+ throw new Error(`No tool-capability metadata for provider "${cli}". ` +
946
+ `Known providers: ${allProviderCapabilityIds(query.providersConfig).join(", ")}.`);
947
+ }
948
+ return buildApiProviderToolCapabilities(runtime, query);
844
949
  }
845
950
  const definition = TOOL_CONTROLS[cli];
846
951
  const discoveredSkills = query.includeSkills && cli !== "grok_api" ? discoverSkills(cli, warnings, query) : [];
@@ -848,7 +953,7 @@ function buildOneProviderToolCapabilities(cli, query) {
848
953
  ? extractProviderTools(cli, discoveredSkills)
849
954
  : [];
850
955
  const features = { ...definition.features };
851
- const gatewayRequestTools = cli === "grok_api" && !isXaiProviderEnabled(loadProvidersConfig())
956
+ const gatewayRequestTools = cli === "grok_api" && !isXaiProviderEnabled(providersConfigForQuery(query))
852
957
  ? []
853
958
  : [...definition.gatewayRequestTools];
854
959
  if (cli === "grok") {
@@ -864,7 +969,7 @@ function buildOneProviderToolCapabilities(cli, query) {
864
969
  providerKind: definition.providerKind,
865
970
  gatewayRequestTools,
866
971
  gatewayRequestTool: gatewayRequestTools[0] ?? definition.gatewayRequestTools[0],
867
- modelInfo: getModelInfo(cli, query.refresh),
972
+ modelInfo: getModelInfo(cli, query),
868
973
  summary: definition.summary,
869
974
  acpContract: { ...ACP_CONTRACT.providers[cli] },
870
975
  acp: cloneAcpCapability(ACP_CAPABILITIES[cli]),
@@ -883,6 +988,172 @@ function buildOneProviderToolCapabilities(cli, query) {
883
988
  },
884
989
  };
885
990
  }
991
+ function apiProviderCapabilityDefinition(runtime) {
992
+ const { name, kind } = runtime;
993
+ const forwardsReasoning = kind === "xai-responses";
994
+ const continuity = apiContinuityForKind(kind);
995
+ const continuityTracked = continuity !== "none";
996
+ return {
997
+ providerKind: "api",
998
+ gatewayRequestTools: [`api_${name}_request`],
999
+ summary: `Generic ${kind} API provider "${name}" configured through [providers.${name}]. ` +
1000
+ "HTTP request tool only: no local CLI tools, skills, MCP servers, or workspaces.",
1001
+ controls: {
1002
+ allowlist: {
1003
+ supported: false,
1004
+ behavior: `api_${name}_request has no CLI tool allow-list input.`,
1005
+ },
1006
+ denylist: {
1007
+ supported: false,
1008
+ behavior: `api_${name}_request has no CLI tool deny-list input.`,
1009
+ },
1010
+ mcpServers: {
1011
+ supported: false,
1012
+ behavior: "API requests do not configure or expose MCP servers.",
1013
+ },
1014
+ nativeSkills: {
1015
+ supported: false,
1016
+ behavior: "API requests do not read local provider skills.",
1017
+ },
1018
+ reasoningEffort: forwardsReasoning
1019
+ ? {
1020
+ supported: true,
1021
+ requestField: "reasoningEffort",
1022
+ behavior: "Passed to the xAI Responses API reasoning.effort field.",
1023
+ }
1024
+ : {
1025
+ supported: false,
1026
+ requestField: "reasoningEffort",
1027
+ behavior: `Accepted by the schema but ignored by the ${kind} adapter.`,
1028
+ },
1029
+ maxOutputTokens: {
1030
+ supported: true,
1031
+ requestField: "maxOutputTokens",
1032
+ behavior: "Bounds the provider API max output tokens.",
1033
+ },
1034
+ sampling: {
1035
+ supported: true,
1036
+ requestField: "temperature/topP",
1037
+ behavior: "Sampling controls are passed through to the provider API.",
1038
+ },
1039
+ timeout: {
1040
+ supported: true,
1041
+ requestField: "timeoutMs",
1042
+ behavior: "Bounds the API HTTP request timeout.",
1043
+ },
1044
+ session: continuityTracked
1045
+ ? {
1046
+ supported: true,
1047
+ requestField: "sessionId/createNewSession",
1048
+ behavior: continuity === "server-side-id"
1049
+ ? "Gateway stores the provider continuation handle (previous_response_id) in session metadata."
1050
+ : "Gateway tracks the session (active/owner); stateless adapters resend prior context caller-side without storing conversation content.",
1051
+ }
1052
+ : {
1053
+ supported: false,
1054
+ requestField: "sessionId/createNewSession",
1055
+ behavior: "This provider kind does not support multi-turn continuity.",
1056
+ },
1057
+ },
1058
+ features: baseFeatures({
1059
+ apiProvider: true,
1060
+ structuredTextResponses: true,
1061
+ sessionContinuity: continuityTracked,
1062
+ }),
1063
+ unsupportedInputs: [
1064
+ {
1065
+ input: "localSkills",
1066
+ behavior: "not_supported",
1067
+ details: `api_${name}_request does not inspect local CLI skills.`,
1068
+ },
1069
+ {
1070
+ input: "allowedTools/disallowedTools",
1071
+ behavior: "not_supported",
1072
+ details: "Tool allow/deny controls are CLI-only and are not routed to the API.",
1073
+ },
1074
+ {
1075
+ input: "workspace/worktree",
1076
+ behavior: "not_supported",
1077
+ details: "API providers have no local workspace or worktree controls.",
1078
+ },
1079
+ ],
1080
+ };
1081
+ }
1082
+ function apiProviderModelInfo(runtime) {
1083
+ const list = runtime.models && runtime.models.length > 0 ? runtime.models : [runtime.defaultModel];
1084
+ const models = {};
1085
+ for (const model of list) {
1086
+ models[model] =
1087
+ model === runtime.defaultModel ? "Configured default model" : "Configured allowlisted model";
1088
+ }
1089
+ if (!(runtime.defaultModel in models)) {
1090
+ models[runtime.defaultModel] = "Configured default model (always permitted)";
1091
+ }
1092
+ return {
1093
+ description: `Generic ${runtime.kind} API provider configured through [providers.${runtime.name}].`,
1094
+ models,
1095
+ defaultModel: runtime.defaultModel,
1096
+ defaultModelSource: `[providers.${runtime.name}].default_model`,
1097
+ };
1098
+ }
1099
+ function apiProviderConfigSurfaces(runtime) {
1100
+ return [
1101
+ {
1102
+ name: `providers.${runtime.name}`,
1103
+ kind: "gateway",
1104
+ present: true,
1105
+ details: `Generic ${runtime.kind} API provider; secret key material is read only from the named environment variable at request time.`,
1106
+ },
1107
+ {
1108
+ name: "api_key_env",
1109
+ kind: "env",
1110
+ present: runtime.apiKey.length > 0,
1111
+ entries: runtime.apiKeyEnv ? [runtime.apiKeyEnv] : [],
1112
+ details: "Reports only the configured environment variable name and whether a key is resolved (keyless-local providers report false); never the value.",
1113
+ },
1114
+ ];
1115
+ }
1116
+ function buildApiProviderToolCapabilities(runtime, query) {
1117
+ const definition = apiProviderCapabilityDefinition(runtime);
1118
+ return {
1119
+ schemaVersion: "provider-tool-capabilities.v2",
1120
+ generatedAt: new Date().toISOString(),
1121
+ cli: runtime.name,
1122
+ providerKind: "api",
1123
+ gatewayRequestTools: [...definition.gatewayRequestTools],
1124
+ gatewayRequestTool: definition.gatewayRequestTools[0],
1125
+ modelInfo: apiProviderModelInfo(runtime),
1126
+ summary: definition.summary,
1127
+ acpContract: {
1128
+ classification: "absent_watchlist",
1129
+ summary: `${runtime.name} is an HTTP API provider with no ACP process transport; watchlist item only.`,
1130
+ },
1131
+ acp: {
1132
+ status: "not_applicable",
1133
+ mediation: "none",
1134
+ targetVersion: `${runtime.kind} API`,
1135
+ entrypoint: null,
1136
+ runtimeEnabled: false,
1137
+ smokeSupported: false,
1138
+ smokeStatus: "unsupported",
1139
+ caveats: ["ACP is a CLI-stdio transport; the HTTP API provider has no ACP surface."],
1140
+ docs: ACP_DOCS_REFERENCE,
1141
+ },
1142
+ controls: cloneControls(definition.controls),
1143
+ features: { ...definition.features },
1144
+ discoveredSkills: [],
1145
+ discoveredProviderTools: [],
1146
+ configSurfaces: apiProviderConfigSurfaces(runtime),
1147
+ unsupportedInputs: query.includeUnsupported ? [...definition.unsupportedInputs] : [],
1148
+ warnings: [],
1149
+ metadata: {
1150
+ deprecatedFields: {
1151
+ gatewayRequestTool: "Use gatewayRequestTools instead.",
1152
+ },
1153
+ cacheTtlMs: CAPABILITY_CACHE_TTL_MS,
1154
+ },
1155
+ };
1156
+ }
886
1157
  function discoverSkills(cli, warnings, query) {
887
1158
  const skills = [];
888
1159
  for (const root of skillRoots(cli)) {
@@ -925,6 +1196,8 @@ function skillRoots(cli) {
925
1196
  return [{ path: path.join(home, ".vibe", "skills"), source: "user" }];
926
1197
  case "devin":
927
1198
  return [];
1199
+ case "cursor":
1200
+ return [];
928
1201
  }
929
1202
  }
930
1203
  function readSkill(cli, skillPath, fallbackName, source, warnings, query) {
@@ -960,6 +1233,7 @@ function normalizeQuery(queryOrCli) {
960
1233
  includeUnsupported: query.includeUnsupported ?? true,
961
1234
  includePaths: query.includePaths ?? false,
962
1235
  refresh: query.refresh ?? false,
1236
+ providersConfig: query.providersConfig,
963
1237
  };
964
1238
  }
965
1239
  function capabilityCacheKey(cli, query) {
@@ -969,8 +1243,33 @@ function capabilityCacheKey(cli, query) {
969
1243
  includeProviderTools: query.includeProviderTools,
970
1244
  includeUnsupported: query.includeUnsupported,
971
1245
  includePaths: query.includePaths,
1246
+ providersConfig: query.providersConfig ? providerConfigCacheKey(query.providersConfig) : null,
972
1247
  });
973
1248
  }
1249
+ function providerConfigCacheKey(config) {
1250
+ return {
1251
+ xai: config.xai
1252
+ ? {
1253
+ apiKeyEnv: config.xai.apiKeyEnv,
1254
+ baseUrl: config.xai.baseUrl,
1255
+ defaultModel: config.xai.defaultModel,
1256
+ }
1257
+ : null,
1258
+ providers: Object.fromEntries(Object.entries(config.providers ?? {})
1259
+ .sort(([a], [b]) => a.localeCompare(b))
1260
+ .map(([name, provider]) => [
1261
+ name,
1262
+ {
1263
+ kind: provider.kind,
1264
+ apiKeyEnv: provider.apiKeyEnv,
1265
+ baseUrl: provider.baseUrl,
1266
+ defaultModel: provider.defaultModel,
1267
+ models: provider.models ?? null,
1268
+ usageInclude: provider.usageInclude ?? null,
1269
+ },
1270
+ ])),
1271
+ };
1272
+ }
974
1273
  function baseFeatures(overrides) {
975
1274
  const names = [
976
1275
  "gatewayRequestTools",
@@ -1012,11 +1311,11 @@ function baseFeatures(overrides) {
1012
1311
  function cloneControls(controls) {
1013
1312
  return Object.fromEntries(Object.entries(controls).map(([name, control]) => [name, { name, ...control }]));
1014
1313
  }
1015
- function getModelInfo(cli, refresh) {
1314
+ function getModelInfo(cli, query) {
1016
1315
  if (cli !== "grok_api") {
1017
- return getAvailableCliInfo(refresh)[cli];
1316
+ return getAvailableCliInfo(query.refresh)[cli];
1018
1317
  }
1019
- const providers = loadProvidersConfig();
1318
+ const providers = providersConfigForQuery(query);
1020
1319
  const enabled = isXaiProviderEnabled(providers);
1021
1320
  const defaultModel = providers.xai?.defaultModel;
1022
1321
  return {
@@ -1031,7 +1330,7 @@ function getModelInfo(cli, refresh) {
1031
1330
  }
1032
1331
  function discoverConfigSurfaces(cli, query, discoveredSkills) {
1033
1332
  if (cli === "grok_api") {
1034
- const providers = loadProvidersConfig();
1333
+ const providers = providersConfigForQuery(query);
1035
1334
  return [
1036
1335
  {
1037
1336
  name: "providers.xai",