llm-cli-gateway 1.1.0 → 1.4.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 +21 -0
- package/README.md +122 -8
- package/dist/approval-manager.d.ts +1 -1
- package/dist/async-job-manager.d.ts +53 -4
- package/dist/async-job-manager.js +237 -17
- package/dist/cli-updater.d.ts +38 -0
- package/dist/cli-updater.js +145 -0
- package/dist/flight-recorder.d.ts +1 -1
- package/dist/index.d.ts +27 -0
- package/dist/index.js +651 -26
- package/dist/job-store.d.ts +84 -0
- package/dist/job-store.js +251 -0
- package/dist/model-registry.d.ts +14 -0
- package/dist/model-registry.js +444 -134
- package/dist/request-helpers.d.ts +41 -0
- package/dist/request-helpers.js +40 -0
- package/dist/resources.js +44 -0
- package/dist/session-manager-pg.js +1 -0
- package/dist/session-manager.d.ts +1 -1
- package/dist/session-manager.js +2 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -16,11 +16,13 @@ import { loadConfig } from "./config.js";
|
|
|
16
16
|
import { checkHealth } from "./health.js";
|
|
17
17
|
import { getCliInfo, resolveModelAlias } from "./model-registry.js";
|
|
18
18
|
import { AsyncJobManager } from "./async-job-manager.js";
|
|
19
|
+
import { JobStore, resolveJobStoreDbPath } from "./job-store.js";
|
|
19
20
|
import { ApprovalManager } from "./approval-manager.js";
|
|
20
21
|
import { checkReviewIntegrity } from "./review-integrity.js";
|
|
21
22
|
import { buildClaudeMcpConfig, CLAUDE_MCP_SERVER_NAMES, } from "./claude-mcp-config.js";
|
|
22
|
-
import { resolveSessionResumeArgs, sanitizeCliArgValues, GATEWAY_SESSION_PREFIX, } from "./request-helpers.js";
|
|
23
|
+
import { resolveSessionResumeArgs, resolveGrokSessionArgs, resolveCodexSessionArgs, sanitizeCliArgValues, GATEWAY_SESSION_PREFIX, } from "./request-helpers.js";
|
|
23
24
|
import { createFlightRecorder } from "./flight-recorder.js";
|
|
25
|
+
import { getCliVersions, runCliUpgrade } from "./cli-updater.js";
|
|
24
26
|
// Simple logger that writes to stderr (stdout is used for MCP protocol)
|
|
25
27
|
const logger = {
|
|
26
28
|
info: (message, ...args) => {
|
|
@@ -88,14 +90,14 @@ const loadedSkills = loadSkills();
|
|
|
88
90
|
// system prompt at connection time. Covers key patterns + pointers to L2 resources.
|
|
89
91
|
const SERVER_INSTRUCTIONS = `llm-cli-gateway: Multi-LLM orchestration via MCP.
|
|
90
92
|
|
|
91
|
-
Tools: claude_request, codex_request, gemini_request (sync) | *_request_async (async)
|
|
93
|
+
Tools: claude_request, codex_request, gemini_request, grok_request (sync) | *_request_async (async)
|
|
92
94
|
Jobs: llm_job_status, llm_job_result, llm_job_cancel
|
|
93
95
|
Sessions: session_create, session_list, session_set_active, session_get, session_delete, session_clear_all
|
|
94
|
-
Other: list_models, approval_list, llm_process_health
|
|
96
|
+
Other: list_models, cli_versions, cli_upgrade, approval_list, llm_process_health
|
|
95
97
|
|
|
96
98
|
Key behaviors:
|
|
97
99
|
- Sync auto-defers at ${SYNC_DEADLINE_MS}ms. Poll deferred jobs via llm_job_status/llm_job_result.
|
|
98
|
-
- Sessions: Claude --continue, Gemini --resume (real CLI continuity). Codex
|
|
100
|
+
- Sessions: Claude --continue, Gemini --resume, Grok --resume/--continue, Codex \`exec resume <ID>\` / \`exec resume --last\` (all real CLI continuity). For Codex, sessionId must be a real Codex UUID (from ~/.codex/sessions/); gateway-generated gw-* IDs are rejected.
|
|
99
101
|
- Approval gates: opt-in via approvalStrategy:"mcp_managed".
|
|
100
102
|
- Idle timeout kills stuck processes (default 10min, configurable via idleTimeoutMs).
|
|
101
103
|
|
|
@@ -108,9 +110,26 @@ let db = null;
|
|
|
108
110
|
const performanceMetrics = new PerformanceMetrics();
|
|
109
111
|
let resourceProvider;
|
|
110
112
|
const flightRecorder = createFlightRecorder(logger);
|
|
113
|
+
// Durable job store: persists every async job to ~/.llm-cli-gateway/logs.db so callers
|
|
114
|
+
// can collect results across long polling gaps and gateway restarts, and so repeated
|
|
115
|
+
// identical requests dedup onto the running/completed job instead of starting over.
|
|
116
|
+
const jobStore = (() => {
|
|
117
|
+
const dbPath = resolveJobStoreDbPath();
|
|
118
|
+
if (!dbPath) {
|
|
119
|
+
logger.info("Durable job store disabled (LLM_GATEWAY_LOGS_DB=none)");
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
return new JobStore(dbPath, logger);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
logger.error("Failed to open durable job store; continuing in-memory only", err);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
111
130
|
const asyncJobManager = new AsyncJobManager(logger, (cli, durationMs, success) => {
|
|
112
131
|
performanceMetrics.recordRequest(cli, durationMs, success);
|
|
113
|
-
});
|
|
132
|
+
}, jobStore);
|
|
114
133
|
const approvalManager = new ApprovalManager(undefined, logger);
|
|
115
134
|
const MCP_SERVER_ENUM = z.enum(CLAUDE_MCP_SERVER_NAMES);
|
|
116
135
|
// Per-CLI idle timeouts: kill process if no stdout/stderr activity for this duration.
|
|
@@ -120,6 +139,7 @@ const CLI_IDLE_TIMEOUTS = {
|
|
|
120
139
|
claude: 600_000, // 10 minutes — only used when outputFormat=stream-json
|
|
121
140
|
codex: 600_000, // 10 minutes — Codex streams stderr progress
|
|
122
141
|
gemini: 600_000, // 10 minutes — Gemini streams stdout in real-time
|
|
142
|
+
grok: 600_000, // 10 minutes — Grok streams stderr/stdout activity in headless mode
|
|
123
143
|
};
|
|
124
144
|
function resolveIdleTimeout(cli, override) {
|
|
125
145
|
if (override !== undefined)
|
|
@@ -131,12 +151,21 @@ const SYNC_POLL_INTERVAL_MS = 1_000;
|
|
|
131
151
|
* Start an async job and poll until completion or deadline.
|
|
132
152
|
* Returns the job result if it finishes in time, or a deferral marker.
|
|
133
153
|
*/
|
|
134
|
-
async function awaitJobOrDefer(cli, args, corrId, idleTimeoutMs, outputFormat) {
|
|
154
|
+
async function awaitJobOrDefer(cli, args, corrId, idleTimeoutMs, outputFormat, forceRefresh) {
|
|
135
155
|
if (SYNC_DEADLINE_MS === 0) {
|
|
136
|
-
// Disabled — fall through to direct execution
|
|
156
|
+
// Disabled — fall through to direct execution.
|
|
157
|
+
// Note: direct execution bypasses dedup. forceRefresh is implied.
|
|
137
158
|
return executeCli(cli, args, { idleTimeout: idleTimeoutMs, logger });
|
|
138
159
|
}
|
|
139
|
-
const
|
|
160
|
+
const outcome = asyncJobManager.startJobWithDedup(cli, args, corrId, {
|
|
161
|
+
idleTimeoutMs,
|
|
162
|
+
outputFormat,
|
|
163
|
+
forceRefresh,
|
|
164
|
+
});
|
|
165
|
+
const job = outcome.snapshot;
|
|
166
|
+
if (outcome.deduped) {
|
|
167
|
+
logger.info(`[${corrId}] sync request deduped onto running job ${job.id} (original corrId=${outcome.originalCorrelationId})`);
|
|
168
|
+
}
|
|
140
169
|
const deadline = Date.now() + SYNC_DEADLINE_MS;
|
|
141
170
|
while (Date.now() < deadline) {
|
|
142
171
|
const snapshot = asyncJobManager.getJobSnapshot(job.id);
|
|
@@ -378,6 +407,16 @@ server.registerResource("gemini-sessions", "sessions://gemini", {
|
|
|
378
407
|
const contents = await resourceProvider.readResource(uri.href);
|
|
379
408
|
return { contents: contents ? [contents] : [] };
|
|
380
409
|
});
|
|
410
|
+
// Register Grok sessions resource
|
|
411
|
+
server.registerResource("grok-sessions", "sessions://grok", {
|
|
412
|
+
title: "⚡ Grok Sessions",
|
|
413
|
+
description: "Grok conversation sessions",
|
|
414
|
+
mimeType: "application/json",
|
|
415
|
+
}, async (uri) => {
|
|
416
|
+
logger.debug("Reading Grok sessions resource");
|
|
417
|
+
const contents = await resourceProvider.readResource(uri.href);
|
|
418
|
+
return { contents: contents ? [contents] : [] };
|
|
419
|
+
});
|
|
381
420
|
// Register Claude models resource
|
|
382
421
|
server.registerResource("claude-models", "models://claude", {
|
|
383
422
|
title: "🧠 Claude Models",
|
|
@@ -408,6 +447,16 @@ server.registerResource("gemini-models", "models://gemini", {
|
|
|
408
447
|
const contents = await resourceProvider.readResource(uri.href);
|
|
409
448
|
return { contents: contents ? [contents] : [] };
|
|
410
449
|
});
|
|
450
|
+
// Register Grok models resource
|
|
451
|
+
server.registerResource("grok-models", "models://grok", {
|
|
452
|
+
title: "⚡ Grok Models",
|
|
453
|
+
description: "Grok models and capabilities",
|
|
454
|
+
mimeType: "application/json",
|
|
455
|
+
}, async (uri) => {
|
|
456
|
+
logger.debug("Reading Grok models resource");
|
|
457
|
+
const contents = await resourceProvider.readResource(uri.href);
|
|
458
|
+
return { contents: contents ? [contents] : [] };
|
|
459
|
+
});
|
|
411
460
|
// Register performance metrics resource
|
|
412
461
|
server.registerResource("performance-metrics", "metrics://performance", {
|
|
413
462
|
title: "📈 Performance Metrics",
|
|
@@ -543,15 +592,40 @@ function prepareCodexRequest(params) {
|
|
|
543
592
|
return createApprovalDeniedResponse(params.operation, approvalDecision);
|
|
544
593
|
}
|
|
545
594
|
}
|
|
595
|
+
// Resume mode: codex exec resume <SESSION_ID|--last> [flags] PROMPT
|
|
596
|
+
// Note: `codex exec resume` does NOT accept `--full-auto`; the original
|
|
597
|
+
// session's approval policy is inherited. We silently drop fullAuto on resume.
|
|
598
|
+
let sessionPlan;
|
|
599
|
+
try {
|
|
600
|
+
sessionPlan = resolveCodexSessionArgs({
|
|
601
|
+
sessionId: params.sessionId,
|
|
602
|
+
resumeLatest: params.resumeLatest,
|
|
603
|
+
createNewSession: params.createNewSession,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
return createErrorResponse(params.operation, 1, "", corrId, err);
|
|
608
|
+
}
|
|
546
609
|
const args = ["exec"];
|
|
610
|
+
if (sessionPlan.mode !== "new") {
|
|
611
|
+
args.push("resume");
|
|
612
|
+
if (sessionPlan.mode === "resume-latest") {
|
|
613
|
+
args.push("--last");
|
|
614
|
+
}
|
|
615
|
+
}
|
|
547
616
|
if (resolvedModel)
|
|
548
617
|
args.push("--model", resolvedModel);
|
|
549
|
-
if (params.fullAuto)
|
|
618
|
+
if (sessionPlan.mode === "new" && params.fullAuto) {
|
|
550
619
|
args.push("--full-auto");
|
|
620
|
+
}
|
|
551
621
|
if (params.dangerouslyBypassApprovalsAndSandbox) {
|
|
552
622
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
553
623
|
}
|
|
554
|
-
args.push("--skip-git-repo-check"
|
|
624
|
+
args.push("--skip-git-repo-check");
|
|
625
|
+
if (sessionPlan.mode === "resume-by-id" && sessionPlan.sessionId) {
|
|
626
|
+
args.push(sessionPlan.sessionId);
|
|
627
|
+
}
|
|
628
|
+
args.push(effectivePrompt);
|
|
555
629
|
return {
|
|
556
630
|
corrId,
|
|
557
631
|
effectivePrompt,
|
|
@@ -631,6 +705,81 @@ function prepareGeminiRequest(params) {
|
|
|
631
705
|
args,
|
|
632
706
|
};
|
|
633
707
|
}
|
|
708
|
+
function prepareGrokRequest(params) {
|
|
709
|
+
const corrId = params.correlationId || randomUUID();
|
|
710
|
+
const cliInfo = getCliInfo();
|
|
711
|
+
const resolvedModel = resolveModelAlias("grok", params.model, cliInfo);
|
|
712
|
+
// Review integrity check on raw prompt (before optimization)
|
|
713
|
+
const reviewIntegrity = checkReviewIntegrity({
|
|
714
|
+
prompt: params.prompt,
|
|
715
|
+
allowedTools: params.allowedTools,
|
|
716
|
+
disallowedTools: params.disallowedTools,
|
|
717
|
+
});
|
|
718
|
+
if (reviewIntegrity.violations.length > 0) {
|
|
719
|
+
logger.info(`[${corrId}] Review integrity violations detected: ${reviewIntegrity.violations.map(v => v.type).join(", ")}`, {
|
|
720
|
+
cli: "grok",
|
|
721
|
+
operation: params.operation,
|
|
722
|
+
score: reviewIntegrity.totalScore,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
let effectivePrompt = params.prompt;
|
|
726
|
+
if (params.optimizePrompt) {
|
|
727
|
+
const optimized = optimizePromptText(effectivePrompt);
|
|
728
|
+
logOptimizationTokens("prompt", corrId, effectivePrompt, optimized);
|
|
729
|
+
effectivePrompt = optimized;
|
|
730
|
+
}
|
|
731
|
+
const requestedMcpServers = normalizeMcpServers(params.mcpServers);
|
|
732
|
+
let approvalDecision = null;
|
|
733
|
+
if (params.approvalStrategy === "mcp_managed") {
|
|
734
|
+
approvalDecision = approvalManager.decide({
|
|
735
|
+
cli: "grok",
|
|
736
|
+
operation: params.operation,
|
|
737
|
+
prompt: params.prompt, // Use raw prompt for review-context detection, not optimized
|
|
738
|
+
bypassRequested: Boolean(params.alwaysApprove) || params.permissionMode === "bypassPermissions",
|
|
739
|
+
fullAuto: false,
|
|
740
|
+
requestedMcpServers,
|
|
741
|
+
allowedTools: params.allowedTools,
|
|
742
|
+
disallowedTools: params.disallowedTools,
|
|
743
|
+
policy: params.approvalPolicy,
|
|
744
|
+
metadata: { model: resolvedModel || "default" },
|
|
745
|
+
reviewIntegrity,
|
|
746
|
+
});
|
|
747
|
+
if (approvalDecision.status !== "approved") {
|
|
748
|
+
return createApprovalDeniedResponse(params.operation, approvalDecision);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const effectiveAlwaysApprove = params.approvalStrategy === "mcp_managed" ? true : Boolean(params.alwaysApprove);
|
|
752
|
+
const args = ["-p", effectivePrompt];
|
|
753
|
+
if (resolvedModel)
|
|
754
|
+
args.push("--model", resolvedModel);
|
|
755
|
+
if (params.outputFormat)
|
|
756
|
+
args.push("--output-format", params.outputFormat);
|
|
757
|
+
if (effectiveAlwaysApprove) {
|
|
758
|
+
args.push("--always-approve");
|
|
759
|
+
}
|
|
760
|
+
else if (params.permissionMode) {
|
|
761
|
+
args.push("--permission-mode", params.permissionMode);
|
|
762
|
+
}
|
|
763
|
+
if (params.effort)
|
|
764
|
+
args.push("--effort", params.effort);
|
|
765
|
+
if (params.reasoningEffort)
|
|
766
|
+
args.push("--reasoning-effort", params.reasoningEffort);
|
|
767
|
+
if (params.allowedTools && params.allowedTools.length > 0) {
|
|
768
|
+
args.push("--tools", params.allowedTools.join(","));
|
|
769
|
+
}
|
|
770
|
+
if (params.disallowedTools && params.disallowedTools.length > 0) {
|
|
771
|
+
args.push("--disallowed-tools", params.disallowedTools.join(","));
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
corrId,
|
|
775
|
+
effectivePrompt,
|
|
776
|
+
resolvedModel,
|
|
777
|
+
requestedMcpServers,
|
|
778
|
+
approvalDecision,
|
|
779
|
+
reviewIntegrity,
|
|
780
|
+
args,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
634
783
|
function buildCliResponse(cli, stdout, optimizeResponse, corrId, sessionId, prep, durationMs, resumable, outputFormat) {
|
|
635
784
|
let finalStdout = stdout;
|
|
636
785
|
// Skip response optimization for JSON output to prevent corrupting structured data
|
|
@@ -718,7 +867,7 @@ export async function handleGeminiRequest(deps, params) {
|
|
|
718
867
|
createNewSession: params.createNewSession,
|
|
719
868
|
});
|
|
720
869
|
args.push(...sessionResult.resumeArgs);
|
|
721
|
-
const result = await awaitJobOrDefer("gemini", args, corrId, resolveIdleTimeout("gemini", params.idleTimeoutMs));
|
|
870
|
+
const result = await awaitJobOrDefer("gemini", args, corrId, resolveIdleTimeout("gemini", params.idleTimeoutMs), undefined, params.forceRefresh);
|
|
722
871
|
// Deferred — job still running, return async reference
|
|
723
872
|
if (isDeferredResponse(result)) {
|
|
724
873
|
return buildDeferredToolResponse(result, sessionResult.effectiveSessionId);
|
|
@@ -840,7 +989,7 @@ export async function handleGeminiRequestAsync(deps, params) {
|
|
|
840
989
|
effectiveSessionId = newSession.id;
|
|
841
990
|
}
|
|
842
991
|
// Start job only after all session I/O succeeds
|
|
843
|
-
const job = deps.asyncJobManager.startJob("gemini", args, corrId, undefined, resolveIdleTimeout("gemini", params.idleTimeoutMs));
|
|
992
|
+
const job = deps.asyncJobManager.startJob("gemini", args, corrId, undefined, resolveIdleTimeout("gemini", params.idleTimeoutMs), undefined, params.forceRefresh);
|
|
844
993
|
deps.logger.info(`[${corrId}] gemini_request_async started job ${job.id}`);
|
|
845
994
|
const asyncResponse = {
|
|
846
995
|
success: true,
|
|
@@ -866,6 +1015,198 @@ export async function handleGeminiRequestAsync(deps, params) {
|
|
|
866
1015
|
return createErrorResponse("gemini_request_async", 1, "", corrId, error);
|
|
867
1016
|
}
|
|
868
1017
|
}
|
|
1018
|
+
export async function handleGrokRequest(deps, params) {
|
|
1019
|
+
const startTime = Date.now();
|
|
1020
|
+
const prep = prepareGrokRequest({
|
|
1021
|
+
prompt: params.prompt,
|
|
1022
|
+
model: params.model,
|
|
1023
|
+
outputFormat: params.outputFormat,
|
|
1024
|
+
alwaysApprove: params.alwaysApprove,
|
|
1025
|
+
permissionMode: params.permissionMode,
|
|
1026
|
+
effort: params.effort,
|
|
1027
|
+
reasoningEffort: params.reasoningEffort,
|
|
1028
|
+
allowedTools: params.allowedTools,
|
|
1029
|
+
disallowedTools: params.disallowedTools,
|
|
1030
|
+
approvalStrategy: params.approvalStrategy,
|
|
1031
|
+
approvalPolicy: params.approvalPolicy,
|
|
1032
|
+
mcpServers: params.mcpServers,
|
|
1033
|
+
correlationId: params.correlationId,
|
|
1034
|
+
optimizePrompt: params.optimizePrompt,
|
|
1035
|
+
operation: "grok_request",
|
|
1036
|
+
});
|
|
1037
|
+
if (!("args" in prep))
|
|
1038
|
+
return prep;
|
|
1039
|
+
const { corrId, args } = prep;
|
|
1040
|
+
let durationMs = 0;
|
|
1041
|
+
let wasSuccessful = false;
|
|
1042
|
+
safeFlightStart({
|
|
1043
|
+
correlationId: corrId,
|
|
1044
|
+
cli: "grok",
|
|
1045
|
+
model: prep.resolvedModel || "default",
|
|
1046
|
+
prompt: params.prompt,
|
|
1047
|
+
sessionId: params.sessionId,
|
|
1048
|
+
});
|
|
1049
|
+
deps.logger.info(`[${corrId}] grok_request invoked with model=${prep.resolvedModel || "default"}, permissionMode=${params.permissionMode}, prompt length=${params.prompt.length}`);
|
|
1050
|
+
try {
|
|
1051
|
+
// Session arg planning (pure, no I/O)
|
|
1052
|
+
const sessionResult = resolveGrokSessionArgs({
|
|
1053
|
+
sessionId: params.sessionId,
|
|
1054
|
+
resumeLatest: params.resumeLatest,
|
|
1055
|
+
createNewSession: params.createNewSession,
|
|
1056
|
+
});
|
|
1057
|
+
args.push(...sessionResult.resumeArgs);
|
|
1058
|
+
const result = await awaitJobOrDefer("grok", args, corrId, resolveIdleTimeout("grok", params.idleTimeoutMs), params.outputFormat, params.forceRefresh);
|
|
1059
|
+
// Deferred — job still running, return async reference
|
|
1060
|
+
if (isDeferredResponse(result)) {
|
|
1061
|
+
return buildDeferredToolResponse(result, sessionResult.effectiveSessionId);
|
|
1062
|
+
}
|
|
1063
|
+
const { stdout, stderr, code } = result;
|
|
1064
|
+
durationMs = Math.max(0, Date.now() - startTime);
|
|
1065
|
+
if (code !== 0) {
|
|
1066
|
+
deps.logger.info(`[${corrId}] grok_request failed in ${durationMs}ms`);
|
|
1067
|
+
safeFlightComplete(corrId, {
|
|
1068
|
+
response: stderr || "",
|
|
1069
|
+
durationMs,
|
|
1070
|
+
retryCount: 0,
|
|
1071
|
+
circuitBreakerState: "closed",
|
|
1072
|
+
optimizationApplied: false,
|
|
1073
|
+
exitCode: code,
|
|
1074
|
+
errorMessage: stderr || `Exit code ${code}`,
|
|
1075
|
+
status: "failed",
|
|
1076
|
+
});
|
|
1077
|
+
return createErrorResponse("grok", code, stderr, corrId);
|
|
1078
|
+
}
|
|
1079
|
+
wasSuccessful = true;
|
|
1080
|
+
// Post-success session I/O (sync handlers: no phantom sessions on CLI failure)
|
|
1081
|
+
let effectiveSessionId = sessionResult.effectiveSessionId;
|
|
1082
|
+
if (sessionResult.userProvidedSession && effectiveSessionId) {
|
|
1083
|
+
const existing = await deps.sessionManager.getSession(effectiveSessionId);
|
|
1084
|
+
if (!existing) {
|
|
1085
|
+
try {
|
|
1086
|
+
await deps.sessionManager.createSession("grok", "Grok Session", effectiveSessionId);
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
const rechecked = await deps.sessionManager.getSession(effectiveSessionId);
|
|
1090
|
+
if (!rechecked)
|
|
1091
|
+
throw new Error(`Failed to create or find session ${effectiveSessionId}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
await deps.sessionManager.updateSessionUsage(effectiveSessionId);
|
|
1095
|
+
}
|
|
1096
|
+
else if (!params.createNewSession && !effectiveSessionId) {
|
|
1097
|
+
const newSession = await deps.sessionManager.createSession("grok", "Grok Session", `${GATEWAY_SESSION_PREFIX}${randomUUID()}`);
|
|
1098
|
+
effectiveSessionId = newSession.id;
|
|
1099
|
+
}
|
|
1100
|
+
deps.logger.info(`[${corrId}] grok_request completed successfully in ${durationMs}ms`);
|
|
1101
|
+
const response = buildCliResponse("grok", stdout, params.optimizeResponse ?? false, corrId, effectiveSessionId, prep, durationMs, sessionResult.userProvidedSession, params.outputFormat);
|
|
1102
|
+
safeFlightComplete(corrId, {
|
|
1103
|
+
response: stdout,
|
|
1104
|
+
durationMs,
|
|
1105
|
+
retryCount: 0,
|
|
1106
|
+
circuitBreakerState: "closed",
|
|
1107
|
+
approvalDecision: prep.approvalDecision?.status,
|
|
1108
|
+
optimizationApplied: params.optimizePrompt || (params.optimizeResponse ?? false),
|
|
1109
|
+
exitCode: 0,
|
|
1110
|
+
status: "completed",
|
|
1111
|
+
});
|
|
1112
|
+
return response;
|
|
1113
|
+
}
|
|
1114
|
+
catch (error) {
|
|
1115
|
+
const elapsedMs = Math.max(0, Date.now() - startTime);
|
|
1116
|
+
deps.logger.info(`[${corrId}] grok_request threw exception after ${elapsedMs}ms`);
|
|
1117
|
+
safeFlightComplete(corrId, {
|
|
1118
|
+
response: "",
|
|
1119
|
+
durationMs: elapsedMs,
|
|
1120
|
+
retryCount: 0,
|
|
1121
|
+
circuitBreakerState: "closed",
|
|
1122
|
+
optimizationApplied: false,
|
|
1123
|
+
exitCode: 1,
|
|
1124
|
+
errorMessage: error.message,
|
|
1125
|
+
status: "failed",
|
|
1126
|
+
});
|
|
1127
|
+
return createErrorResponse("grok", 1, "", corrId, error);
|
|
1128
|
+
}
|
|
1129
|
+
finally {
|
|
1130
|
+
const finalizedDurationMs = Math.max(0, durationMs || Date.now() - startTime);
|
|
1131
|
+
performanceMetrics.recordRequest("grok", finalizedDurationMs, wasSuccessful);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
export async function handleGrokRequestAsync(deps, params) {
|
|
1135
|
+
const prep = prepareGrokRequest({
|
|
1136
|
+
prompt: params.prompt,
|
|
1137
|
+
model: params.model,
|
|
1138
|
+
outputFormat: params.outputFormat,
|
|
1139
|
+
alwaysApprove: params.alwaysApprove,
|
|
1140
|
+
permissionMode: params.permissionMode,
|
|
1141
|
+
effort: params.effort,
|
|
1142
|
+
reasoningEffort: params.reasoningEffort,
|
|
1143
|
+
allowedTools: params.allowedTools,
|
|
1144
|
+
disallowedTools: params.disallowedTools,
|
|
1145
|
+
approvalStrategy: params.approvalStrategy,
|
|
1146
|
+
approvalPolicy: params.approvalPolicy,
|
|
1147
|
+
mcpServers: params.mcpServers,
|
|
1148
|
+
correlationId: params.correlationId,
|
|
1149
|
+
optimizePrompt: params.optimizePrompt,
|
|
1150
|
+
operation: "grok_request_async",
|
|
1151
|
+
});
|
|
1152
|
+
if (!("args" in prep))
|
|
1153
|
+
return prep;
|
|
1154
|
+
const { corrId, args, requestedMcpServers, approvalDecision } = prep;
|
|
1155
|
+
try {
|
|
1156
|
+
// Session arg planning (pure, no I/O)
|
|
1157
|
+
const sessionResult = resolveGrokSessionArgs({
|
|
1158
|
+
sessionId: params.sessionId,
|
|
1159
|
+
resumeLatest: params.resumeLatest,
|
|
1160
|
+
createNewSession: params.createNewSession,
|
|
1161
|
+
});
|
|
1162
|
+
args.push(...sessionResult.resumeArgs);
|
|
1163
|
+
// Pre-start session I/O (async handlers: prevent orphaned jobs)
|
|
1164
|
+
let effectiveSessionId = sessionResult.effectiveSessionId;
|
|
1165
|
+
if (sessionResult.userProvidedSession && effectiveSessionId) {
|
|
1166
|
+
const existing = await deps.sessionManager.getSession(effectiveSessionId);
|
|
1167
|
+
if (!existing) {
|
|
1168
|
+
try {
|
|
1169
|
+
await deps.sessionManager.createSession("grok", "Grok Session", effectiveSessionId);
|
|
1170
|
+
}
|
|
1171
|
+
catch {
|
|
1172
|
+
const rechecked = await deps.sessionManager.getSession(effectiveSessionId);
|
|
1173
|
+
if (!rechecked)
|
|
1174
|
+
throw new Error(`Failed to create or find session ${effectiveSessionId}`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
await deps.sessionManager.updateSessionUsage(effectiveSessionId);
|
|
1178
|
+
}
|
|
1179
|
+
else if (!params.createNewSession && !effectiveSessionId) {
|
|
1180
|
+
const newSession = await deps.sessionManager.createSession("grok", "Grok Session", `${GATEWAY_SESSION_PREFIX}${randomUUID()}`);
|
|
1181
|
+
effectiveSessionId = newSession.id;
|
|
1182
|
+
}
|
|
1183
|
+
// Start job only after all session I/O succeeds
|
|
1184
|
+
const job = deps.asyncJobManager.startJob("grok", args, corrId, undefined, resolveIdleTimeout("grok", params.idleTimeoutMs), params.outputFormat, params.forceRefresh);
|
|
1185
|
+
deps.logger.info(`[${corrId}] grok_request_async started job ${job.id}`);
|
|
1186
|
+
const asyncResponse = {
|
|
1187
|
+
success: true,
|
|
1188
|
+
job,
|
|
1189
|
+
sessionId: effectiveSessionId || null,
|
|
1190
|
+
resumable: sessionResult.userProvidedSession,
|
|
1191
|
+
approval: approvalDecision,
|
|
1192
|
+
mcpServers: { requested: requestedMcpServers },
|
|
1193
|
+
};
|
|
1194
|
+
if (prep.reviewIntegrity && prep.reviewIntegrity.violations.length > 0) {
|
|
1195
|
+
asyncResponse.reviewIntegrity = prep.reviewIntegrity;
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
content: [
|
|
1199
|
+
{
|
|
1200
|
+
type: "text",
|
|
1201
|
+
text: JSON.stringify(asyncResponse, null, 2),
|
|
1202
|
+
},
|
|
1203
|
+
],
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
catch (error) {
|
|
1207
|
+
return createErrorResponse("grok_request_async", 1, "", corrId, error);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
869
1210
|
export async function handleCodexRequestAsync(deps, params) {
|
|
870
1211
|
const prep = prepareCodexRequest({
|
|
871
1212
|
prompt: params.prompt,
|
|
@@ -875,6 +1216,9 @@ export async function handleCodexRequestAsync(deps, params) {
|
|
|
875
1216
|
approvalStrategy: params.approvalStrategy,
|
|
876
1217
|
approvalPolicy: params.approvalPolicy,
|
|
877
1218
|
mcpServers: params.mcpServers,
|
|
1219
|
+
sessionId: params.sessionId,
|
|
1220
|
+
resumeLatest: params.resumeLatest,
|
|
1221
|
+
createNewSession: params.createNewSession,
|
|
878
1222
|
correlationId: params.correlationId,
|
|
879
1223
|
optimizePrompt: params.optimizePrompt,
|
|
880
1224
|
operation: "codex_request_async",
|
|
@@ -903,7 +1247,7 @@ export async function handleCodexRequestAsync(deps, params) {
|
|
|
903
1247
|
effectiveSessionId = newSession.id;
|
|
904
1248
|
}
|
|
905
1249
|
// Start job only after all session I/O succeeds
|
|
906
|
-
const job = deps.asyncJobManager.startJob("codex", args, corrId, undefined, resolveIdleTimeout("codex", params.idleTimeoutMs));
|
|
1250
|
+
const job = deps.asyncJobManager.startJob("codex", args, corrId, undefined, resolveIdleTimeout("codex", params.idleTimeoutMs), undefined, params.forceRefresh);
|
|
907
1251
|
deps.logger.info(`[${corrId}] codex_request_async started job ${job.id}`);
|
|
908
1252
|
const asyncResponse = {
|
|
909
1253
|
success: true,
|
|
@@ -983,7 +1327,11 @@ server.tool("claude_request", {
|
|
|
983
1327
|
.max(3_600_000)
|
|
984
1328
|
.optional()
|
|
985
1329
|
.describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
|
|
986
|
-
|
|
1330
|
+
forceRefresh: z
|
|
1331
|
+
.boolean()
|
|
1332
|
+
.default(false)
|
|
1333
|
+
.describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
|
|
1334
|
+
}, async ({ prompt, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
|
|
987
1335
|
const startTime = Date.now();
|
|
988
1336
|
const prep = prepareClaudeRequest({
|
|
989
1337
|
prompt,
|
|
@@ -1034,7 +1382,7 @@ server.tool("claude_request", {
|
|
|
1034
1382
|
}
|
|
1035
1383
|
// Idle timeout only for stream-json (text/json produce no output until done)
|
|
1036
1384
|
const effectiveIdleTimeout = outputFormat === "stream-json" ? resolveIdleTimeout("claude", idleTimeoutMs) : undefined;
|
|
1037
|
-
const result = await awaitJobOrDefer("claude", args, corrId, effectiveIdleTimeout, outputFormat);
|
|
1385
|
+
const result = await awaitJobOrDefer("claude", args, corrId, effectiveIdleTimeout, outputFormat, forceRefresh);
|
|
1038
1386
|
// Deferred — job still running, return async reference
|
|
1039
1387
|
if (isDeferredResponse(result)) {
|
|
1040
1388
|
return buildDeferredToolResponse(result, effectiveSessionId);
|
|
@@ -1142,8 +1490,15 @@ server.tool("codex_request", {
|
|
|
1142
1490
|
.array(MCP_SERVER_ENUM)
|
|
1143
1491
|
.default(["sqry"])
|
|
1144
1492
|
.describe("MCP server names for approval tracking (Codex manages its own MCP config)"),
|
|
1145
|
-
sessionId: z
|
|
1146
|
-
|
|
1493
|
+
sessionId: z
|
|
1494
|
+
.string()
|
|
1495
|
+
.optional()
|
|
1496
|
+
.describe("Codex session UUID to resume via `codex exec resume <ID>`. Must be a real Codex session ID (from `~/.codex/sessions/` or the `codex resume` picker). Gateway-generated `gw-*` IDs are rejected."),
|
|
1497
|
+
resumeLatest: z
|
|
1498
|
+
.boolean()
|
|
1499
|
+
.default(false)
|
|
1500
|
+
.describe("Resume the most recent Codex session in the current cwd via `codex exec resume --last`. Ignored if sessionId is set."),
|
|
1501
|
+
createNewSession: z.boolean().default(false).describe("Force a fresh session (no resume)"),
|
|
1147
1502
|
correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
|
|
1148
1503
|
optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
|
|
1149
1504
|
optimizeResponse: z.boolean().default(false).describe("Optimize response output"),
|
|
@@ -1154,7 +1509,11 @@ server.tool("codex_request", {
|
|
|
1154
1509
|
.max(3_600_000)
|
|
1155
1510
|
.optional()
|
|
1156
1511
|
.describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
|
|
1157
|
-
|
|
1512
|
+
forceRefresh: z
|
|
1513
|
+
.boolean()
|
|
1514
|
+
.default(false)
|
|
1515
|
+
.describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
|
|
1516
|
+
}, async ({ prompt, model, fullAuto, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
|
|
1158
1517
|
const startTime = Date.now();
|
|
1159
1518
|
const prep = prepareCodexRequest({
|
|
1160
1519
|
prompt,
|
|
@@ -1164,6 +1523,9 @@ server.tool("codex_request", {
|
|
|
1164
1523
|
approvalStrategy,
|
|
1165
1524
|
approvalPolicy,
|
|
1166
1525
|
mcpServers,
|
|
1526
|
+
sessionId,
|
|
1527
|
+
resumeLatest,
|
|
1528
|
+
createNewSession,
|
|
1167
1529
|
correlationId,
|
|
1168
1530
|
optimizePrompt,
|
|
1169
1531
|
operation: "codex_request",
|
|
@@ -1182,7 +1544,7 @@ server.tool("codex_request", {
|
|
|
1182
1544
|
});
|
|
1183
1545
|
logger.info(`[${corrId}] codex_request invoked with model=${prep.resolvedModel || "default"}, fullAuto=${fullAuto}, prompt length=${prompt.length}`);
|
|
1184
1546
|
try {
|
|
1185
|
-
const result = await awaitJobOrDefer("codex", args, corrId, resolveIdleTimeout("codex", idleTimeoutMs));
|
|
1547
|
+
const result = await awaitJobOrDefer("codex", args, corrId, resolveIdleTimeout("codex", idleTimeoutMs), undefined, forceRefresh);
|
|
1186
1548
|
// Deferred — job still running, return async reference
|
|
1187
1549
|
if (isDeferredResponse(result)) {
|
|
1188
1550
|
return buildDeferredToolResponse(result, sessionId);
|
|
@@ -1302,7 +1664,11 @@ server.tool("gemini_request", {
|
|
|
1302
1664
|
.max(3_600_000)
|
|
1303
1665
|
.optional()
|
|
1304
1666
|
.describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
|
|
1305
|
-
|
|
1667
|
+
forceRefresh: z
|
|
1668
|
+
.boolean()
|
|
1669
|
+
.default(false)
|
|
1670
|
+
.describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
|
|
1671
|
+
}, async ({ prompt, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
|
|
1306
1672
|
return handleGeminiRequest({ sessionManager, logger }, {
|
|
1307
1673
|
prompt,
|
|
1308
1674
|
model,
|
|
@@ -1319,6 +1685,98 @@ server.tool("gemini_request", {
|
|
|
1319
1685
|
optimizePrompt,
|
|
1320
1686
|
optimizeResponse,
|
|
1321
1687
|
idleTimeoutMs,
|
|
1688
|
+
forceRefresh,
|
|
1689
|
+
});
|
|
1690
|
+
});
|
|
1691
|
+
//──────────────────────────────────────────────────────────────────────────────
|
|
1692
|
+
// Grok Tool
|
|
1693
|
+
//──────────────────────────────────────────────────────────────────────────────
|
|
1694
|
+
server.tool("grok_request", {
|
|
1695
|
+
prompt: z
|
|
1696
|
+
.string()
|
|
1697
|
+
.min(1, "Prompt cannot be empty")
|
|
1698
|
+
.max(100000, "Prompt too long (max 100k chars)")
|
|
1699
|
+
.describe("Prompt text for Grok"),
|
|
1700
|
+
model: z.string().optional().describe("Model name or alias (e.g. grok-build, latest)"),
|
|
1701
|
+
outputFormat: z
|
|
1702
|
+
.enum(["plain", "json", "streaming-json"])
|
|
1703
|
+
.optional()
|
|
1704
|
+
.describe("Output format (plain|json|streaming-json). Grok default is plain."),
|
|
1705
|
+
sessionId: z.string().optional().describe("Session ID (user-provided CLI handle for --resume)"),
|
|
1706
|
+
resumeLatest: z
|
|
1707
|
+
.boolean()
|
|
1708
|
+
.default(false)
|
|
1709
|
+
.describe("Resume most recent Grok session in cwd (--continue)"),
|
|
1710
|
+
createNewSession: z.boolean().default(false).describe("Force new session"),
|
|
1711
|
+
alwaysApprove: z
|
|
1712
|
+
.boolean()
|
|
1713
|
+
.default(false)
|
|
1714
|
+
.describe("Auto-approve all tool executions (--always-approve)"),
|
|
1715
|
+
permissionMode: z
|
|
1716
|
+
.enum(["default", "acceptEdits", "auto", "dontAsk", "bypassPermissions", "plan"])
|
|
1717
|
+
.optional()
|
|
1718
|
+
.describe("Grok permission mode"),
|
|
1719
|
+
effort: z
|
|
1720
|
+
.enum(["low", "medium", "high", "xhigh", "max"])
|
|
1721
|
+
.optional()
|
|
1722
|
+
.describe("Grok effort level"),
|
|
1723
|
+
reasoningEffort: z.string().optional().describe("Reasoning effort for reasoning models"),
|
|
1724
|
+
approvalStrategy: z
|
|
1725
|
+
.enum(["legacy", "mcp_managed"])
|
|
1726
|
+
.default("legacy")
|
|
1727
|
+
.describe("Approval strategy"),
|
|
1728
|
+
approvalPolicy: z
|
|
1729
|
+
.enum(["strict", "balanced", "permissive"])
|
|
1730
|
+
.optional()
|
|
1731
|
+
.describe("Approval policy override"),
|
|
1732
|
+
mcpServers: z
|
|
1733
|
+
.array(MCP_SERVER_ENUM)
|
|
1734
|
+
.default(["sqry"])
|
|
1735
|
+
.describe("MCP server names for approval tracking (Grok manages its own MCP config via `grok mcp`)"),
|
|
1736
|
+
allowedTools: z
|
|
1737
|
+
.array(z.string())
|
|
1738
|
+
.optional()
|
|
1739
|
+
.describe("Allowed built-in tools (passed as --tools comma list)"),
|
|
1740
|
+
disallowedTools: z
|
|
1741
|
+
.array(z.string())
|
|
1742
|
+
.optional()
|
|
1743
|
+
.describe("Disallowed built-in tools (passed as --disallowed-tools comma list)"),
|
|
1744
|
+
correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
|
|
1745
|
+
optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
|
|
1746
|
+
optimizeResponse: z.boolean().default(false).describe("Optimize response output"),
|
|
1747
|
+
idleTimeoutMs: z
|
|
1748
|
+
.number()
|
|
1749
|
+
.int()
|
|
1750
|
+
.min(30_000)
|
|
1751
|
+
.max(3_600_000)
|
|
1752
|
+
.optional()
|
|
1753
|
+
.describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
|
|
1754
|
+
forceRefresh: z
|
|
1755
|
+
.boolean()
|
|
1756
|
+
.default(false)
|
|
1757
|
+
.describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
|
|
1758
|
+
}, async ({ prompt, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, forceRefresh, }) => {
|
|
1759
|
+
return handleGrokRequest({ sessionManager, logger }, {
|
|
1760
|
+
prompt,
|
|
1761
|
+
model,
|
|
1762
|
+
outputFormat,
|
|
1763
|
+
sessionId,
|
|
1764
|
+
resumeLatest,
|
|
1765
|
+
createNewSession,
|
|
1766
|
+
alwaysApprove,
|
|
1767
|
+
permissionMode,
|
|
1768
|
+
effort,
|
|
1769
|
+
reasoningEffort,
|
|
1770
|
+
approvalStrategy,
|
|
1771
|
+
approvalPolicy,
|
|
1772
|
+
mcpServers,
|
|
1773
|
+
allowedTools,
|
|
1774
|
+
disallowedTools,
|
|
1775
|
+
correlationId,
|
|
1776
|
+
optimizePrompt,
|
|
1777
|
+
optimizeResponse,
|
|
1778
|
+
idleTimeoutMs,
|
|
1779
|
+
forceRefresh,
|
|
1322
1780
|
});
|
|
1323
1781
|
});
|
|
1324
1782
|
//──────────────────────────────────────────────────────────────────────────────
|
|
@@ -1375,7 +1833,11 @@ server.tool("claude_request_async", {
|
|
|
1375
1833
|
.max(3_600_000)
|
|
1376
1834
|
.optional()
|
|
1377
1835
|
.describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
|
|
1378
|
-
|
|
1836
|
+
forceRefresh: z
|
|
1837
|
+
.boolean()
|
|
1838
|
+
.default(false)
|
|
1839
|
+
.describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
|
|
1840
|
+
}, async ({ prompt, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
|
|
1379
1841
|
const prep = prepareClaudeRequest({
|
|
1380
1842
|
prompt,
|
|
1381
1843
|
model,
|
|
@@ -1421,7 +1883,7 @@ server.tool("claude_request_async", {
|
|
|
1421
1883
|
}
|
|
1422
1884
|
// Idle timeout only for stream-json (text/json produce no output until done)
|
|
1423
1885
|
const effectiveIdleTimeout = outputFormat === "stream-json" ? resolveIdleTimeout("claude", idleTimeoutMs) : undefined;
|
|
1424
|
-
const job = asyncJobManager.startJob("claude", args, corrId, undefined, effectiveIdleTimeout, outputFormat);
|
|
1886
|
+
const job = asyncJobManager.startJob("claude", args, corrId, undefined, effectiveIdleTimeout, outputFormat, forceRefresh);
|
|
1425
1887
|
logger.info(`[${corrId}] claude_request_async started job ${job.id}, outputFormat=${outputFormat}`);
|
|
1426
1888
|
const asyncResponse = {
|
|
1427
1889
|
success: true,
|
|
@@ -1474,8 +1936,15 @@ server.tool("codex_request_async", {
|
|
|
1474
1936
|
.array(MCP_SERVER_ENUM)
|
|
1475
1937
|
.default(["sqry"])
|
|
1476
1938
|
.describe("MCP server names for approval tracking (Codex manages its own MCP config)"),
|
|
1477
|
-
sessionId: z
|
|
1478
|
-
|
|
1939
|
+
sessionId: z
|
|
1940
|
+
.string()
|
|
1941
|
+
.optional()
|
|
1942
|
+
.describe("Codex session UUID to resume via `codex exec resume <ID>`. Must be a real Codex session ID (from `~/.codex/sessions/` or the `codex resume` picker). Gateway-generated `gw-*` IDs are rejected."),
|
|
1943
|
+
resumeLatest: z
|
|
1944
|
+
.boolean()
|
|
1945
|
+
.default(false)
|
|
1946
|
+
.describe("Resume the most recent Codex session in the current cwd via `codex exec resume --last`. Ignored if sessionId is set."),
|
|
1947
|
+
createNewSession: z.boolean().default(false).describe("Force a fresh session (no resume)"),
|
|
1479
1948
|
correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
|
|
1480
1949
|
optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
|
|
1481
1950
|
idleTimeoutMs: z
|
|
@@ -1485,7 +1954,11 @@ server.tool("codex_request_async", {
|
|
|
1485
1954
|
.max(3_600_000)
|
|
1486
1955
|
.optional()
|
|
1487
1956
|
.describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
|
|
1488
|
-
|
|
1957
|
+
forceRefresh: z
|
|
1958
|
+
.boolean()
|
|
1959
|
+
.default(false)
|
|
1960
|
+
.describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
|
|
1961
|
+
}, async ({ prompt, model, fullAuto, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, resumeLatest, createNewSession, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
|
|
1489
1962
|
return handleCodexRequestAsync({ sessionManager, asyncJobManager, logger }, {
|
|
1490
1963
|
prompt,
|
|
1491
1964
|
model,
|
|
@@ -1495,10 +1968,12 @@ server.tool("codex_request_async", {
|
|
|
1495
1968
|
approvalPolicy,
|
|
1496
1969
|
mcpServers,
|
|
1497
1970
|
sessionId,
|
|
1971
|
+
resumeLatest,
|
|
1498
1972
|
createNewSession,
|
|
1499
1973
|
correlationId,
|
|
1500
1974
|
optimizePrompt,
|
|
1501
1975
|
idleTimeoutMs,
|
|
1976
|
+
forceRefresh,
|
|
1502
1977
|
});
|
|
1503
1978
|
});
|
|
1504
1979
|
server.tool("gemini_request_async", {
|
|
@@ -1544,7 +2019,11 @@ server.tool("gemini_request_async", {
|
|
|
1544
2019
|
.max(3_600_000)
|
|
1545
2020
|
.optional()
|
|
1546
2021
|
.describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
|
|
1547
|
-
|
|
2022
|
+
forceRefresh: z
|
|
2023
|
+
.boolean()
|
|
2024
|
+
.default(false)
|
|
2025
|
+
.describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
|
|
2026
|
+
}, async ({ prompt, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
|
|
1548
2027
|
return handleGeminiRequestAsync({ sessionManager, asyncJobManager, logger }, {
|
|
1549
2028
|
prompt,
|
|
1550
2029
|
model,
|
|
@@ -1560,6 +2039,93 @@ server.tool("gemini_request_async", {
|
|
|
1560
2039
|
correlationId,
|
|
1561
2040
|
optimizePrompt,
|
|
1562
2041
|
idleTimeoutMs,
|
|
2042
|
+
forceRefresh,
|
|
2043
|
+
});
|
|
2044
|
+
});
|
|
2045
|
+
server.tool("grok_request_async", {
|
|
2046
|
+
prompt: z
|
|
2047
|
+
.string()
|
|
2048
|
+
.min(1, "Prompt cannot be empty")
|
|
2049
|
+
.max(100000, "Prompt too long (max 100k chars)")
|
|
2050
|
+
.describe("Prompt text for Grok"),
|
|
2051
|
+
model: z.string().optional().describe("Model name or alias (e.g. grok-build, latest)"),
|
|
2052
|
+
outputFormat: z
|
|
2053
|
+
.enum(["plain", "json", "streaming-json"])
|
|
2054
|
+
.optional()
|
|
2055
|
+
.describe("Output format (plain|json|streaming-json). Grok default is plain."),
|
|
2056
|
+
sessionId: z.string().optional().describe("Session ID (user-provided CLI handle for --resume)"),
|
|
2057
|
+
resumeLatest: z
|
|
2058
|
+
.boolean()
|
|
2059
|
+
.default(false)
|
|
2060
|
+
.describe("Resume most recent Grok session in cwd (--continue)"),
|
|
2061
|
+
createNewSession: z.boolean().default(false).describe("Force new session"),
|
|
2062
|
+
alwaysApprove: z
|
|
2063
|
+
.boolean()
|
|
2064
|
+
.default(false)
|
|
2065
|
+
.describe("Auto-approve all tool executions (--always-approve)"),
|
|
2066
|
+
permissionMode: z
|
|
2067
|
+
.enum(["default", "acceptEdits", "auto", "dontAsk", "bypassPermissions", "plan"])
|
|
2068
|
+
.optional()
|
|
2069
|
+
.describe("Grok permission mode"),
|
|
2070
|
+
effort: z
|
|
2071
|
+
.enum(["low", "medium", "high", "xhigh", "max"])
|
|
2072
|
+
.optional()
|
|
2073
|
+
.describe("Grok effort level"),
|
|
2074
|
+
reasoningEffort: z.string().optional().describe("Reasoning effort for reasoning models"),
|
|
2075
|
+
approvalStrategy: z
|
|
2076
|
+
.enum(["legacy", "mcp_managed"])
|
|
2077
|
+
.default("legacy")
|
|
2078
|
+
.describe("Approval strategy"),
|
|
2079
|
+
approvalPolicy: z
|
|
2080
|
+
.enum(["strict", "balanced", "permissive"])
|
|
2081
|
+
.optional()
|
|
2082
|
+
.describe("Approval policy override"),
|
|
2083
|
+
mcpServers: z
|
|
2084
|
+
.array(MCP_SERVER_ENUM)
|
|
2085
|
+
.default(["sqry"])
|
|
2086
|
+
.describe("MCP server names for approval tracking (Grok manages its own MCP config via `grok mcp`)"),
|
|
2087
|
+
allowedTools: z
|
|
2088
|
+
.array(z.string())
|
|
2089
|
+
.optional()
|
|
2090
|
+
.describe("Allowed built-in tools (passed as --tools comma list)"),
|
|
2091
|
+
disallowedTools: z
|
|
2092
|
+
.array(z.string())
|
|
2093
|
+
.optional()
|
|
2094
|
+
.describe("Disallowed built-in tools (passed as --disallowed-tools comma list)"),
|
|
2095
|
+
correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
|
|
2096
|
+
optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
|
|
2097
|
+
idleTimeoutMs: z
|
|
2098
|
+
.number()
|
|
2099
|
+
.int()
|
|
2100
|
+
.min(30_000)
|
|
2101
|
+
.max(3_600_000)
|
|
2102
|
+
.optional()
|
|
2103
|
+
.describe("Idle timeout in ms (min 30s, max 1h, omit=CLI default)"),
|
|
2104
|
+
forceRefresh: z
|
|
2105
|
+
.boolean()
|
|
2106
|
+
.default(false)
|
|
2107
|
+
.describe("Bypass dedup and force a fresh CLI run even if a recent identical request exists"),
|
|
2108
|
+
}, async ({ prompt, model, outputFormat, sessionId, resumeLatest, createNewSession, alwaysApprove, permissionMode, effort, reasoningEffort, approvalStrategy, approvalPolicy, mcpServers, allowedTools, disallowedTools, correlationId, optimizePrompt, idleTimeoutMs, forceRefresh, }) => {
|
|
2109
|
+
return handleGrokRequestAsync({ sessionManager, asyncJobManager, logger }, {
|
|
2110
|
+
prompt,
|
|
2111
|
+
model,
|
|
2112
|
+
outputFormat,
|
|
2113
|
+
sessionId,
|
|
2114
|
+
resumeLatest,
|
|
2115
|
+
createNewSession,
|
|
2116
|
+
alwaysApprove,
|
|
2117
|
+
permissionMode,
|
|
2118
|
+
effort,
|
|
2119
|
+
reasoningEffort,
|
|
2120
|
+
approvalStrategy,
|
|
2121
|
+
approvalPolicy,
|
|
2122
|
+
mcpServers,
|
|
2123
|
+
allowedTools,
|
|
2124
|
+
disallowedTools,
|
|
2125
|
+
correlationId,
|
|
2126
|
+
optimizePrompt,
|
|
2127
|
+
idleTimeoutMs,
|
|
2128
|
+
forceRefresh,
|
|
1563
2129
|
});
|
|
1564
2130
|
});
|
|
1565
2131
|
server.tool("llm_job_status", {
|
|
@@ -1723,6 +2289,63 @@ server.tool("list_models", {
|
|
|
1723
2289
|
const result = cli ? { [cli]: cliInfo[cli] } : cliInfo;
|
|
1724
2290
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1725
2291
|
});
|
|
2292
|
+
server.tool("cli_versions", {
|
|
2293
|
+
cli: z
|
|
2294
|
+
.preprocess(value => (value === "" || value === null ? undefined : value), z.enum(["claude", "codex", "gemini"]).optional())
|
|
2295
|
+
.describe("CLI filter (claude|codex|gemini)"),
|
|
2296
|
+
}, async ({ cli }) => {
|
|
2297
|
+
const versions = await getCliVersions(cli);
|
|
2298
|
+
return { content: [{ type: "text", text: JSON.stringify({ versions }, null, 2) }] };
|
|
2299
|
+
});
|
|
2300
|
+
server.tool("cli_upgrade", {
|
|
2301
|
+
cli: z.enum(["claude", "codex", "gemini"]).describe("CLI to upgrade"),
|
|
2302
|
+
target: z
|
|
2303
|
+
.string()
|
|
2304
|
+
.min(1)
|
|
2305
|
+
.default("latest")
|
|
2306
|
+
.describe("Package tag/version/target to install (default: latest)"),
|
|
2307
|
+
dryRun: z
|
|
2308
|
+
.boolean()
|
|
2309
|
+
.default(true)
|
|
2310
|
+
.describe("When true, return the upgrade plan without running it"),
|
|
2311
|
+
timeoutMs: z
|
|
2312
|
+
.number()
|
|
2313
|
+
.int()
|
|
2314
|
+
.min(30_000)
|
|
2315
|
+
.max(3_600_000)
|
|
2316
|
+
.optional()
|
|
2317
|
+
.describe("Upgrade timeout in ms when dryRun=false"),
|
|
2318
|
+
}, async ({ cli, target, dryRun, timeoutMs }) => {
|
|
2319
|
+
try {
|
|
2320
|
+
const result = await runCliUpgrade({ cli, target, dryRun, timeoutMs, logger });
|
|
2321
|
+
return {
|
|
2322
|
+
content: [
|
|
2323
|
+
{
|
|
2324
|
+
type: "text",
|
|
2325
|
+
text: JSON.stringify({
|
|
2326
|
+
success: true,
|
|
2327
|
+
...result,
|
|
2328
|
+
}, null, 2),
|
|
2329
|
+
},
|
|
2330
|
+
],
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
catch (error) {
|
|
2334
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2335
|
+
return {
|
|
2336
|
+
content: [
|
|
2337
|
+
{
|
|
2338
|
+
type: "text",
|
|
2339
|
+
text: JSON.stringify({
|
|
2340
|
+
success: false,
|
|
2341
|
+
error: message,
|
|
2342
|
+
}, null, 2),
|
|
2343
|
+
},
|
|
2344
|
+
],
|
|
2345
|
+
isError: true,
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
1726
2349
|
//──────────────────────────────────────────────────────────────────────────────
|
|
1727
2350
|
// Session Management Tools
|
|
1728
2351
|
//──────────────────────────────────────────────────────────────────────────────
|
|
@@ -1771,6 +2394,7 @@ server.tool("session_list", {
|
|
|
1771
2394
|
claude: await sessionManager.getActiveSession("claude"),
|
|
1772
2395
|
codex: await sessionManager.getActiveSession("codex"),
|
|
1773
2396
|
gemini: await sessionManager.getActiveSession("gemini"),
|
|
2397
|
+
grok: await sessionManager.getActiveSession("grok"),
|
|
1774
2398
|
};
|
|
1775
2399
|
const sessionList = sessions.map(s => ({
|
|
1776
2400
|
id: s.id,
|
|
@@ -1791,6 +2415,7 @@ server.tool("session_list", {
|
|
|
1791
2415
|
claude: activeSessions.claude?.id || null,
|
|
1792
2416
|
codex: activeSessions.codex?.id || null,
|
|
1793
2417
|
gemini: activeSessions.gemini?.id || null,
|
|
2418
|
+
grok: activeSessions.grok?.id || null,
|
|
1794
2419
|
},
|
|
1795
2420
|
}, null, 2),
|
|
1796
2421
|
},
|