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/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 bookkeeping only.
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 job = asyncJobManager.startJob(cli, args, corrId, undefined, idleTimeoutMs, outputFormat);
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", effectivePrompt);
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
- }, async ({ prompt, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, }) => {
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.string().optional().describe("Session ID (Codex manages internally)"),
1146
- createNewSession: z.boolean().default(false).describe("Force new session"),
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
- }, async ({ prompt, model, fullAuto, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, createNewSession, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, }) => {
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
- }, async ({ prompt, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, optimizeResponse, idleTimeoutMs, }) => {
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
- }, async ({ prompt, model, outputFormat, sessionId, continueSession, createNewSession, allowedTools, disallowedTools, dangerouslySkipPermissions, approvalStrategy, approvalPolicy, mcpServers, strictMcpConfig, correlationId, optimizePrompt, idleTimeoutMs, }) => {
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.string().optional().describe("Session ID (Codex manages internally)"),
1478
- createNewSession: z.boolean().default(false).describe("Force new session"),
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
- }, async ({ prompt, model, fullAuto, dangerouslyBypassApprovalsAndSandbox, approvalStrategy, approvalPolicy, mcpServers, sessionId, createNewSession, correlationId, optimizePrompt, idleTimeoutMs, }) => {
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
- }, async ({ prompt, model, sessionId, resumeLatest, createNewSession, approvalMode, approvalStrategy, approvalPolicy, mcpServers, allowedTools, includeDirs, correlationId, optimizePrompt, idleTimeoutMs, }) => {
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
  },