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/index.js CHANGED
@@ -18,7 +18,7 @@ import { createWorktree, createWorktreeSessionCleanupHook, } from "./worktree-ma
18
18
  import { ResourceProvider } from "./resources.js";
19
19
  import { PerformanceMetrics } from "./metrics.js";
20
20
  import { estimateTokens, optimizePrompt as optimizePromptText, optimizeResponse as optimizeResponseText, } from "./optimizer.js";
21
- import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, loadProvidersConfig, loadAcpConfig, defaultGatewayConfigPath, isXaiProviderEnabled, enabledApiProviders, minStableTokensForModel, } from "./config.js";
21
+ import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, loadProvidersConfig, loadAcpConfig, loadLimitsConfig, defaultGatewayConfigPath, isXaiProviderEnabled, enabledApiProviders, minStableTokensForModel, } from "./config.js";
22
22
  import { runAcpRequest } from "./acp/runtime.js";
23
23
  import { isAcpError } from "./acp/errors.js";
24
24
  import { createApiProvider, runApiRequest, apiProviderBreakerState, } from "./api-provider.js";
@@ -26,7 +26,7 @@ import { prepareApiRequest, apiProviderCatalogEntry, ApiModelNotAllowedError, }
26
26
  import { checkHealth } from "./health.js";
27
27
  import { clearModelRegistryCache, getAvailableCliInfo, getCliInfo, resolveModelAlias, } from "./model-registry.js";
28
28
  import { getProviderToolCapabilities } from "./provider-tool-capabilities.js";
29
- import { AsyncJobManager, extractApiHttpStatus, extractApiErrorBody, } from "./async-job-manager.js";
29
+ import { AsyncJobManager, JobSaturationError, isAsyncJobInProgress, extractApiHttpStatus, extractApiErrorBody, } from "./async-job-manager.js";
30
30
  import { createJobStore } from "./job-store.js";
31
31
  import { ApprovalManager, bypassAllowedByOperator, } from "./approval-manager.js";
32
32
  import { checkReviewIntegrity } from "./review-integrity.js";
@@ -39,6 +39,7 @@ import { getCliVersions, runCliUpgrade } from "./cli-updater.js";
39
39
  import { startHttpGateway } from "./http-transport.js";
40
40
  import { getRequestContext, resolveOwnerPrincipal, principalCanAccess } from "./request-context.js";
41
41
  import { printDoctorJson } from "./doctor.js";
42
+ import { redactDiagnosticUrl } from "./endpoint-exposure.js";
42
43
  import { createWorkspace, describeWorkspace, getWorkspace, loadWorkspaceRegistry, registerExistingWorkspace, resolveWorkspaceForProvider, validatePathInsideWorkspace, } from "./workspace-registry.js";
43
44
  import { generateSecret, hashSecret } from "./oauth.js";
44
45
  import { registerValidationTools } from "./validation-tools.js";
@@ -161,16 +162,17 @@ export function buildServerInstructions(asyncJobsEnabled, grokApiToolsEnabled =
161
162
  : '- Async jobs are DISABLED (persistence.backend = "none"): *_request_async and llm_job_* tools are not registered, and sync requests run to completion (no auto-deferral).';
162
163
  return `llm-cli-gateway: Multi-LLM orchestration via MCP.
163
164
 
164
- Tools: claude_request, codex_request, gemini_request, grok_request, mistral_request, devin_request${apiToolsNote} (sync)${asyncToolsNote} | codex_fork_session (fork a Codex session into a new branch)
165
+ Tools: claude_request, codex_request, gemini_request, grok_request, mistral_request, devin_request, cursor_request${apiToolsNote} (sync)${asyncToolsNote} | codex_fork_session (fork a Codex session into a new branch)
165
166
  Validation: validate_with_models, second_opinion, compare_answers, red_team_review, consensus_check, ask_model, synthesize_validation, list_available_models | job_status/job_result (validation jobs)
166
167
  ${jobsLine}Sessions: session_create, session_list, session_set_active, session_get, session_delete, session_clear_all
167
168
  Other: list_models, provider_tool_capabilities, cli_versions, upstream_contracts, provider_subcommands_* (read-only subcommand contract/drift introspection), cli_upgrade, approval_list, llm_process_health, llm_request_result (read back any persisted request, sync or async, by correlationId)
168
- Workspaces: workspace_create, workspace_list, workspace_get, workspace_register_existing_repo
169
+ Workspaces: workspace_create, workspace_list, workspace_get, workspace_register_existing_repo (remote HTTP/OAuth workspace registry only; do not use workspace_* to fix stdio/local provider path access)
169
170
 
170
171
  Key behaviors:
171
172
  ${deferralLine}
172
173
  - Sessions: Claude --continue, Gemini (Antigravity) --conversation <id>/--continue, Grok --resume/--continue, Mistral --resume/--continue (current Vibe defaults session logging on; doctor flags explicit session_logging.enabled=false), Codex \`exec resume <ID>\` / \`exec resume --last\`, Devin --resume <id>/--continue (all real CLI continuity). For Codex, sessionId must be a real Codex UUID (from ~/.codex/sessions/); gateway-generated gw-* IDs are rejected.
173
174
  - Approval gates: opt-in via approvalStrategy:"mcp_managed".
175
+ - Stdio/local provider calls may pass local workingDir/addDir/includeDirs directly. Workspace aliases are for remote HTTP/OAuth calls and must not be used as a fallback after a stdio provider path error.
174
176
  - Upstream drift detection: After upgrading any provider CLI (especially grok), use upstream_contracts with probeInstalled:true and provider_subcommand_drift for declared subcommand help surfaces. Probes are safe, read-only --help checks.
175
177
  - Idle timeout kills stuck processes (default 10min, configurable via idleTimeoutMs).
176
178
 
@@ -189,6 +191,7 @@ let persistenceConfig = null;
189
191
  let cacheAwarenessConfig = null;
190
192
  let providersConfig = null;
191
193
  let acpConfig = null;
194
+ let limitsConfig = null;
192
195
  let jobStore = null;
193
196
  let jobStoreInitialized = false;
194
197
  let asyncJobManager = null;
@@ -201,6 +204,10 @@ function getPersistenceConfig(runtimeLogger = logger) {
201
204
  persistenceConfig ??= loadPersistenceConfig(runtimeLogger);
202
205
  return persistenceConfig;
203
206
  }
207
+ function getLimitsConfig(runtimeLogger = logger) {
208
+ limitsConfig ??= loadLimitsConfig(runtimeLogger);
209
+ return limitsConfig;
210
+ }
204
211
  function getAcpConfig(runtimeLogger = logger) {
205
212
  acpConfig ??= loadAcpConfig(runtimeLogger);
206
213
  return acpConfig;
@@ -229,7 +236,7 @@ function getJobStore(runtimeLogger = logger) {
229
236
  function newAsyncJobManager(metrics, runtimeLogger, store = getJobStore(runtimeLogger), fr = getFlightRecorder(runtimeLogger)) {
230
237
  return new AsyncJobManager(runtimeLogger, (cli, durationMs, success) => {
231
238
  metrics.recordRequest(cli, durationMs, success);
232
- }, store, fr);
239
+ }, store, fr, getLimitsConfig(runtimeLogger).jobs);
233
240
  }
234
241
  function getAsyncJobManager(runtimeLogger = logger) {
235
242
  asyncJobManager ??= newAsyncJobManager(performanceMetrics, runtimeLogger);
@@ -278,7 +285,14 @@ export const WORKSPACE_ALIAS_SCHEMA = z
278
285
  .min(1)
279
286
  .max(64)
280
287
  .regex(/^[A-Za-z][A-Za-z0-9._-]{0,63}$/)
281
- .describe("Registered workspace alias. Remote clients use aliases, not absolute paths.");
288
+ .describe("Registered workspace alias for remote HTTP/OAuth provider calls. Stdio/local callers should not use workspace_* tools to fix provider path access; pass local workingDir/addDir/includeDirs directly.");
289
+ const PROVIDER_WORKSPACE_FIELD_DESCRIPTION = "Registered workspace alias for remote HTTP/OAuth provider calls. Do not use this field, workspace_list, or workspace_register_existing_repo as a fallback for stdio/local provider path access; pass workingDir/addDir/includeDirs directly instead.";
290
+ const LOCAL_WORKING_DIR_FIELD_SUFFIX = " Stdio/local callers may pass local paths directly. Remote HTTP/OAuth callers must use relative paths inside a selected registered workspace. Do not call workspace_* tools to fix stdio/local provider path access.";
291
+ const LOCAL_ADD_DIR_FIELD_SUFFIX = " Stdio/local callers may pass local paths directly. Remote HTTP/OAuth callers must use relative paths inside a selected registered workspace. Do not call workspace_* tools to fix stdio/local provider path access.";
292
+ const LOCAL_INCLUDE_DIRS_FIELD_SUFFIX = " Stdio/local callers may pass local paths directly. Remote HTTP/OAuth callers must use relative paths inside a selected registered workspace. Do not call workspace_* tools to fix stdio/local provider path access.";
293
+ function providerWorkspaceAliasSchema() {
294
+ return WORKSPACE_ALIAS_SCHEMA.describe(PROVIDER_WORKSPACE_FIELD_DESCRIPTION).optional();
295
+ }
282
296
  export const SESSION_PROVIDER_VALUES = PROVIDER_TYPES;
283
297
  export const SESSION_PROVIDER_ENUM = z.enum(SESSION_PROVIDER_VALUES);
284
298
  export function sessionProviderValuesFor(providers) {
@@ -307,7 +321,7 @@ export function resolveGatewayServerRuntime(deps = {}, options = {}) {
307
321
  sessionManager: runtimeSessionManager,
308
322
  resourceProvider: deps.resourceProvider ??
309
323
  (options.isolateState
310
- ? new ResourceProvider(runtimeSessionManager, runtimePerformanceMetrics, runtimeFlightRecorder, deps.cacheAwareness ?? getCacheAwarenessConfig(runtimeLogger))
324
+ ? new ResourceProvider(runtimeSessionManager, runtimePerformanceMetrics, runtimeFlightRecorder, deps.cacheAwareness ?? getCacheAwarenessConfig(runtimeLogger), deps.providers ?? getProvidersConfig(runtimeLogger))
311
325
  : resourceProvider),
312
326
  db: "db" in deps ? (deps.db ?? null) : db,
313
327
  performanceMetrics: runtimePerformanceMetrics,
@@ -331,6 +345,7 @@ const CLI_IDLE_TIMEOUTS = {
331
345
  gemini: 600_000,
332
346
  grok: 600_000,
333
347
  mistral: 600_000,
348
+ cursor: 600_000,
334
349
  };
335
350
  function resolveIdleTimeout(cli, override) {
336
351
  if (override !== undefined)
@@ -402,6 +417,14 @@ async function awaitJobOrDefer(cli, args, corrId, idleTimeoutMs, outputFormat, f
402
417
  runtime.asyncJobManager.hasStore();
403
418
  if (SYNC_DEADLINE_MS === 0 || !deferralAvailable) {
404
419
  const command = providerCommandName(cli);
420
+ let slot;
421
+ try {
422
+ slot = await runtime.asyncJobManager.acquireProcessSlot(cli);
423
+ }
424
+ catch (err) {
425
+ consumeOnComplete();
426
+ throw err;
427
+ }
405
428
  try {
406
429
  return await executeCli(command, args, {
407
430
  idleTimeout: idleTimeoutMs,
@@ -412,6 +435,7 @@ async function awaitJobOrDefer(cli, args, corrId, idleTimeoutMs, outputFormat, f
412
435
  });
413
436
  }
414
437
  finally {
438
+ slot.release();
415
439
  consumeOnComplete();
416
440
  }
417
441
  }
@@ -441,7 +465,7 @@ async function awaitJobOrDefer(cli, args, corrId, idleTimeoutMs, outputFormat, f
441
465
  const deadline = Date.now() + SYNC_DEADLINE_MS;
442
466
  while (Date.now() < deadline) {
443
467
  const snapshot = runtime.asyncJobManager.getJobSnapshot(job.id);
444
- if (snapshot && snapshot.status !== "running") {
468
+ if (snapshot && !isAsyncJobInProgress(snapshot.status)) {
445
469
  const result = runtime.asyncJobManager.getJobResult(job.id);
446
470
  if (!result) {
447
471
  return { stdout: "", stderr: "Job result unavailable", code: 1 };
@@ -481,6 +505,14 @@ async function awaitApiJobOrDefer(provider, apiRequest, corrId, runtime = resolv
481
505
  runtime.persistence.asyncJobsEnabled &&
482
506
  runtime.asyncJobManager.hasStore();
483
507
  if (SYNC_DEADLINE_MS === 0 || !deferralAvailable || forceInline) {
508
+ let slot;
509
+ try {
510
+ slot = await runtime.asyncJobManager.acquireProcessSlot(provider.name);
511
+ }
512
+ catch (err) {
513
+ consumeOnComplete();
514
+ throw err;
515
+ }
484
516
  try {
485
517
  const result = await runApiRequest(provider, apiRequest, runtime.logger);
486
518
  return {
@@ -504,6 +536,7 @@ async function awaitApiJobOrDefer(provider, apiRequest, corrId, runtime = resolv
504
536
  };
505
537
  }
506
538
  finally {
539
+ slot.release();
507
540
  consumeOnComplete();
508
541
  }
509
542
  }
@@ -531,7 +564,7 @@ async function awaitApiJobOrDefer(provider, apiRequest, corrId, runtime = resolv
531
564
  const deadline = Date.now() + SYNC_DEADLINE_MS;
532
565
  while (Date.now() < deadline) {
533
566
  const snapshot = runtime.asyncJobManager.getJobSnapshot(job.id);
534
- if (snapshot && snapshot.status !== "running") {
567
+ if (snapshot && !isAsyncJobInProgress(snapshot.status)) {
535
568
  const result = runtime.asyncJobManager.getJobResult(job.id);
536
569
  if (!result)
537
570
  return { stdout: "", stderr: "Job result unavailable", code: 1 };
@@ -678,38 +711,50 @@ function workspaceAdminEnabled() {
678
711
  const scopes = getRequestContext()?.authScopes ?? [];
679
712
  return process.env.LLM_GATEWAY_WORKSPACE_ADMIN === "1" && scopes.includes("workspace:admin");
680
713
  }
714
+ function assertWorkspaceToolCaller(toolName) {
715
+ const context = getRequestContext();
716
+ if (context?.transport === "http" || context?.authKind === "oauth")
717
+ return;
718
+ throw new Error(`${toolName} is only for remote HTTP/OAuth workspace clients. Stdio/local provider calls must not use workspace_* tools for path access; pass workingDir/addDir/includeDirs directly on the provider request instead.`);
719
+ }
681
720
  function registerWorkspaceTools(server, runtime) {
682
- server.tool("workspace_list", "List registered workspace aliases and summary metadata. Does not browse files.", {}, {
721
+ server.tool("workspace_list", "List registered workspace aliases for remote HTTP/OAuth provider calls. Does not browse files. Stdio/local callers should not use workspace_* tools to fix provider path access; pass local workingDir/addDir/includeDirs directly.", {}, {
683
722
  title: "List workspaces",
684
723
  readOnlyHint: true,
685
724
  destructiveHint: false,
686
725
  idempotentHint: true,
687
726
  openWorldHint: false,
688
727
  }, async () => {
689
- const registry = loadWorkspaceRegistry(runtime.logger);
690
- return {
691
- content: [
692
- {
693
- type: "text",
694
- text: JSON.stringify({
695
- success: true,
696
- enabled: registry.enabled,
697
- default: registry.defaultAlias,
698
- workspaces: registry.repos.map(describeWorkspace),
699
- allowed_roots: registry.allowedRoots.map(root => ({
700
- alias: root.alias,
701
- path: root.path,
702
- allow_register_existing_git_repos: root.allowRegisterExistingGitRepos,
703
- allow_create_directories: root.allowCreateDirectories,
704
- allow_init_git_repos: root.allowInitGitRepos,
705
- max_create_depth: root.maxCreateDepth,
706
- })),
707
- }, null, 2),
708
- },
709
- ],
710
- };
728
+ try {
729
+ assertWorkspaceToolCaller("workspace_list");
730
+ const registry = loadWorkspaceRegistry(runtime.logger);
731
+ return {
732
+ content: [
733
+ {
734
+ type: "text",
735
+ text: JSON.stringify({
736
+ success: true,
737
+ enabled: registry.enabled,
738
+ default: registry.defaultAlias,
739
+ workspaces: registry.repos.map(describeWorkspace),
740
+ allowed_roots: registry.allowedRoots.map(root => ({
741
+ alias: root.alias,
742
+ path: root.path,
743
+ allow_register_existing_git_repos: root.allowRegisterExistingGitRepos,
744
+ allow_create_directories: root.allowCreateDirectories,
745
+ allow_init_git_repos: root.allowInitGitRepos,
746
+ max_create_depth: root.maxCreateDepth,
747
+ })),
748
+ }, null, 2),
749
+ },
750
+ ],
751
+ };
752
+ }
753
+ catch (error) {
754
+ return createErrorResponse("workspace_list", 1, "", undefined, error);
755
+ }
711
756
  });
712
- server.tool("workspace_get", "Inspect a registered workspace alias. Does not list files.", { alias: WORKSPACE_ALIAS_SCHEMA }, {
757
+ server.tool("workspace_get", "Inspect a registered remote HTTP/OAuth workspace alias. Does not list files. Not needed for stdio/local provider calls; do not use workspace_* tools to fix local path access.", { alias: WORKSPACE_ALIAS_SCHEMA }, {
713
758
  title: "Get workspace",
714
759
  readOnlyHint: true,
715
760
  destructiveHint: false,
@@ -717,6 +762,7 @@ function registerWorkspaceTools(server, runtime) {
717
762
  openWorldHint: false,
718
763
  }, async ({ alias }) => {
719
764
  try {
765
+ assertWorkspaceToolCaller("workspace_get");
720
766
  const registry = loadWorkspaceRegistry(runtime.logger);
721
767
  return {
722
768
  content: [
@@ -731,7 +777,7 @@ function registerWorkspaceTools(server, runtime) {
731
777
  return createErrorResponse("workspace_get", 1, "", undefined, error);
732
778
  }
733
779
  });
734
- server.tool("workspace_create", "Create a new local folder or git repo under a configured allowed root. Requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin.", {
780
+ server.tool("workspace_create", "Create a remote HTTP/OAuth workspace alias by creating a new local folder or git repo under a configured allowed root. Not for stdio/local provider path access. Requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin.", {
735
781
  alias: WORKSPACE_ALIAS_SCHEMA,
736
782
  root: WORKSPACE_ALIAS_SCHEMA.describe("Allowed-root alias from workspace_list."),
737
783
  slug: z.string().min(1).max(255).describe("Safe relative path under the allowed root."),
@@ -745,6 +791,7 @@ function registerWorkspaceTools(server, runtime) {
745
791
  openWorldHint: false,
746
792
  }, async ({ alias, root, slug, kind, setDefault }) => {
747
793
  try {
794
+ assertWorkspaceToolCaller("workspace_create");
748
795
  if (!workspaceAdminEnabled()) {
749
796
  throw new Error("workspace_create requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin");
750
797
  }
@@ -769,7 +816,7 @@ function registerWorkspaceTools(server, runtime) {
769
816
  return createErrorResponse("workspace_create", 1, "", undefined, error);
770
817
  }
771
818
  });
772
- server.tool("workspace_register_existing_repo", "Register an existing local Git repo under an allowed root. Requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin.", {
819
+ server.tool("workspace_register_existing_repo", "Register an existing local Git repo as a remote HTTP/OAuth workspace alias. Not for stdio/local provider path access. Requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin.", {
773
820
  alias: WORKSPACE_ALIAS_SCHEMA,
774
821
  path: z
775
822
  .string()
@@ -784,6 +831,7 @@ function registerWorkspaceTools(server, runtime) {
784
831
  openWorldHint: false,
785
832
  }, async ({ alias, path, setDefault }) => {
786
833
  try {
834
+ assertWorkspaceToolCaller("workspace_register_existing_repo");
787
835
  if (!workspaceAdminEnabled()) {
788
836
  throw new Error("workspace_register_existing_repo requires LLM_GATEWAY_WORKSPACE_ADMIN=1 and OAuth scope workspace:admin");
789
837
  }
@@ -808,6 +856,23 @@ function registerWorkspaceTools(server, runtime) {
808
856
  });
809
857
  }
810
858
  export function createErrorResponse(cli, code, stderr, correlationId, error, apiError) {
859
+ if (error instanceof JobSaturationError) {
860
+ const satMessage = `${error.message}. The gateway is protecting the host from process/request ` +
861
+ `overload; this is transient. Retry after a short delay, or reduce concurrent requests.`;
862
+ logger.error(`[${correlationId || "unknown"}] ${cli} rejected: gateway at capacity`);
863
+ return {
864
+ content: [{ type: "text", text: satMessage }],
865
+ isError: true,
866
+ structuredContent: {
867
+ response: satMessage,
868
+ correlationId: correlationId || null,
869
+ cli,
870
+ exitCode: code,
871
+ errorCategory: "saturated",
872
+ retryable: true,
873
+ },
874
+ };
875
+ }
811
876
  let errorMessage = `Error executing ${cli} CLI`;
812
877
  const isLaunchExit = code === 127 || code === -4058;
813
878
  const isOutputOverflow = Boolean(error) && /Output exceeded maximum size/i.test(error.message);
@@ -820,6 +885,7 @@ export function createErrorResponse(cli, code, stderr, correlationId, error, api
820
885
  grok: "Run `grok login` (or re-authenticate) and retry.",
821
886
  mistral: "Run `vibe --setup` (or re-authenticate) and retry.",
822
887
  devin: "Run `devin auth login` (or set WINDSURF_API_KEY) and retry.",
888
+ cursor: "Run `cursor-agent login` (or set CURSOR_API_KEY) and retry.",
823
889
  };
824
890
  if (error) {
825
891
  errorMessage += `:\n${error.message}`;
@@ -3583,6 +3649,347 @@ export async function handleDevinRequestAsync(deps, params) {
3583
3649
  return createErrorResponse("devin_request_async", 1, "", corrId, error);
3584
3650
  }
3585
3651
  }
3652
+ export function prepareCursorRequest(params, runtime) {
3653
+ const corrId = params.correlationId ?? randomUUID();
3654
+ let prompt = (params.prompt ?? "").trim();
3655
+ if (!prompt) {
3656
+ return createErrorResponse(params.operation, 1, "prompt is required and cannot be empty", corrId);
3657
+ }
3658
+ const reviewIntegrity = checkReviewIntegrity({ prompt });
3659
+ if (reviewIntegrity.violations.length > 0) {
3660
+ runtime.logger.info(`[${corrId}] Review integrity violations detected: ${reviewIntegrity.violations.map(v => v.type).join(", ")}`, {
3661
+ cli: "cursor",
3662
+ operation: params.operation,
3663
+ score: reviewIntegrity.totalScore,
3664
+ });
3665
+ }
3666
+ const highImpactRequested = Boolean(params.force) || Boolean(params.trust) || params.sandbox === "disabled";
3667
+ let approvalDecision = null;
3668
+ if (params.approvalStrategy === "mcp_managed") {
3669
+ approvalDecision = runtime.approvalManager.decide({
3670
+ cli: "cursor",
3671
+ operation: params.operation,
3672
+ prompt,
3673
+ bypassRequested: highImpactRequested,
3674
+ fullAuto: false,
3675
+ requestedMcpServers: [],
3676
+ policy: params.approvalPolicy,
3677
+ metadata: {
3678
+ model: params.model ?? "default",
3679
+ force: Boolean(params.force),
3680
+ trust: Boolean(params.trust),
3681
+ sandbox: params.sandbox ?? null,
3682
+ },
3683
+ reviewIntegrity,
3684
+ });
3685
+ if (approvalDecision.status !== "approved") {
3686
+ return createApprovalDeniedResponse(params.operation, approvalDecision);
3687
+ }
3688
+ }
3689
+ if (params.optimizePrompt)
3690
+ prompt = optimizePromptText(prompt);
3691
+ const resolvedModel = resolveModelAlias("cursor", params.model, getCliInfo());
3692
+ const args = ["--print"];
3693
+ const outputFormat = params.outputFormat ?? "text";
3694
+ if (outputFormat !== "text")
3695
+ args.push("--output-format", outputFormat);
3696
+ if (resolvedModel)
3697
+ args.push("--model", resolvedModel);
3698
+ if (params.mode)
3699
+ args.push("--mode", params.mode);
3700
+ if (params.force)
3701
+ args.push("--force");
3702
+ if (params.autoReview)
3703
+ args.push("--auto-review");
3704
+ if (params.sandbox)
3705
+ args.push("--sandbox", params.sandbox);
3706
+ if (params.trust)
3707
+ args.push("--trust");
3708
+ if (params.workspace)
3709
+ args.push("--workspace", params.workspace);
3710
+ for (const dir of params.addDir ?? [])
3711
+ args.push("--add-dir", dir);
3712
+ args.push(prompt);
3713
+ return {
3714
+ corrId,
3715
+ effectivePrompt: prompt,
3716
+ resolvedModel,
3717
+ requestedMcpServers: [],
3718
+ approvalDecision,
3719
+ reviewIntegrity,
3720
+ args,
3721
+ stablePrefixHash: null,
3722
+ stablePrefixTokens: null,
3723
+ };
3724
+ }
3725
+ function rejectUnsupportedCursorAcpParams(params, operation) {
3726
+ const unsupported = [];
3727
+ if (params.mode)
3728
+ unsupported.push("mode");
3729
+ if (params.outputFormat && params.outputFormat !== "text")
3730
+ unsupported.push("outputFormat");
3731
+ if (params.force)
3732
+ unsupported.push("force");
3733
+ if (params.autoReview)
3734
+ unsupported.push("autoReview");
3735
+ if (params.sandbox)
3736
+ unsupported.push("sandbox");
3737
+ if (params.trust)
3738
+ unsupported.push("trust");
3739
+ if (params.workspace)
3740
+ unsupported.push("workspace");
3741
+ if ((params.addDir ?? []).length > 0)
3742
+ unsupported.push("addDir");
3743
+ if (params.resumeLatest)
3744
+ unsupported.push("resumeLatest");
3745
+ if (params.createNewSession)
3746
+ unsupported.push("createNewSession");
3747
+ if (params.optimizePrompt)
3748
+ unsupported.push("optimizePrompt");
3749
+ if (params.optimizeResponse)
3750
+ unsupported.push("optimizeResponse");
3751
+ if (params.forceRefresh)
3752
+ unsupported.push("forceRefresh");
3753
+ if (unsupported.length === 0)
3754
+ return null;
3755
+ return createErrorResponse(operation, 1, "", params.correlationId, new Error(`cursor_request transport=acp does not support these Cursor CLI-only options yet: ${unsupported.join(", ")}. Omit them or use transport=cli.`));
3756
+ }
3757
+ export async function handleCursorRequest(deps, params) {
3758
+ if (params.transport === "acp") {
3759
+ const unsupported = rejectUnsupportedCursorAcpParams(params, "cursor_request");
3760
+ if (unsupported)
3761
+ return unsupported;
3762
+ return runAcpTransport(deps, {
3763
+ provider: "cursor",
3764
+ prompt: params.prompt,
3765
+ model: params.model,
3766
+ sessionId: params.sessionId,
3767
+ correlationId: params.correlationId,
3768
+ });
3769
+ }
3770
+ const runtime = resolveHandlerRuntime(deps);
3771
+ const startTime = Date.now();
3772
+ const prep = prepareCursorRequest({
3773
+ prompt: params.prompt,
3774
+ model: params.model,
3775
+ mode: params.mode,
3776
+ outputFormat: params.outputFormat,
3777
+ force: params.force,
3778
+ autoReview: params.autoReview,
3779
+ sandbox: params.sandbox,
3780
+ trust: params.trust,
3781
+ workspace: params.workspace,
3782
+ addDir: params.addDir,
3783
+ approvalStrategy: params.approvalStrategy,
3784
+ approvalPolicy: params.approvalPolicy,
3785
+ correlationId: params.correlationId,
3786
+ optimizePrompt: params.optimizePrompt,
3787
+ operation: "cursor_request",
3788
+ }, runtime);
3789
+ if (!("args" in prep))
3790
+ return prep;
3791
+ const { corrId, args } = prep;
3792
+ let durationMs = 0;
3793
+ let wasSuccessful = false;
3794
+ safeFlightStart({
3795
+ correlationId: corrId,
3796
+ cli: "cursor",
3797
+ model: prep.resolvedModel || "default",
3798
+ prompt: prep.effectivePrompt,
3799
+ sessionId: params.sessionId,
3800
+ }, runtime);
3801
+ try {
3802
+ const sessionResult = resolveGrokSessionArgs({
3803
+ sessionId: params.sessionId,
3804
+ resumeLatest: params.resumeLatest,
3805
+ createNewSession: params.createNewSession,
3806
+ });
3807
+ if (sessionResult.userProvidedSession) {
3808
+ await getExistingSessionForProvider(deps.sessionManager, sessionResult.effectiveSessionId, "cursor");
3809
+ }
3810
+ args.splice(args.length - 1, 0, ...sessionResult.resumeArgs);
3811
+ let worktreeResolution = {};
3812
+ try {
3813
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
3814
+ provider: "cursor",
3815
+ workspace: params.workspace,
3816
+ sessionId: sessionResult.effectiveSessionId,
3817
+ runtime,
3818
+ addDir: params.addDir,
3819
+ });
3820
+ }
3821
+ catch (err) {
3822
+ return createErrorResponse("cursor_request", 1, "", corrId, err);
3823
+ }
3824
+ if (worktreeResolution.workspace && params.workspace) {
3825
+ const workspaceFlagIndex = args.indexOf("--workspace");
3826
+ if (workspaceFlagIndex >= 0 && workspaceFlagIndex + 1 < args.length) {
3827
+ args[workspaceFlagIndex + 1] = worktreeResolution.workspace.cwd;
3828
+ }
3829
+ }
3830
+ const cursorFrHandoff = buildAsyncFlightRecorderHandoff("cursor", prep, params.sessionId, params.outputFormat);
3831
+ const result = await awaitJobOrDefer("cursor", args, corrId, resolveIdleTimeout("cursor", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, runtime, undefined, undefined, cursorFrHandoff.flightRecorderEntry, cursorFrHandoff.extractUsage, undefined, worktreeResolution.cwd);
3832
+ if (isDeferredResponse(result)) {
3833
+ return buildDeferredToolResponse(result, sessionResult.effectiveSessionId);
3834
+ }
3835
+ const { stdout, stderr, code } = result;
3836
+ durationMs = Math.max(0, Date.now() - startTime);
3837
+ if (code !== 0) {
3838
+ safeFlightComplete(corrId, {
3839
+ response: stderr || "",
3840
+ durationMs,
3841
+ retryCount: 0,
3842
+ circuitBreakerState: "closed",
3843
+ optimizationApplied: false,
3844
+ exitCode: code,
3845
+ errorMessage: stderr || `Exit code ${code}`,
3846
+ status: "failed",
3847
+ }, runtime);
3848
+ return createErrorResponse("cursor", code, stderr, corrId);
3849
+ }
3850
+ wasSuccessful = true;
3851
+ let effectiveSessionId = sessionResult.effectiveSessionId;
3852
+ if (sessionResult.userProvidedSession && effectiveSessionId) {
3853
+ const existing = await deps.sessionManager.getSession(effectiveSessionId);
3854
+ if (!existing) {
3855
+ try {
3856
+ await deps.sessionManager.createSession("cursor", "Cursor Session", effectiveSessionId);
3857
+ }
3858
+ catch {
3859
+ const rechecked = await deps.sessionManager.getSession(effectiveSessionId);
3860
+ if (!rechecked)
3861
+ throw new Error(`Failed to create or find session ${effectiveSessionId}`);
3862
+ }
3863
+ }
3864
+ await deps.sessionManager.updateSessionUsage(effectiveSessionId);
3865
+ }
3866
+ else if (!params.createNewSession && !effectiveSessionId) {
3867
+ const newSession = await deps.sessionManager.createSession("cursor", "Cursor Session", `${GATEWAY_SESSION_PREFIX}${randomUUID()}`);
3868
+ effectiveSessionId = newSession.id;
3869
+ }
3870
+ const response = buildCliResponse("cursor", stdout, params.optimizeResponse ?? false, corrId, effectiveSessionId, prep, durationMs, sessionResult.userProvidedSession, params.outputFormat);
3871
+ safeFlightComplete(corrId, {
3872
+ response: stdout,
3873
+ durationMs,
3874
+ retryCount: 0,
3875
+ circuitBreakerState: "closed",
3876
+ optimizationApplied: params.optimizePrompt || (params.optimizeResponse ?? false),
3877
+ exitCode: 0,
3878
+ status: "completed",
3879
+ }, runtime);
3880
+ return response;
3881
+ }
3882
+ catch (error) {
3883
+ const elapsedMs = Math.max(0, Date.now() - startTime);
3884
+ safeFlightComplete(corrId, {
3885
+ response: "",
3886
+ durationMs: elapsedMs,
3887
+ retryCount: 0,
3888
+ circuitBreakerState: "closed",
3889
+ optimizationApplied: false,
3890
+ exitCode: 1,
3891
+ errorMessage: error.message,
3892
+ status: "failed",
3893
+ }, runtime);
3894
+ return createErrorResponse("cursor", 1, "", corrId, error);
3895
+ }
3896
+ finally {
3897
+ runtime.performanceMetrics.recordRequest("cursor", Math.max(0, durationMs || Date.now() - startTime), wasSuccessful);
3898
+ }
3899
+ }
3900
+ export async function handleCursorRequestAsync(deps, params) {
3901
+ const runtime = resolveHandlerRuntime(deps);
3902
+ const prep = prepareCursorRequest({
3903
+ prompt: params.prompt,
3904
+ model: params.model,
3905
+ mode: params.mode,
3906
+ outputFormat: params.outputFormat,
3907
+ force: params.force,
3908
+ autoReview: params.autoReview,
3909
+ sandbox: params.sandbox,
3910
+ trust: params.trust,
3911
+ workspace: params.workspace,
3912
+ addDir: params.addDir,
3913
+ approvalStrategy: params.approvalStrategy,
3914
+ approvalPolicy: params.approvalPolicy,
3915
+ correlationId: params.correlationId,
3916
+ optimizePrompt: params.optimizePrompt,
3917
+ operation: "cursor_request_async",
3918
+ }, runtime);
3919
+ if (!("args" in prep))
3920
+ return prep;
3921
+ const { corrId, args } = prep;
3922
+ try {
3923
+ const sessionResult = resolveGrokSessionArgs({
3924
+ sessionId: params.sessionId,
3925
+ resumeLatest: params.resumeLatest,
3926
+ createNewSession: params.createNewSession,
3927
+ });
3928
+ if (sessionResult.userProvidedSession) {
3929
+ await getExistingSessionForProvider(deps.sessionManager, sessionResult.effectiveSessionId, "cursor");
3930
+ }
3931
+ args.splice(args.length - 1, 0, ...sessionResult.resumeArgs);
3932
+ let effectiveSessionId = sessionResult.effectiveSessionId;
3933
+ if (sessionResult.userProvidedSession && effectiveSessionId) {
3934
+ const existing = await deps.sessionManager.getSession(effectiveSessionId);
3935
+ if (!existing) {
3936
+ try {
3937
+ await deps.sessionManager.createSession("cursor", "Cursor Session", effectiveSessionId);
3938
+ }
3939
+ catch {
3940
+ const rechecked = await deps.sessionManager.getSession(effectiveSessionId);
3941
+ if (!rechecked)
3942
+ throw new Error(`Failed to create or find session ${effectiveSessionId}`);
3943
+ }
3944
+ }
3945
+ await deps.sessionManager.updateSessionUsage(effectiveSessionId);
3946
+ }
3947
+ else if (!params.createNewSession && !effectiveSessionId) {
3948
+ const newSession = await deps.sessionManager.createSession("cursor", "Cursor Session", `${GATEWAY_SESSION_PREFIX}${randomUUID()}`);
3949
+ effectiveSessionId = newSession.id;
3950
+ }
3951
+ let worktreeResolution = {};
3952
+ try {
3953
+ worktreeResolution = await resolveWorkspaceAndWorktreeForRequest({
3954
+ provider: "cursor",
3955
+ workspace: params.workspace,
3956
+ sessionId: effectiveSessionId,
3957
+ runtime,
3958
+ addDir: params.addDir,
3959
+ });
3960
+ }
3961
+ catch (err) {
3962
+ return createErrorResponse("cursor_request_async", 1, "", corrId, err);
3963
+ }
3964
+ if (worktreeResolution.workspace && params.workspace) {
3965
+ const workspaceFlagIndex = args.indexOf("--workspace");
3966
+ if (workspaceFlagIndex >= 0 && workspaceFlagIndex + 1 < args.length) {
3967
+ args[workspaceFlagIndex + 1] = worktreeResolution.workspace.cwd;
3968
+ }
3969
+ }
3970
+ assertUpstreamCliArgs("cursor", args);
3971
+ assertUpstreamCliEnv("cursor", undefined);
3972
+ const cursorAsyncFrHandoff = buildAsyncFlightRecorderHandoff("cursor", prep, effectiveSessionId, params.outputFormat);
3973
+ const job = deps.asyncJobManager.startJob("cursor", args, corrId, worktreeResolution.cwd, resolveIdleTimeout("cursor", params.idleTimeoutMs), params.outputFormat, params.forceRefresh, undefined, undefined, cursorAsyncFrHandoff.flightRecorderEntry, cursorAsyncFrHandoff.extractUsage, true);
3974
+ deps.logger.info(`[${corrId}] cursor_request_async started job ${job.id}`);
3975
+ return {
3976
+ content: [
3977
+ {
3978
+ type: "text",
3979
+ text: JSON.stringify({
3980
+ success: true,
3981
+ job,
3982
+ sessionId: effectiveSessionId || null,
3983
+ resumable: sessionResult.userProvidedSession,
3984
+ }, null, 2),
3985
+ },
3986
+ ],
3987
+ };
3988
+ }
3989
+ catch (error) {
3990
+ return createErrorResponse("cursor_request_async", 1, "", corrId, error);
3991
+ }
3992
+ }
3586
3993
  export async function handleMistralRequest(deps, params) {
3587
3994
  if (params.transport === "acp") {
3588
3995
  return runAcpTransport(deps, {
@@ -4162,7 +4569,8 @@ export function createGatewayServer(deps = {}) {
4162
4569
  addDir: z
4163
4570
  .array(z.string())
4164
4571
  .optional()
4165
- .describe("Claude --add-dir: additional directories the CLI is allowed to read/write beyond the process cwd. Each entry is emitted as its own --add-dir instance."),
4572
+ .describe("Claude --add-dir: additional directories the CLI is allowed to read/write beyond the process cwd. Each entry is emitted as its own --add-dir instance." +
4573
+ LOCAL_ADD_DIR_FIELD_SUFFIX),
4166
4574
  noSessionPersistence: z
4167
4575
  .boolean()
4168
4576
  .optional()
@@ -4181,7 +4589,7 @@ export function createGatewayServer(deps = {}) {
4181
4589
  .array(z.string())
4182
4590
  .optional()
4183
4591
  .describe('Claude --tools: restrict the available built-in tool set (distinct from allowedTools permission gating). Pass [""] to disable all tools.'),
4184
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4592
+ workspace: providerWorkspaceAliasSchema(),
4185
4593
  worktree: WORKTREE_SCHEMA.optional(),
4186
4594
  approvalStrategy: z
4187
4595
  .enum(["legacy", "mcp_managed"])
@@ -4328,7 +4736,11 @@ export function createGatewayServer(deps = {}) {
4328
4736
  const claudeSyncFrHandoff = buildAsyncFlightRecorderHandoff("claude", prep, effectiveSessionId, outputFormat);
4329
4737
  const result = await awaitJobOrDefer("claude", args, corrId, effectiveIdleTimeout, outputFormat, forceRefresh, runtime, undefined, undefined, claudeSyncFrHandoff.flightRecorderEntry, claudeSyncFrHandoff.extractUsage, prep.stdinPayload, worktreeResolution.cwd);
4330
4738
  if (isDeferredResponse(result)) {
4331
- return buildDeferredToolResponse(result, effectiveSessionId);
4739
+ const deferred = buildDeferredToolResponse(result, effectiveSessionId);
4740
+ if (warnings.length > 0) {
4741
+ deferred.warnings = warnings;
4742
+ }
4743
+ return deferred;
4332
4744
  }
4333
4745
  const { stdout, stderr, code } = result;
4334
4746
  durationMs = Math.max(0, Date.now() - startTime);
@@ -4525,12 +4937,14 @@ export function createGatewayServer(deps = {}) {
4525
4937
  .string()
4526
4938
  .min(1)
4527
4939
  .optional()
4528
- .describe("Codex -C/--cd <DIR>: working root for this session. Emitted on new sessions only; resume inherits the original session's cwd via CODEX_RESUME_FILTERED_FLAGS."),
4940
+ .describe("Codex -C/--cd <DIR>: working root for this session. Emitted on new sessions only; resume inherits the original session's cwd via CODEX_RESUME_FILTERED_FLAGS." +
4941
+ LOCAL_WORKING_DIR_FIELD_SUFFIX),
4529
4942
  addDir: z
4530
4943
  .array(z.string())
4531
4944
  .optional()
4532
- .describe("Codex --add-dir <DIR>: additional writable workspace directories. Emitted once per entry on new sessions only; resume inherits the original session's writable-dir policy."),
4533
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
4945
+ .describe("Codex --add-dir <DIR>: additional writable workspace directories. Emitted once per entry on new sessions only; resume inherits the original session's writable-dir policy." +
4946
+ LOCAL_ADD_DIR_FIELD_SUFFIX),
4947
+ workspace: providerWorkspaceAliasSchema(),
4534
4948
  worktree: WORKTREE_SCHEMA.optional(),
4535
4949
  }, {
4536
4950
  title: "Codex request",
@@ -4729,7 +5143,7 @@ export function createGatewayServer(deps = {}) {
4729
5143
  .max(3_600_000)
4730
5144
  .optional()
4731
5145
  .describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
4732
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
5146
+ workspace: providerWorkspaceAliasSchema(),
4733
5147
  }, {
4734
5148
  title: "Fork Codex session",
4735
5149
  readOnlyHint: false,
@@ -4853,7 +5267,7 @@ export function createGatewayServer(deps = {}) {
4853
5267
  includeDirs: z
4854
5268
  .array(z.string())
4855
5269
  .optional()
4856
- .describe("Additional workspace directories passed as --add-dir"),
5270
+ .describe("Additional workspace directories passed as --add-dir." + LOCAL_INCLUDE_DIRS_FIELD_SUFFIX),
4857
5271
  correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
4858
5272
  optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
4859
5273
  optimizeResponse: z.boolean().default(false).describe("Optimize response output"),
@@ -4893,7 +5307,7 @@ export function createGatewayServer(deps = {}) {
4893
5307
  .boolean()
4894
5308
  .optional()
4895
5309
  .describe("Antigravity 1.0.13 --new-project: create a new project for this session. Mutually exclusive with project."),
4896
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
5310
+ workspace: providerWorkspaceAliasSchema(),
4897
5311
  worktree: WORKTREE_SCHEMA.optional(),
4898
5312
  }, {
4899
5313
  title: "Gemini request",
@@ -5015,7 +5429,7 @@ export function createGatewayServer(deps = {}) {
5015
5429
  .union([z.string().min(1), z.record(z.string(), z.unknown())])
5016
5430
  .optional()
5017
5431
  .describe("Grok 0.2.73 --json-schema: constrain output to a JSON Schema (string literal or object; implies json output). Set outputFormat:json so the gateway treats the reply as structured output (skips response optimization and warning injection); the default output format is not json, so pass it explicitly."),
5018
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
5432
+ workspace: providerWorkspaceAliasSchema(),
5019
5433
  worktree: WORKTREE_SCHEMA.optional(),
5020
5434
  }, {
5021
5435
  title: "Grok request",
@@ -5148,6 +5562,108 @@ export function createGatewayServer(deps = {}) {
5148
5562
  forceRefresh,
5149
5563
  });
5150
5564
  });
5565
+ server.tool("cursor_request", "Run a Cursor Agent CLI request synchronously (auto-defers to a pollable job past the sync deadline when async jobs are enabled; otherwise runs to completion). Headless print mode (`cursor-agent --print`).", {
5566
+ prompt: z
5567
+ .string()
5568
+ .min(1, "Prompt cannot be empty")
5569
+ .max(100000, "Prompt too long (max 100k chars)")
5570
+ .describe("Prompt text for Cursor Agent CLI"),
5571
+ model: z.string().optional().describe("Model name or alias passed via --model"),
5572
+ mode: z
5573
+ .enum(["plan", "ask"])
5574
+ .optional()
5575
+ .describe("Cursor execution mode: plan (read-only planning) or ask (Q&A/read-only)"),
5576
+ outputFormat: z
5577
+ .enum(["text", "json", "stream-json"])
5578
+ .default("text")
5579
+ .describe("Cursor --output-format for --print mode"),
5580
+ transport: z
5581
+ .enum(["cli", "acp"])
5582
+ .default("cli")
5583
+ .describe("Transport selector. Default `cli` uses cursor-agent --print. `acp` uses the native cursor-agent acp transport and fails closed unless [acp] and [acp.providers.cursor].runtime_enabled are true."),
5584
+ force: z
5585
+ .boolean()
5586
+ .default(false)
5587
+ .describe("Emit --force (Cursor yolo mode; auto-allows commands unless explicitly denied)"),
5588
+ autoReview: z
5589
+ .boolean()
5590
+ .default(false)
5591
+ .describe("Emit --auto-review (Cursor Smart Auto classifier for tool calls)"),
5592
+ sandbox: z
5593
+ .enum(["enabled", "disabled"])
5594
+ .optional()
5595
+ .describe("Cursor sandbox mode override (--sandbox enabled|disabled)"),
5596
+ trust: z.boolean().default(false).describe("Trust the workspace in headless mode (--trust)"),
5597
+ workspace: z
5598
+ .string()
5599
+ .optional()
5600
+ .describe("Workspace directory or saved workspace name (--workspace). Remote HTTP/OAuth callers must pass a registered workspace alias; local stdio callers may pass local Cursor workspace paths."),
5601
+ addDir: z
5602
+ .array(z.string())
5603
+ .optional()
5604
+ .describe("Additional workspace root directories (--add-dir, repeatable)"),
5605
+ sessionId: z
5606
+ .string()
5607
+ .optional()
5608
+ .describe("Cursor chat/session ID to resume (emits --resume <id>). Note: the gw-* id minted for a brand-new gateway session is not resumable via sessionId; continue with resumeLatest:true."),
5609
+ resumeLatest: z
5610
+ .boolean()
5611
+ .default(false)
5612
+ .describe("Resume the latest Cursor chat (--continue). Note: the gw-* id minted for a brand-new gateway session is not resumable via sessionId; continue with resumeLatest:true."),
5613
+ createNewSession: z.boolean().default(false).describe("Force a new session"),
5614
+ approvalStrategy: z
5615
+ .enum(["legacy", "mcp_managed"])
5616
+ .default("legacy")
5617
+ .describe("Approval strategy: legacy (default) lets Cursor's own flags decide; mcp_managed routes high-impact Cursor controls (force, trust, sandbox disabled) through the gateway approval gate."),
5618
+ approvalPolicy: z
5619
+ .enum(["strict", "balanced", "permissive"])
5620
+ .optional()
5621
+ .describe("Approval policy when approvalStrategy is mcp_managed: strict|balanced|permissive (default balanced). Ignored under legacy strategy."),
5622
+ correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
5623
+ optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
5624
+ optimizeResponse: z.boolean().default(false).describe("Optimize response output"),
5625
+ idleTimeoutMs: z
5626
+ .number()
5627
+ .int()
5628
+ .min(30_000)
5629
+ .max(3_600_000)
5630
+ .optional()
5631
+ .describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
5632
+ forceRefresh: z
5633
+ .boolean()
5634
+ .default(false)
5635
+ .describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
5636
+ }, {
5637
+ title: "Cursor Agent CLI request",
5638
+ readOnlyHint: false,
5639
+ destructiveHint: true,
5640
+ idempotentHint: false,
5641
+ openWorldHint: true,
5642
+ }, async ({ prompt, model, mode, outputFormat, transport, force, autoReview, sandbox, trust, workspace, addDir, sessionId, resumeLatest, createNewSession, approvalStrategy, approvalPolicy, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
5643
+ return handleCursorRequest({ sessionManager, logger, runtime }, {
5644
+ prompt,
5645
+ model,
5646
+ mode,
5647
+ outputFormat,
5648
+ transport,
5649
+ force,
5650
+ autoReview,
5651
+ sandbox,
5652
+ trust,
5653
+ workspace,
5654
+ addDir,
5655
+ sessionId,
5656
+ resumeLatest,
5657
+ createNewSession,
5658
+ approvalStrategy,
5659
+ approvalPolicy,
5660
+ correlationId,
5661
+ optimizePrompt,
5662
+ optimizeResponse,
5663
+ idleTimeoutMs,
5664
+ forceRefresh,
5665
+ });
5666
+ });
5151
5667
  server.tool("mistral_request", "Run a Mistral Vibe CLI request synchronously (when async jobs are enabled, auto-defers to a pollable job past the sync deadline; otherwise runs to completion). Requires exactly one of prompt or promptParts. Defaults to --agent auto-approve (unattended tool execution); pass permissionMode plan or accept-edits for safer runs.", {
5152
5668
  prompt: z
5153
5669
  .string()
@@ -5226,12 +5742,14 @@ export function createGatewayServer(deps = {}) {
5226
5742
  .string()
5227
5743
  .min(1)
5228
5744
  .optional()
5229
- .describe("Vibe --workdir <DIR>: change to this directory before running. Single value (Vibe accepts one --workdir per invocation)."),
5745
+ .describe("Vibe --workdir <DIR>: change to this directory before running. Single value (Vibe accepts one --workdir per invocation)." +
5746
+ LOCAL_WORKING_DIR_FIELD_SUFFIX),
5230
5747
  addDir: z
5231
5748
  .array(z.string())
5232
5749
  .optional()
5233
- .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance (Vibe states this flag may be specified multiple times)."),
5234
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
5750
+ .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance (Vibe states this flag may be specified multiple times)." +
5751
+ LOCAL_ADD_DIR_FIELD_SUFFIX),
5752
+ workspace: providerWorkspaceAliasSchema(),
5235
5753
  worktree: WORKTREE_SCHEMA.optional(),
5236
5754
  }, {
5237
5755
  title: "Mistral Vibe request",
@@ -5360,7 +5878,8 @@ export function createGatewayServer(deps = {}) {
5360
5878
  addDir: z
5361
5879
  .array(z.string())
5362
5880
  .optional()
5363
- .describe("Claude --add-dir: additional directories the CLI is allowed to read/write beyond the process cwd. Each entry is emitted as its own --add-dir instance."),
5881
+ .describe("Claude --add-dir: additional directories the CLI is allowed to read/write beyond the process cwd. Each entry is emitted as its own --add-dir instance." +
5882
+ LOCAL_ADD_DIR_FIELD_SUFFIX),
5364
5883
  noSessionPersistence: z
5365
5884
  .boolean()
5366
5885
  .optional()
@@ -5379,7 +5898,7 @@ export function createGatewayServer(deps = {}) {
5379
5898
  .array(z.string())
5380
5899
  .optional()
5381
5900
  .describe('Claude --tools: restrict the available built-in tool set (distinct from allowedTools permission gating). Pass [""] to disable all tools.'),
5382
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
5901
+ workspace: providerWorkspaceAliasSchema(),
5383
5902
  worktree: WORKTREE_SCHEMA.optional(),
5384
5903
  approvalStrategy: z
5385
5904
  .enum(["legacy", "mcp_managed"])
@@ -5629,12 +6148,14 @@ export function createGatewayServer(deps = {}) {
5629
6148
  .string()
5630
6149
  .min(1)
5631
6150
  .optional()
5632
- .describe("Codex -C/--cd <DIR>: working root for this session. New sessions only; resume inherits the original session's cwd."),
6151
+ .describe("Codex -C/--cd <DIR>: working root for this session. New sessions only; resume inherits the original session's cwd." +
6152
+ LOCAL_WORKING_DIR_FIELD_SUFFIX),
5633
6153
  addDir: z
5634
6154
  .array(z.string())
5635
6155
  .optional()
5636
- .describe("Codex --add-dir <DIR>: additional writable workspace directories (repeat per entry). New sessions only."),
5637
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
6156
+ .describe("Codex --add-dir <DIR>: additional writable workspace directories (repeat per entry). New sessions only." +
6157
+ LOCAL_ADD_DIR_FIELD_SUFFIX),
6158
+ workspace: providerWorkspaceAliasSchema(),
5638
6159
  worktree: WORKTREE_SCHEMA.optional(),
5639
6160
  }, {
5640
6161
  title: "Codex request (async job)",
@@ -5721,7 +6242,8 @@ export function createGatewayServer(deps = {}) {
5721
6242
  includeDirs: z
5722
6243
  .array(z.string())
5723
6244
  .optional()
5724
- .describe("Additional workspace directories passed as --add-dir"),
6245
+ .describe("Additional workspace directories passed as --add-dir." +
6246
+ LOCAL_INCLUDE_DIRS_FIELD_SUFFIX),
5725
6247
  correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
5726
6248
  optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
5727
6249
  idleTimeoutMs: z
@@ -5760,7 +6282,7 @@ export function createGatewayServer(deps = {}) {
5760
6282
  .boolean()
5761
6283
  .optional()
5762
6284
  .describe("Antigravity 1.0.13 --new-project: create a new project for this session. Mutually exclusive with project."),
5763
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
6285
+ workspace: providerWorkspaceAliasSchema(),
5764
6286
  worktree: WORKTREE_SCHEMA.optional(),
5765
6287
  }, {
5766
6288
  title: "Gemini request (async job)",
@@ -5872,7 +6394,8 @@ export function createGatewayServer(deps = {}) {
5872
6394
  .string()
5873
6395
  .min(1)
5874
6396
  .optional()
5875
- .describe("Grok --cwd <DIR>: working directory for this invocation. Lets headless callers run Grok against a directory other than the gateway process's cwd."),
6397
+ .describe("Grok --cwd <DIR>: working directory for this invocation. Lets headless callers run Grok against a directory other than the gateway process's cwd." +
6398
+ LOCAL_WORKING_DIR_FIELD_SUFFIX),
5876
6399
  sandbox: z
5877
6400
  .string()
5878
6401
  .min(1)
@@ -5988,7 +6511,7 @@ export function createGatewayServer(deps = {}) {
5988
6511
  .union([z.string().min(1), z.record(z.string(), z.unknown())])
5989
6512
  .optional()
5990
6513
  .describe("Grok 0.2.73 --json-schema: constrain output to a JSON Schema (string literal or object; implies json output). Set outputFormat:json so the gateway treats the reply as structured output (skips response optimization and warning injection); the default output format is not json, so pass it explicitly."),
5991
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
6514
+ workspace: providerWorkspaceAliasSchema(),
5992
6515
  worktree: WORKTREE_SCHEMA.optional(),
5993
6516
  }, {
5994
6517
  title: "Grok request (async job)",
@@ -6112,6 +6635,101 @@ export function createGatewayServer(deps = {}) {
6112
6635
  forceRefresh,
6113
6636
  });
6114
6637
  });
6638
+ server.tool("cursor_request_async", "Start a Cursor Agent CLI request as a durable background job. Poll with llm_job_status, collect with llm_job_result.", {
6639
+ prompt: z
6640
+ .string()
6641
+ .min(1, "Prompt cannot be empty")
6642
+ .max(100000, "Prompt too long (max 100k chars)")
6643
+ .describe("Prompt text for Cursor Agent CLI"),
6644
+ model: z.string().optional().describe("Model name or alias passed via --model"),
6645
+ mode: z
6646
+ .enum(["plan", "ask"])
6647
+ .optional()
6648
+ .describe("Cursor execution mode: plan (read-only planning) or ask (Q&A/read-only)"),
6649
+ outputFormat: z
6650
+ .enum(["text", "json", "stream-json"])
6651
+ .default("text")
6652
+ .describe("Cursor --output-format for --print mode"),
6653
+ force: z
6654
+ .boolean()
6655
+ .default(false)
6656
+ .describe("Emit --force (Cursor yolo mode; auto-allows commands unless explicitly denied)"),
6657
+ autoReview: z
6658
+ .boolean()
6659
+ .default(false)
6660
+ .describe("Emit --auto-review (Cursor Smart Auto classifier for tool calls)"),
6661
+ sandbox: z
6662
+ .enum(["enabled", "disabled"])
6663
+ .optional()
6664
+ .describe("Cursor sandbox mode override (--sandbox enabled|disabled)"),
6665
+ trust: z.boolean().default(false).describe("Trust the workspace in headless mode"),
6666
+ workspace: z
6667
+ .string()
6668
+ .optional()
6669
+ .describe("Workspace directory or saved workspace name (--workspace). Remote HTTP/OAuth callers must pass a registered workspace alias; local stdio callers may pass local Cursor workspace paths."),
6670
+ addDir: z
6671
+ .array(z.string())
6672
+ .optional()
6673
+ .describe("Additional workspace root directories (--add-dir, repeatable)"),
6674
+ sessionId: z
6675
+ .string()
6676
+ .optional()
6677
+ .describe("Cursor chat/session ID to resume (emits --resume <id>). Note: the gw-* id minted for a brand-new gateway session is not resumable via sessionId; continue with resumeLatest:true."),
6678
+ resumeLatest: z
6679
+ .boolean()
6680
+ .default(false)
6681
+ .describe("Resume the latest Cursor chat (--continue). Note: the gw-* id minted for a brand-new gateway session is not resumable via sessionId; continue with resumeLatest:true."),
6682
+ createNewSession: z.boolean().default(false).describe("Force a new session"),
6683
+ approvalStrategy: z
6684
+ .enum(["legacy", "mcp_managed"])
6685
+ .default("legacy")
6686
+ .describe("Approval strategy: legacy (default) lets Cursor's own flags decide; mcp_managed routes high-impact Cursor controls (force, trust, sandbox disabled) through the gateway approval gate."),
6687
+ approvalPolicy: z
6688
+ .enum(["strict", "balanced", "permissive"])
6689
+ .optional()
6690
+ .describe("Approval policy when approvalStrategy is mcp_managed: strict|balanced|permissive (default balanced). Ignored under legacy strategy."),
6691
+ correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
6692
+ optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
6693
+ idleTimeoutMs: z
6694
+ .number()
6695
+ .int()
6696
+ .min(30_000)
6697
+ .max(3_600_000)
6698
+ .optional()
6699
+ .describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
6700
+ forceRefresh: z
6701
+ .boolean()
6702
+ .default(false)
6703
+ .describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
6704
+ }, {
6705
+ title: "Cursor Agent CLI request (async)",
6706
+ readOnlyHint: false,
6707
+ destructiveHint: true,
6708
+ idempotentHint: false,
6709
+ openWorldHint: true,
6710
+ }, async ({ prompt, model, mode, outputFormat, force, autoReview, sandbox, trust, workspace, addDir, sessionId, resumeLatest, createNewSession, approvalStrategy, approvalPolicy, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
6711
+ return handleCursorRequestAsync({ sessionManager, asyncJobManager, logger, runtime }, {
6712
+ prompt,
6713
+ model,
6714
+ mode,
6715
+ outputFormat,
6716
+ force,
6717
+ autoReview,
6718
+ sandbox,
6719
+ trust,
6720
+ workspace,
6721
+ addDir,
6722
+ sessionId,
6723
+ resumeLatest,
6724
+ createNewSession,
6725
+ approvalStrategy,
6726
+ approvalPolicy,
6727
+ correlationId,
6728
+ optimizePrompt,
6729
+ idleTimeoutMs,
6730
+ forceRefresh,
6731
+ });
6732
+ });
6115
6733
  server.tool("mistral_request_async", "Start a Mistral Vibe CLI request as a durable background job. Poll with llm_job_status, collect with llm_job_result.", {
6116
6734
  prompt: z
6117
6735
  .string()
@@ -6185,12 +6803,14 @@ export function createGatewayServer(deps = {}) {
6185
6803
  .string()
6186
6804
  .min(1)
6187
6805
  .optional()
6188
- .describe("Vibe --workdir <DIR>: change to this directory before running. Single value per invocation."),
6806
+ .describe("Vibe --workdir <DIR>: change to this directory before running. Single value per invocation." +
6807
+ LOCAL_WORKING_DIR_FIELD_SUFFIX),
6189
6808
  addDir: z
6190
6809
  .array(z.string())
6191
6810
  .optional()
6192
- .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance."),
6193
- workspace: WORKSPACE_ALIAS_SCHEMA.optional(),
6811
+ .describe("Vibe --add-dir <DIR>: additional writable workspace directories. Each entry is emitted as its own --add-dir instance." +
6812
+ LOCAL_ADD_DIR_FIELD_SUFFIX),
6813
+ workspace: providerWorkspaceAliasSchema(),
6194
6814
  worktree: WORKTREE_SCHEMA.optional(),
6195
6815
  }, {
6196
6816
  title: "Mistral Vibe request (async job)",
@@ -6227,7 +6847,7 @@ export function createGatewayServer(deps = {}) {
6227
6847
  worktree,
6228
6848
  });
6229
6849
  });
6230
- server.tool("llm_job_status", "Check lifecycle status (running|completed|failed|canceled|orphaned) of a gateway async or deferred-sync job by jobId.", {
6850
+ server.tool("llm_job_status", "Check lifecycle status (queued|running|completed|failed|canceled|orphaned) of a gateway async or deferred-sync job by jobId.", {
6231
6851
  jobId: z.string().describe("Async job ID from *_request_async"),
6232
6852
  }, {
6233
6853
  title: "Async job status",
@@ -6447,18 +7067,22 @@ export function createGatewayServer(deps = {}) {
6447
7067
  openWorldHint: false,
6448
7068
  }, async () => {
6449
7069
  const health = asyncJobManager.getJobHealth();
7070
+ const storeAttached = asyncJobManager.hasStore();
7071
+ const asyncJobsEffective = persistence.backend !== "none" && persistence.asyncJobsEnabled && storeAttached;
6450
7072
  const persistenceBlock = {
6451
7073
  backend: persistence.backend,
6452
7074
  dbPath: persistence.path,
6453
7075
  dsn: persistence.dsn ? "[redacted]" : null,
6454
7076
  retentionDays: persistence.retentionDays,
6455
7077
  dedupWindowMs: persistence.dedupWindowMs,
6456
- asyncJobsEnabled: persistence.asyncJobsEnabled,
7078
+ asyncJobsEnabled: asyncJobsEffective,
6457
7079
  acknowledgeEphemeral: persistence.acknowledgeEphemeral,
6458
7080
  sources: persistence.sources,
6459
- warning: persistence.asyncJobsEnabled
7081
+ warning: asyncJobsEffective
6460
7082
  ? null
6461
- : "Async job persistence is disabled (backend = 'none'). *_request_async tools are NOT registered on this gateway. Set [persistence].backend = 'sqlite' (or 'memory' + acknowledgeEphemeral = true) to enable them.",
7083
+ : persistence.backend === "none"
7084
+ ? "Async job persistence is disabled (backend = 'none'). *_request_async tools are NOT registered on this gateway. Set [persistence].backend = 'sqlite' (or 'memory' + acknowledgeEphemeral = true) to enable them."
7085
+ : `Async job persistence is configured (backend = '${persistence.backend}') but the durable job store failed to open, so *_request_async tools are NOT registered on this gateway. Check gateway startup logs for the store-open error.`,
6462
7086
  };
6463
7087
  const outboundProviders = {
6464
7088
  xai: providers.xai
@@ -6467,7 +7091,7 @@ export function createGatewayServer(deps = {}) {
6467
7091
  enabled: isXaiProviderEnabled(providers),
6468
7092
  apiKeyEnv: providers.xai.apiKeyEnv,
6469
7093
  apiKeyPresent: isXaiProviderEnabled(providers),
6470
- baseUrl: providers.xai.baseUrl,
7094
+ baseUrl: redactDiagnosticUrl(providers.xai.baseUrl) ?? providers.xai.baseUrl,
6471
7095
  defaultModel: providers.xai.defaultModel,
6472
7096
  mode: isXaiProviderEnabled(providers) ? "sync" : "configured-missing-key",
6473
7097
  }
@@ -6482,16 +7106,59 @@ export function createGatewayServer(deps = {}) {
6482
7106
  },
6483
7107
  apiProviders: enabledApiProviders(providers).map(p => ({
6484
7108
  ...apiProviderCatalogEntry(p),
6485
- baseUrl: p.baseUrl,
7109
+ baseUrl: redactDiagnosticUrl(p.baseUrl) ?? p.baseUrl,
6486
7110
  breakerState: apiProviderBreakerState(p.name),
6487
7111
  })),
6488
7112
  sources: providers.sources,
6489
7113
  };
7114
+ const limiter = asyncJobManager.getLimiterSnapshot();
7115
+ const jobLimits = asyncJobManager.getConfiguredLimits();
7116
+ const httpLimits = getLimitsConfig().http;
7117
+ const mem = process.memoryUsage();
7118
+ const backpressure = {
7119
+ jobs: {
7120
+ running: limiter.running,
7121
+ queued: limiter.queued,
7122
+ runningByProvider: limiter.runningByProvider,
7123
+ queuedByProvider: limiter.queuedByProvider,
7124
+ maxRunning: limiter.maxRunning,
7125
+ maxRunningPerProvider: limiter.maxRunningPerProvider,
7126
+ maxQueued: limiter.maxQueued,
7127
+ rejected: limiter.rejected,
7128
+ timedOut: limiter.timedOut,
7129
+ saturated: limiter.saturated,
7130
+ completedJobMemoryTtlMs: jobLimits.completedJobMemoryTtlMs,
7131
+ maxJobOutputBytes: jobLimits.maxJobOutputBytes,
7132
+ },
7133
+ httpSessions: activeHttpGateway
7134
+ ? { active: true, ...activeHttpGateway.sessionHealth() }
7135
+ : {
7136
+ active: false,
7137
+ current: 0,
7138
+ max: httpLimits.maxSessions,
7139
+ oldestAgeMs: 0,
7140
+ idleTtlMs: httpLimits.sessionIdleTtlMs,
7141
+ reaperIntervalMs: httpLimits.sessionReaperIntervalMs,
7142
+ saturated: false,
7143
+ },
7144
+ memory: {
7145
+ rss: mem.rss,
7146
+ heapUsed: mem.heapUsed,
7147
+ heapTotal: mem.heapTotal,
7148
+ external: mem.external,
7149
+ },
7150
+ };
6490
7151
  return {
6491
7152
  content: [
6492
7153
  {
6493
7154
  type: "text",
6494
- text: JSON.stringify({ success: true, ...health, persistence: persistenceBlock, outboundProviders }, null, 2),
7155
+ text: JSON.stringify({
7156
+ success: true,
7157
+ ...health,
7158
+ backpressure,
7159
+ persistence: persistenceBlock,
7160
+ outboundProviders,
7161
+ }, null, 2),
6495
7162
  },
6496
7163
  ],
6497
7164
  };
@@ -6529,10 +7196,13 @@ export function createGatewayServer(deps = {}) {
6529
7196
  ],
6530
7197
  };
6531
7198
  });
6532
- server.tool("list_models", "List models, aliases, and defaults for one provider CLI (claude|codex|gemini|grok|mistral|devin), or omit cli to list all providers.", {
7199
+ const listModelsFilterValues = [
7200
+ ...new Set([...CLI_TYPES, ...enabledApiProviders(providers).map(p => p.name)]),
7201
+ ];
7202
+ server.tool("list_models", "List models, aliases, and defaults for one provider (claude|codex|gemini|grok|mistral|devin|cursor, or an enabled API provider name), or omit cli to list all providers. API providers are returned under an `apiProviders` array.", {
6533
7203
  cli: z
6534
- .preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
6535
- .describe("CLI filter (claude|codex|gemini|grok|mistral|devin)"),
7204
+ .preprocess(value => (value === "" || value === null ? undefined : value), z.enum(listModelsFilterValues).optional())
7205
+ .describe("Provider filter (claude|codex|gemini|grok|mistral|devin|cursor, or an enabled API provider name)"),
6536
7206
  }, {
6537
7207
  title: "Provider models",
6538
7208
  readOnlyHint: true,
@@ -6541,13 +7211,32 @@ export function createGatewayServer(deps = {}) {
6541
7211
  openWorldHint: false,
6542
7212
  }, async ({ cli }) => {
6543
7213
  const cliInfo = getAvailableCliInfo();
6544
- const result = cli ? { [cli]: cliInfo[cli] } : cliInfo;
7214
+ const apiRuntimes = enabledApiProviders(providers);
7215
+ let result;
7216
+ if (cli && CLI_TYPES.includes(cli)) {
7217
+ result = { [cli]: cliInfo[cli] };
7218
+ }
7219
+ else if (cli) {
7220
+ const runtime = apiRuntimes.find(p => p.name === cli);
7221
+ result = { apiProviders: runtime ? [apiProviderCatalogEntry(runtime)] : [] };
7222
+ }
7223
+ else {
7224
+ const apiProviders = apiRuntimes.map(apiProviderCatalogEntry);
7225
+ result = { ...cliInfo, ...(apiProviders.length > 0 ? { apiProviders } : {}) };
7226
+ }
6545
7227
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
6546
7228
  });
6547
- server.tool("provider_tool_capabilities", "Report provider tool/feature capabilities and discovered local skill/tool integrations for claude|codex|gemini|grok|mistral|devin|grok_api.", {
7229
+ const providerToolCapabilitiesFilterValues = [
7230
+ ...new Set([
7231
+ ...CLI_TYPES,
7232
+ "grok_api",
7233
+ ...enabledApiProviders(providers).map(p => p.name),
7234
+ ]),
7235
+ ];
7236
+ server.tool("provider_tool_capabilities", "Report provider tool/feature capabilities and discovered local skill/tool integrations for claude|codex|gemini|grok|mistral|devin|cursor|grok_api, or an enabled API provider name.", {
6548
7237
  cli: z
6549
- .preprocess(value => (value === "" || value === null ? undefined : value), z.enum(["claude", "codex", "gemini", "grok", "mistral", "devin", "grok_api"]).optional())
6550
- .describe("Provider filter (claude|codex|gemini|grok|mistral|devin|grok_api)"),
7238
+ .preprocess(value => (value === "" || value === null ? undefined : value), z.enum(providerToolCapabilitiesFilterValues).optional())
7239
+ .describe("Provider filter (claude|codex|gemini|grok|mistral|devin|cursor|grok_api, or an enabled API provider name)"),
6551
7240
  includeSkills: z
6552
7241
  .boolean()
6553
7242
  .default(true)
@@ -6579,13 +7268,14 @@ export function createGatewayServer(deps = {}) {
6579
7268
  includeUnsupported,
6580
7269
  includePaths,
6581
7270
  refresh,
7271
+ providersConfig: providers,
6582
7272
  });
6583
7273
  return { content: [{ type: "text", text: JSON.stringify(capabilities, null, 2) }] };
6584
7274
  });
6585
- server.tool("cli_versions", "Report installed provider CLI versions, availability, and login status for all six providers (claude|codex|gemini|grok|mistral|devin) or one.", {
7275
+ server.tool("cli_versions", "Report installed provider CLI versions, availability, and login status for all registered CLI providers (claude|codex|gemini|grok|mistral|devin|cursor) or one.", {
6586
7276
  cli: z
6587
7277
  .preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
6588
- .describe("CLI filter (claude|codex|gemini|grok|mistral|devin)"),
7278
+ .describe("CLI filter (claude|codex|gemini|grok|mistral|devin|cursor)"),
6589
7279
  }, {
6590
7280
  title: "Provider CLI versions",
6591
7281
  readOnlyHint: true,
@@ -6599,7 +7289,7 @@ export function createGatewayServer(deps = {}) {
6599
7289
  server.tool("upstream_contracts", "Return the gateway's declared provider CLI contracts; with probeInstalled true, diff against installed --help surfaces to detect flag drift.", {
6600
7290
  cli: z
6601
7291
  .preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
6602
- .describe("CLI filter (claude|codex|gemini|grok|mistral|devin)"),
7292
+ .describe("CLI filter (claude|codex|gemini|grok|mistral|devin|cursor)"),
6603
7293
  probeInstalled: z
6604
7294
  .boolean()
6605
7295
  .default(false)
@@ -6617,7 +7307,7 @@ export function createGatewayServer(deps = {}) {
6617
7307
  server.tool("provider_subcommands_list", "Return a compact, filterable read-only catalog of declared provider CLI subcommands without flags or raw help.", {
6618
7308
  provider: z
6619
7309
  .preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
6620
- .describe("Optional provider filter (claude|codex|gemini|grok|mistral|devin)"),
7310
+ .describe("Optional provider filter (claude|codex|gemini|grok|mistral|devin|cursor)"),
6621
7311
  tier: z
6622
7312
  .enum(["catalog", "inspect", "execute_candidate", "diagnostic"])
6623
7313
  .optional()
@@ -6667,7 +7357,7 @@ export function createGatewayServer(deps = {}) {
6667
7357
  };
6668
7358
  });
6669
7359
  server.tool("provider_subcommand_contract", "Return the detailed read-only contract for exactly one declared provider CLI subcommand.", {
6670
- provider: CLI_TYPE_ENUM.describe("Provider (claude|codex|gemini|grok|mistral|devin)"),
7360
+ provider: CLI_TYPE_ENUM.describe("Provider (claude|codex|gemini|grok|mistral|devin|cursor)"),
6671
7361
  commandPath: z.array(z.string().min(1)).min(1).describe("Command path segments"),
6672
7362
  }, {
6673
7363
  title: "Provider subcommand contract",
@@ -6691,7 +7381,7 @@ export function createGatewayServer(deps = {}) {
6691
7381
  server.tool("provider_subcommand_drift", "Probe declared provider subcommand --help surfaces and return compact drift rows without raw help output.", {
6692
7382
  provider: z
6693
7383
  .preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
6694
- .describe("Optional provider filter (claude|codex|gemini|grok|mistral|devin)"),
7384
+ .describe("Optional provider filter (claude|codex|gemini|grok|mistral|devin|cursor)"),
6695
7385
  includeClean: z
6696
7386
  .boolean()
6697
7387
  .default(false)
@@ -6742,7 +7432,7 @@ export function createGatewayServer(deps = {}) {
6742
7432
  };
6743
7433
  });
6744
7434
  server.tool("cli_upgrade", "Plan (dryRun, default true) or execute an upgrade for one provider CLI using its native update mechanism.", {
6745
- cli: CLI_TYPE_ENUM.describe("CLI to upgrade (claude|codex|gemini|grok|mistral|devin)"),
7435
+ cli: CLI_TYPE_ENUM.describe("CLI to upgrade (claude|codex|gemini|grok|mistral|devin|cursor)"),
6746
7436
  target: z
6747
7437
  .string()
6748
7438
  .min(1)
@@ -7124,7 +7814,7 @@ async function initializeSessionManager() {
7124
7814
  });
7125
7815
  logger.info("File-based session manager initialized");
7126
7816
  }
7127
- resourceProvider = new ResourceProvider(sessionManager, performanceMetrics, getFlightRecorder(logger), getCacheAwarenessConfig(logger));
7817
+ resourceProvider = new ResourceProvider(sessionManager, performanceMetrics, getFlightRecorder(logger), getCacheAwarenessConfig(logger), getProvidersConfig(logger));
7128
7818
  }
7129
7819
  function registerHealthResource(server) {
7130
7820
  if (db) {
@@ -7430,7 +8120,8 @@ async function main() {
7430
8120
  if (args[0] === "doctor") {
7431
8121
  if (args.includes("--json")) {
7432
8122
  const probeUpstream = args.includes("--probe-upstream") || args.includes("--probe-installed");
7433
- printDoctorJson({ probeUpstream });
8123
+ const probeApiProviders = args.includes("--probe-api-providers");
8124
+ await printDoctorJson({ probeUpstream, probeApiProviders });
7434
8125
  return;
7435
8126
  }
7436
8127
  process.stderr.write("Only doctor --json is supported in this layer.\n");