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/CHANGELOG.md +24 -1
- package/README.md +48 -17
- package/dist/acp/provider-registry.js +13 -0
- package/dist/approval-manager.d.ts +1 -1
- package/dist/async-job-manager.d.ts +1 -1
- package/dist/cli-updater.d.ts +1 -1
- package/dist/cli-updater.js +17 -9
- package/dist/config.js +2 -8
- package/dist/doctor.d.ts +2 -2
- package/dist/doctor.js +2 -7
- package/dist/executor.js +2 -0
- package/dist/index.d.ts +47 -5
- package/dist/index.js +557 -13
- package/dist/model-registry.d.ts +1 -1
- package/dist/model-registry.js +12 -16
- package/dist/provider-login-guidance.js +18 -0
- package/dist/provider-status.d.ts +1 -1
- package/dist/provider-status.js +8 -13
- package/dist/provider-tool-capabilities.js +85 -0
- package/dist/provider-types.d.ts +4 -0
- package/dist/provider-types.js +10 -0
- package/dist/session-manager.d.ts +3 -5
- package/dist/session-manager.js +4 -2
- package/dist/upstream-contracts.js +169 -0
- package/dist/validation-orchestrator.js +4 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/setup/status.schema.json +3 -3
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
|
-
|
|
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
|
|
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)
|