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.
- package/CHANGELOG.md +46 -0
- package/README.md +177 -19
- package/dist/acp/provider-registry.js +13 -0
- package/dist/api-provider.d.ts +1 -0
- package/dist/api-provider.js +13 -0
- package/dist/approval-manager.d.ts +1 -1
- package/dist/async-job-manager.d.ts +35 -3
- package/dist/async-job-manager.js +344 -46
- package/dist/cli-updater.d.ts +1 -1
- package/dist/cli-updater.js +17 -9
- package/dist/config.d.ts +34 -0
- package/dist/config.js +93 -8
- package/dist/doctor.d.ts +32 -4
- package/dist/doctor.js +80 -13
- package/dist/endpoint-exposure.js +15 -3
- package/dist/executor.js +2 -0
- package/dist/http-transport.d.ts +3 -0
- package/dist/http-transport.js +138 -8
- package/dist/index.d.ts +62 -5
- package/dist/index.js +774 -83
- package/dist/model-registry.d.ts +1 -1
- package/dist/model-registry.js +12 -16
- package/dist/provider-login-guidance.d.ts +12 -0
- package/dist/provider-login-guidance.js +46 -0
- package/dist/provider-status.d.ts +13 -1
- package/dist/provider-status.js +22 -13
- package/dist/provider-tool-capabilities.d.ts +4 -1
- package/dist/provider-tool-capabilities.js +310 -11
- package/dist/provider-types.d.ts +4 -0
- package/dist/provider-types.js +10 -0
- package/dist/resources.d.ts +6 -2
- package/dist/resources.js +72 -8
- 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-normalizer.js +5 -4
- package/dist/validation-orchestrator.js +4 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- 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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
7078
|
+
asyncJobsEnabled: asyncJobsEffective,
|
|
6457
7079
|
acknowledgeEphemeral: persistence.acknowledgeEphemeral,
|
|
6458
7080
|
sources: persistence.sources,
|
|
6459
|
-
warning:
|
|
7081
|
+
warning: asyncJobsEffective
|
|
6460
7082
|
? null
|
|
6461
|
-
:
|
|
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({
|
|
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
|
-
|
|
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),
|
|
6535
|
-
.describe("
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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");
|