llm-cli-gateway 2.12.2 → 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.
package/dist/index.js CHANGED
@@ -162,7 +162,7 @@ export function buildServerInstructions(asyncJobsEnabled, grokApiToolsEnabled =
162
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).';
163
163
  return `llm-cli-gateway: Multi-LLM orchestration via MCP.
164
164
 
165
- 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)
166
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)
167
167
  ${jobsLine}Sessions: session_create, session_list, session_set_active, session_get, session_delete, session_clear_all
168
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)
@@ -345,6 +345,7 @@ const CLI_IDLE_TIMEOUTS = {
345
345
  gemini: 600_000,
346
346
  grok: 600_000,
347
347
  mistral: 600_000,
348
+ cursor: 600_000,
348
349
  };
349
350
  function resolveIdleTimeout(cli, override) {
350
351
  if (override !== undefined)
@@ -884,6 +885,7 @@ export function createErrorResponse(cli, code, stderr, correlationId, error, api
884
885
  grok: "Run `grok login` (or re-authenticate) and retry.",
885
886
  mistral: "Run `vibe --setup` (or re-authenticate) and retry.",
886
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.",
887
889
  };
888
890
  if (error) {
889
891
  errorMessage += `:\n${error.message}`;
@@ -3647,6 +3649,347 @@ export async function handleDevinRequestAsync(deps, params) {
3647
3649
  return createErrorResponse("devin_request_async", 1, "", corrId, error);
3648
3650
  }
3649
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
+ }
3650
3993
  export async function handleMistralRequest(deps, params) {
3651
3994
  if (params.transport === "acp") {
3652
3995
  return runAcpTransport(deps, {
@@ -4393,7 +4736,11 @@ export function createGatewayServer(deps = {}) {
4393
4736
  const claudeSyncFrHandoff = buildAsyncFlightRecorderHandoff("claude", prep, effectiveSessionId, outputFormat);
4394
4737
  const result = await awaitJobOrDefer("claude", args, corrId, effectiveIdleTimeout, outputFormat, forceRefresh, runtime, undefined, undefined, claudeSyncFrHandoff.flightRecorderEntry, claudeSyncFrHandoff.extractUsage, prep.stdinPayload, worktreeResolution.cwd);
4395
4738
  if (isDeferredResponse(result)) {
4396
- return buildDeferredToolResponse(result, effectiveSessionId);
4739
+ const deferred = buildDeferredToolResponse(result, effectiveSessionId);
4740
+ if (warnings.length > 0) {
4741
+ deferred.warnings = warnings;
4742
+ }
4743
+ return deferred;
4397
4744
  }
4398
4745
  const { stdout, stderr, code } = result;
4399
4746
  durationMs = Math.max(0, Date.now() - startTime);
@@ -5215,6 +5562,108 @@ export function createGatewayServer(deps = {}) {
5215
5562
  forceRefresh,
5216
5563
  });
5217
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
+ });
5218
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.", {
5219
5668
  prompt: z
5220
5669
  .string()
@@ -6186,6 +6635,101 @@ export function createGatewayServer(deps = {}) {
6186
6635
  forceRefresh,
6187
6636
  });
6188
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
+ });
6189
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.", {
6190
6734
  prompt: z
6191
6735
  .string()
@@ -6655,10 +7199,10 @@ export function createGatewayServer(deps = {}) {
6655
7199
  const listModelsFilterValues = [
6656
7200
  ...new Set([...CLI_TYPES, ...enabledApiProviders(providers).map(p => p.name)]),
6657
7201
  ];
6658
- server.tool("list_models", "List models, aliases, and defaults for one provider (claude|codex|gemini|grok|mistral|devin, or an enabled API provider name), or omit cli to list all providers. API providers are returned under an `apiProviders` array.", {
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.", {
6659
7203
  cli: z
6660
7204
  .preprocess(value => (value === "" || value === null ? undefined : value), z.enum(listModelsFilterValues).optional())
6661
- .describe("Provider filter (claude|codex|gemini|grok|mistral|devin, or an enabled API provider name)"),
7205
+ .describe("Provider filter (claude|codex|gemini|grok|mistral|devin|cursor, or an enabled API provider name)"),
6662
7206
  }, {
6663
7207
  title: "Provider models",
6664
7208
  readOnlyHint: true,
@@ -6689,10 +7233,10 @@ export function createGatewayServer(deps = {}) {
6689
7233
  ...enabledApiProviders(providers).map(p => p.name),
6690
7234
  ]),
6691
7235
  ];
6692
- 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, or an enabled API provider name.", {
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.", {
6693
7237
  cli: z
6694
7238
  .preprocess(value => (value === "" || value === null ? undefined : value), z.enum(providerToolCapabilitiesFilterValues).optional())
6695
- .describe("Provider filter (claude|codex|gemini|grok|mistral|devin|grok_api, or an enabled API provider name)"),
7239
+ .describe("Provider filter (claude|codex|gemini|grok|mistral|devin|cursor|grok_api, or an enabled API provider name)"),
6696
7240
  includeSkills: z
6697
7241
  .boolean()
6698
7242
  .default(true)
@@ -6728,10 +7272,10 @@ export function createGatewayServer(deps = {}) {
6728
7272
  });
6729
7273
  return { content: [{ type: "text", text: JSON.stringify(capabilities, null, 2) }] };
6730
7274
  });
6731
- 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.", {
6732
7276
  cli: z
6733
7277
  .preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
6734
- .describe("CLI filter (claude|codex|gemini|grok|mistral|devin)"),
7278
+ .describe("CLI filter (claude|codex|gemini|grok|mistral|devin|cursor)"),
6735
7279
  }, {
6736
7280
  title: "Provider CLI versions",
6737
7281
  readOnlyHint: true,
@@ -6745,7 +7289,7 @@ export function createGatewayServer(deps = {}) {
6745
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.", {
6746
7290
  cli: z
6747
7291
  .preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
6748
- .describe("CLI filter (claude|codex|gemini|grok|mistral|devin)"),
7292
+ .describe("CLI filter (claude|codex|gemini|grok|mistral|devin|cursor)"),
6749
7293
  probeInstalled: z
6750
7294
  .boolean()
6751
7295
  .default(false)
@@ -6763,7 +7307,7 @@ export function createGatewayServer(deps = {}) {
6763
7307
  server.tool("provider_subcommands_list", "Return a compact, filterable read-only catalog of declared provider CLI subcommands without flags or raw help.", {
6764
7308
  provider: z
6765
7309
  .preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
6766
- .describe("Optional provider filter (claude|codex|gemini|grok|mistral|devin)"),
7310
+ .describe("Optional provider filter (claude|codex|gemini|grok|mistral|devin|cursor)"),
6767
7311
  tier: z
6768
7312
  .enum(["catalog", "inspect", "execute_candidate", "diagnostic"])
6769
7313
  .optional()
@@ -6813,7 +7357,7 @@ export function createGatewayServer(deps = {}) {
6813
7357
  };
6814
7358
  });
6815
7359
  server.tool("provider_subcommand_contract", "Return the detailed read-only contract for exactly one declared provider CLI subcommand.", {
6816
- 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)"),
6817
7361
  commandPath: z.array(z.string().min(1)).min(1).describe("Command path segments"),
6818
7362
  }, {
6819
7363
  title: "Provider subcommand contract",
@@ -6837,7 +7381,7 @@ export function createGatewayServer(deps = {}) {
6837
7381
  server.tool("provider_subcommand_drift", "Probe declared provider subcommand --help surfaces and return compact drift rows without raw help output.", {
6838
7382
  provider: z
6839
7383
  .preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
6840
- .describe("Optional provider filter (claude|codex|gemini|grok|mistral|devin)"),
7384
+ .describe("Optional provider filter (claude|codex|gemini|grok|mistral|devin|cursor)"),
6841
7385
  includeClean: z
6842
7386
  .boolean()
6843
7387
  .default(false)
@@ -6888,7 +7432,7 @@ export function createGatewayServer(deps = {}) {
6888
7432
  };
6889
7433
  });
6890
7434
  server.tool("cli_upgrade", "Plan (dryRun, default true) or execute an upgrade for one provider CLI using its native update mechanism.", {
6891
- 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)"),
6892
7436
  target: z
6893
7437
  .string()
6894
7438
  .min(1)