@steipete/oracle 0.18.0 → 0.20.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/README.md +1 -1
- package/dist/bin/oracle-cli.js +76 -76
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/src/browser/actions/archiveConversation.js +31 -7
- package/dist/src/browser/actions/assistantResponse.js +43 -1
- package/dist/src/browser/actions/attachmentContext.js +239 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
- package/dist/src/browser/actions/attachmentEvidence.js +78 -0
- package/dist/src/browser/actions/attachmentProgress.js +41 -0
- package/dist/src/browser/actions/attachmentPrompt.js +130 -0
- package/dist/src/browser/actions/attachments.js +306 -355
- package/dist/src/browser/actions/deepResearch.js +170 -45
- package/dist/src/browser/actions/modelSelection.js +91 -4
- package/dist/src/browser/actions/navigation.js +1 -1
- package/dist/src/browser/actions/promptComposer.js +364 -186
- package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
- package/dist/src/browser/actions/thinkingStatus.js +2 -5
- package/dist/src/browser/actions/thinkingTime.js +156 -16
- package/dist/src/browser/actions/webSearch.js +96 -0
- package/dist/src/browser/attachRunning.js +46 -2
- package/dist/src/browser/attachmentValidation.js +53 -0
- package/dist/src/browser/cancellation.js +105 -0
- package/dist/src/browser/chatgptImages.js +2 -0
- package/dist/src/browser/chatgptThrottle.js +104 -0
- package/dist/src/browser/chromeLifecycle.js +272 -47
- package/dist/src/browser/config.js +14 -1
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +525 -274
- package/dist/src/browser/liveTabs.js +3 -4
- package/dist/src/browser/modelDisplay.js +9 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/profileState.js +114 -9
- package/dist/src/browser/projectSourcesRunner.js +83 -15
- package/dist/src/browser/prompt.js +272 -113
- package/dist/src/browser/providers/chatgptDomProvider.js +3 -0
- package/dist/src/browser/reattach.js +31 -4
- package/dist/src/browser/recoveryTarget.js +131 -0
- package/dist/src/browser/sessionRunner.js +73 -11
- package/dist/src/browser/tabLeaseRegistry.js +434 -38
- package/dist/src/browser/targetClaim.js +54 -0
- package/dist/src/browser/utils.js +20 -2
- package/dist/src/cli/browserConfig.js +36 -4
- package/dist/src/cli/browserDefaults.js +10 -0
- package/dist/src/cli/browserTabs.js +15 -29
- package/dist/src/cli/detachedSession.js +69 -0
- package/dist/src/cli/dryRun.js +58 -48
- package/dist/src/cli/harvestIntegrity.js +140 -0
- package/dist/src/cli/options.js +15 -0
- package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
- package/dist/src/cli/runOptions.js +19 -4
- package/dist/src/cli/serveBrowserConfig.js +24 -0
- package/dist/src/cli/sessionDisplay.js +13 -7
- package/dist/src/cli/sessionRunner.js +95 -12
- package/dist/src/config.js +1 -0
- package/dist/src/gemini-web/client.js +67 -21
- package/dist/src/gemini-web/executor.js +4 -0
- package/dist/src/gemini-web/http.js +15 -0
- package/dist/src/gemini-web/models.js +15 -11
- package/dist/src/mcp/server.js +11 -14
- package/dist/src/mcp/tools/chatgptImage.js +4 -4
- package/dist/src/mcp/tools/consult.js +86 -24
- package/dist/src/mcp/tools/projectSources.js +4 -6
- package/dist/src/mcp/tools/sessionResources.js +1 -1
- package/dist/src/mcp/tools/sessions.js +2 -2
- package/dist/src/mcp/tools/wait.js +182 -0
- package/dist/src/mcp/types.js +8 -1
- package/dist/src/oracle/config.js +14 -0
- package/dist/src/oracle/errors.js +11 -0
- package/dist/src/oracle/geminiModels.js +1 -0
- package/dist/src/oracle/providerRoutePlan.js +3 -2
- package/dist/src/oracle/run.js +27 -4
- package/dist/src/remote/client.js +73 -6
- package/dist/src/remote/health.js +6 -2
- package/dist/src/remote/runSlots.js +83 -0
- package/dist/src/remote/server.js +346 -71
- package/dist/src/sessionManager.js +36 -1
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +25 -23
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/bin/oracle.js +0 -683
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -400
- package/dist/docs-site/browser-mode.html +0 -588
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -436
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -368
- package/dist/docs-site/gemini.html +0 -379
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -595
- package/dist/docs-site/mcp.html +0 -368
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -387
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/profileSync.js +0 -141
- package/dist/src/browser/windowsCookies.js +0 -219
package/dist/src/mcp/server.js
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
import "dotenv/config";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { McpServer } from "@modelcontextprotocol/
|
|
6
|
-
import {
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
6
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
7
7
|
import { getCliVersion } from "../version.js";
|
|
8
8
|
import { registerChatGptImageTool } from "./tools/chatgptImage.js";
|
|
9
9
|
import { registerConsultTool } from "./tools/consult.js";
|
|
10
10
|
import { registerProjectSourcesTool } from "./tools/projectSources.js";
|
|
11
11
|
import { registerSessionsTool } from "./tools/sessions.js";
|
|
12
|
+
import { registerWaitTool } from "./tools/wait.js";
|
|
12
13
|
import { registerSessionResources } from "./tools/sessionResources.js";
|
|
13
|
-
export
|
|
14
|
+
export function createMcpServer() {
|
|
14
15
|
const server = new McpServer({
|
|
15
16
|
name: "oracle-mcp",
|
|
16
17
|
version: getCliVersion(),
|
|
@@ -23,19 +24,15 @@ export async function startMcpServer() {
|
|
|
23
24
|
registerChatGptImageTool(server);
|
|
24
25
|
registerProjectSourcesTool(server);
|
|
25
26
|
registerSessionsTool(server);
|
|
27
|
+
registerWaitTool(server);
|
|
26
28
|
registerSessionResources(server);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
resolve();
|
|
34
|
-
};
|
|
29
|
+
return server;
|
|
30
|
+
}
|
|
31
|
+
export async function startMcpServer() {
|
|
32
|
+
serveStdio(createMcpServer, {
|
|
33
|
+
legacy: "serve",
|
|
34
|
+
onerror: (error) => console.error("MCP transport error:", error),
|
|
35
35
|
});
|
|
36
|
-
// Keep the process alive until the client closes the transport.
|
|
37
|
-
await server.connect(transport);
|
|
38
|
-
await closed;
|
|
39
36
|
}
|
|
40
37
|
export function shouldStartMcpServerFromModule(moduleUrl = import.meta.url, argv1 = process.argv[1]) {
|
|
41
38
|
return argv1 ? moduleUrl === pathToFileURL(argv1).href : false;
|
|
@@ -101,9 +101,9 @@ export function registerChatGptImageTool(server) {
|
|
|
101
101
|
server.registerTool("chatgpt_image", {
|
|
102
102
|
title: "Generate an image with ChatGPT",
|
|
103
103
|
description: "Agent-friendly wrapper for ChatGPT browser image generation. It selects browser mode, enables the image-aware wait/download path, uploads reference files when provided, and returns saved image paths in structuredContent.images.",
|
|
104
|
-
inputSchema: chatGptImageInputShape,
|
|
105
|
-
outputSchema: chatGptImageOutputShape,
|
|
106
|
-
}, async (input) => {
|
|
104
|
+
inputSchema: z.object(chatGptImageInputShape),
|
|
105
|
+
outputSchema: z.object(chatGptImageOutputShape),
|
|
106
|
+
}, async (input, context) => {
|
|
107
107
|
const textContent = (text) => [{ type: "text", text }];
|
|
108
108
|
let parsed;
|
|
109
109
|
try {
|
|
@@ -116,7 +116,7 @@ export function registerChatGptImageTool(server) {
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
const consultInput = buildChatGptImageConsultInput(parsed);
|
|
119
|
-
const result = await runConsultTool(consultInput, {
|
|
119
|
+
const result = await runConsultTool(consultInput, { log: context.mcpReq.log });
|
|
120
120
|
if (result.isError || !result.structuredContent) {
|
|
121
121
|
return result;
|
|
122
122
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getCliVersion } from "../../version.js";
|
|
3
|
-
import { LoggingMessageNotificationParamsSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
3
|
import { ensureBrowserAvailable, mapConsultToRunOptions } from "../utils.js";
|
|
5
4
|
import { sessionStore } from "../../sessionStore.js";
|
|
6
5
|
import { resolveRemoteServiceConfig } from "../../remote/remoteServiceConfig.js";
|
|
7
6
|
import { createRemoteBrowserExecutor } from "../../remote/client.js";
|
|
8
|
-
async function readSessionLogTail(sessionId, maxBytes) {
|
|
7
|
+
export async function readSessionLogTail(sessionId, maxBytes) {
|
|
9
8
|
try {
|
|
10
9
|
const log = await sessionStore.readLog(sessionId);
|
|
11
10
|
if (log.length <= maxBytes) {
|
|
@@ -26,7 +25,9 @@ import { loadUserConfig } from "../../config.js";
|
|
|
26
25
|
import { resolveNotificationSettings } from "../../cli/notifier.js";
|
|
27
26
|
import { mapModelToBrowserLabel, resolveBrowserModelLabel, resolveDefaultBrowserThinkingTime, } from "../../cli/browserConfig.js";
|
|
28
27
|
import { normalizeThinkingTimeLevel } from "../../oracle/thinkingTime.js";
|
|
29
|
-
|
|
28
|
+
import { launchDetachedSession } from "../../cli/detachedSession.js";
|
|
29
|
+
import { buildSessionLifecycle } from "../../cli/sessionLifecycle.js";
|
|
30
|
+
// Shared fields are composed into explicit schemas when registering tools.
|
|
30
31
|
const consultInputShape = {
|
|
31
32
|
preset: z
|
|
32
33
|
.enum(CONSULT_PRESETS)
|
|
@@ -60,11 +61,11 @@ const consultInputShape = {
|
|
|
60
61
|
browserBundleFiles: z
|
|
61
62
|
.boolean()
|
|
62
63
|
.optional()
|
|
63
|
-
.describe("Browser-only: bundle
|
|
64
|
+
.describe("Browser-only: force one upload bundle; auto/zip includes all resolved files. Multiple text/source uploads already bundle by default (flattened text unless ZIP is selected)."),
|
|
64
65
|
browserBundleFormat: z
|
|
65
66
|
.enum(["auto", "text", "zip"])
|
|
66
67
|
.optional()
|
|
67
|
-
.describe('Browser-only: bundle upload format
|
|
68
|
+
.describe('Browser-only: bundle upload format. Defaults to "auto", which keeps flattened text for text-only uploads and uses ZIP when raw files are present; "zip" forces a byte-preserving archive.'),
|
|
68
69
|
browserThinkingTime: browserThinkingTimeRawSchema
|
|
69
70
|
.optional()
|
|
70
71
|
.describe("Browser-only: set ChatGPT thinking time when supported by the chosen model."),
|
|
@@ -73,9 +74,9 @@ const consultInputShape = {
|
|
|
73
74
|
.optional()
|
|
74
75
|
.describe("Browser-only: model picker strategy. Mirrors the CLI --browser-model-strategy flag."),
|
|
75
76
|
browserResearchMode: z
|
|
76
|
-
.enum(["deep"])
|
|
77
|
+
.enum(["search", "deep"])
|
|
77
78
|
.optional()
|
|
78
|
-
.describe("Browser-only:
|
|
79
|
+
.describe("Browser-only: explicitly select ChatGPT Web Search or Deep Research."),
|
|
79
80
|
browserArchive: z
|
|
80
81
|
.enum(["auto", "always", "never"])
|
|
81
82
|
.optional()
|
|
@@ -96,6 +97,10 @@ const consultInputShape = {
|
|
|
96
97
|
.string()
|
|
97
98
|
.optional()
|
|
98
99
|
.describe("Browser-only image output fallback path, mirroring the CLI --output option for image operations."),
|
|
100
|
+
waitForCompletion: z
|
|
101
|
+
.boolean()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("When false, start the Oracle run in a detached worker and return its sessionId immediately. Use the wait tool to block for completion without agent-side polling. Defaults to true for compatibility."),
|
|
99
104
|
dryRun: z
|
|
100
105
|
.boolean()
|
|
101
106
|
.optional()
|
|
@@ -109,7 +114,7 @@ const consultInputShape = {
|
|
|
109
114
|
.optional()
|
|
110
115
|
.describe("Optional human-friendly session id (used for later `oracle sessions` lookups)."),
|
|
111
116
|
};
|
|
112
|
-
const consultModelSummaryShape = z.object({
|
|
117
|
+
export const consultModelSummaryShape = z.object({
|
|
113
118
|
model: z.string(),
|
|
114
119
|
status: z.string(),
|
|
115
120
|
startedAt: z.string().optional(),
|
|
@@ -138,14 +143,14 @@ const consultModelSummaryShape = z.object({
|
|
|
138
143
|
.optional(),
|
|
139
144
|
logPath: z.string().optional(),
|
|
140
145
|
});
|
|
141
|
-
const consultArtifactSummaryShape = z.object({
|
|
146
|
+
export const consultArtifactSummaryShape = z.object({
|
|
142
147
|
kind: z.enum(["transcript", "deep-research-report", "image", "file"]),
|
|
143
148
|
path: z.string(),
|
|
144
149
|
label: z.string().optional(),
|
|
145
150
|
mimeType: z.string().optional(),
|
|
146
151
|
sizeBytes: z.number().optional(),
|
|
147
152
|
});
|
|
148
|
-
const consultImageSummaryShape = consultArtifactSummaryShape.extend({
|
|
153
|
+
export const consultImageSummaryShape = consultArtifactSummaryShape.extend({
|
|
149
154
|
kind: z.literal("image"),
|
|
150
155
|
alt: z.string().optional(),
|
|
151
156
|
width: z.number().optional(),
|
|
@@ -185,6 +190,7 @@ export const consultOutputShape = {
|
|
|
185
190
|
models: z.array(consultModelSummaryShape).optional(),
|
|
186
191
|
artifacts: z.array(consultArtifactSummaryShape).optional(),
|
|
187
192
|
images: z.array(consultImageSummaryShape).optional(),
|
|
193
|
+
detached: z.boolean().optional(),
|
|
188
194
|
};
|
|
189
195
|
export function summarizeModelRunsForConsult(runs) {
|
|
190
196
|
if (!runs || runs.length === 0) {
|
|
@@ -387,7 +393,7 @@ export function formatConsultDryRunResolved(details) {
|
|
|
387
393
|
}
|
|
388
394
|
return lines;
|
|
389
395
|
}
|
|
390
|
-
export async function runConsultTool(input, {
|
|
396
|
+
export async function runConsultTool(input, { log: requestLog, launchDetached = launchDetachedSession, }) {
|
|
391
397
|
const textContent = (text) => [{ type: "text", text }];
|
|
392
398
|
let parsedInput;
|
|
393
399
|
try {
|
|
@@ -399,7 +405,7 @@ export async function runConsultTool(input, { server }) {
|
|
|
399
405
|
content: textContent(error instanceof Error ? error.message : String(error)),
|
|
400
406
|
};
|
|
401
407
|
}
|
|
402
|
-
const { prompt, files, model, models, engine, search, browserModelLabel, browserAttachments, browserBundleFiles, browserBundleFormat, browserThinkingTime, browserModelStrategy, browserResearchMode, browserArchive, browserFollowUps, browserKeepBrowser, generateImage, outputPath, dryRun, slug, } = parsedInput;
|
|
408
|
+
const { prompt, files, model, models, engine, search, browserModelLabel, browserAttachments, browserBundleFiles, browserBundleFormat, browserThinkingTime, browserModelStrategy, browserResearchMode, browserArchive, browserFollowUps, browserKeepBrowser, generateImage, outputPath, waitForCompletion = true, dryRun, slug, } = parsedInput;
|
|
403
409
|
const { config: userConfig } = await loadUserConfig();
|
|
404
410
|
let runOptions;
|
|
405
411
|
let resolvedEngine;
|
|
@@ -428,12 +434,7 @@ export async function runConsultTool(input, { server }) {
|
|
|
428
434
|
};
|
|
429
435
|
}
|
|
430
436
|
const cwd = process.cwd();
|
|
431
|
-
const sendLog = (text, level = "info") =>
|
|
432
|
-
.sendLoggingMessage(LoggingMessageNotificationParamsSchema.parse({
|
|
433
|
-
level,
|
|
434
|
-
data: { text, bytes: Buffer.byteLength(text, "utf8") },
|
|
435
|
-
}))
|
|
436
|
-
.catch(() => { });
|
|
437
|
+
const sendLog = (text, level = "info") => requestLog(level, { text, bytes: Buffer.byteLength(text, "utf8") }).catch(() => { });
|
|
437
438
|
const resolvedRemote = resolveRemoteServiceConfig({ userConfig, env: process.env });
|
|
438
439
|
const imageOutputPath = runOptions.generateImage ?? runOptions.outputPath;
|
|
439
440
|
if (resolvedEngine === "browser" && resolvedRemote.host && imageOutputPath) {
|
|
@@ -490,6 +491,18 @@ export async function runConsultTool(input, { server }) {
|
|
|
490
491
|
},
|
|
491
492
|
};
|
|
492
493
|
}
|
|
494
|
+
if (!waitForCompletion && resolvedEngine === "browser" && resolvedRemote.host) {
|
|
495
|
+
return {
|
|
496
|
+
isError: true,
|
|
497
|
+
content: textContent("Detached MCP consults are not supported with a remote browser service yet. Keep waitForCompletion:true for this run."),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
if (!waitForCompletion && process.env.ORACLE_NO_DETACH === "1") {
|
|
501
|
+
return {
|
|
502
|
+
isError: true,
|
|
503
|
+
content: textContent("Detached MCP consults are disabled by ORACLE_NO_DETACH=1. Remove it or keep waitForCompletion:true."),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
493
506
|
const browserGuard = ensureBrowserAvailable(resolvedEngine, {
|
|
494
507
|
remoteHost: resolvedRemote.host,
|
|
495
508
|
});
|
|
@@ -525,8 +538,58 @@ export async function runConsultTool(input, { server }) {
|
|
|
525
538
|
mode: resolvedEngine,
|
|
526
539
|
slug,
|
|
527
540
|
browserConfig,
|
|
528
|
-
waitPreference:
|
|
541
|
+
waitPreference: waitForCompletion,
|
|
529
542
|
}, cwd, notifications);
|
|
543
|
+
if (!waitForCompletion) {
|
|
544
|
+
try {
|
|
545
|
+
await launchDetached({
|
|
546
|
+
sessionId: sessionMeta.id,
|
|
547
|
+
prepare: async (workerPid) => {
|
|
548
|
+
const lifecycle = buildSessionLifecycle({
|
|
549
|
+
engine: resolvedEngine,
|
|
550
|
+
detached: true,
|
|
551
|
+
workerPid,
|
|
552
|
+
reattachCommand: `oracle session ${sessionMeta.id}`,
|
|
553
|
+
});
|
|
554
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
555
|
+
status: "running",
|
|
556
|
+
startedAt: new Date().toISOString(),
|
|
557
|
+
lifecycle,
|
|
558
|
+
});
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
const started = (await sessionStore.readSession(sessionMeta.id)) ?? sessionMeta;
|
|
562
|
+
const summary = `Session ${sessionMeta.id} (${started.status}; detached)`;
|
|
563
|
+
return {
|
|
564
|
+
content: textContent(`${summary}\nUse the wait tool with id=${JSON.stringify(sessionMeta.id)} to wait without polling.`),
|
|
565
|
+
structuredContent: {
|
|
566
|
+
sessionId: sessionMeta.id,
|
|
567
|
+
status: started.status,
|
|
568
|
+
output: "",
|
|
569
|
+
models: summarizeModelRunsForConsult(started.models),
|
|
570
|
+
artifacts: summarizeArtifactsForConsult(started.artifacts),
|
|
571
|
+
images: summarizeImageArtifactsForConsult(started.artifacts),
|
|
572
|
+
detached: true,
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
578
|
+
await sessionStore
|
|
579
|
+
.updateSession(sessionMeta.id, {
|
|
580
|
+
status: "error",
|
|
581
|
+
completedAt: new Date().toISOString(),
|
|
582
|
+
errorMessage: message,
|
|
583
|
+
response: { status: "error" },
|
|
584
|
+
error: { category: "internal", message },
|
|
585
|
+
})
|
|
586
|
+
.catch(() => undefined);
|
|
587
|
+
return {
|
|
588
|
+
isError: true,
|
|
589
|
+
content: textContent(`Unable to start detached session ${sessionMeta.id}: ${message}`),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
}
|
|
530
593
|
const logWriter = sessionStore.createLogWriter(sessionMeta.id);
|
|
531
594
|
// Stream logs to both the session log and MCP logging notifications, but avoid buffering in memory
|
|
532
595
|
const log = (line) => {
|
|
@@ -594,9 +657,8 @@ export async function runConsultTool(input, { server }) {
|
|
|
594
657
|
export function registerConsultTool(server) {
|
|
595
658
|
server.registerTool("consult", {
|
|
596
659
|
title: "Run an oracle session",
|
|
597
|
-
description: 'Run an Oracle session (API or ChatGPT browser automation). Use `files` to attach project context. If `engine` is omitted, Oracle follows CLI defaults: config/ORACLE_ENGINE first, then API when OPENAI_API_KEY is set, otherwise browser. Browser GPT-5.5 Pro consults can take many minutes; use `dryRun:true` first when configuring an agent and inspect `sessions`/`oracle status` before retrying. Browser manual-login uses a private Oracle Chrome profile separate from the user\'s normal Chrome; dry-run output includes first-time setup guidance when that path is active. For browser-based image/file uploads, set `browserAttachments:"always"`. For ChatGPT image generation, set `generateImage` to enable the same image wait/download path as CLI --generate-image and read returned paths from `images`. Browser consults can include `browserFollowUps` for a multi-turn ChatGPT review in one conversation. Sessions are stored under `ORACLE_HOME_DIR` (shared with the CLI).',
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}, async (input) => runConsultTool(input, { server: server.server }));
|
|
660
|
+
description: 'Run an Oracle session (API or ChatGPT browser automation). Use `files` to attach project context. If `engine` is omitted, Oracle follows CLI defaults: config/ORACLE_ENGINE first, then API when OPENAI_API_KEY is set, otherwise browser. Browser GPT-5.5 Pro consults can take many minutes; set `waitForCompletion:false` to return a durable sessionId immediately, then use `wait` to block without agent-side polling. Use `dryRun:true` first when configuring an agent and inspect `sessions`/`oracle status` before retrying. Browser manual-login uses a private Oracle Chrome profile separate from the user\'s normal Chrome; dry-run output includes first-time setup guidance when that path is active. For browser-based image/file uploads, set `browserAttachments:"always"`. For ChatGPT image generation, set `generateImage` to enable the same image wait/download path as CLI --generate-image and read returned paths from `images`. Browser consults can include `browserFollowUps` for a multi-turn ChatGPT review in one conversation. Sessions are stored under `ORACLE_HOME_DIR` (shared with the CLI).',
|
|
661
|
+
inputSchema: z.object(consultInputShape),
|
|
662
|
+
outputSchema: z.object(consultOutputShape),
|
|
663
|
+
}, async (input, context) => runConsultTool(input, { log: context.mcpReq.log }));
|
|
602
664
|
}
|
|
@@ -56,9 +56,9 @@ export function registerProjectSourcesTool(server) {
|
|
|
56
56
|
server.registerTool("project_sources", {
|
|
57
57
|
title: "Manage ChatGPT Project Sources",
|
|
58
58
|
description: "List or append files to a ChatGPT Project's persistent Sources tab. This is useful for Developer Mode workflows where chats do not share memory, but explicit project sources provide shared context. Destructive delete/replace/sync operations are intentionally not included in v1.",
|
|
59
|
-
inputSchema: projectSourcesInputShape,
|
|
60
|
-
outputSchema: projectSourcesOutputShape,
|
|
61
|
-
}, async (input) => {
|
|
59
|
+
inputSchema: z.object(projectSourcesInputShape),
|
|
60
|
+
outputSchema: z.object(projectSourcesOutputShape),
|
|
61
|
+
}, async (input, context) => {
|
|
62
62
|
const textContent = (text) => [{ type: "text", text }];
|
|
63
63
|
let parsed;
|
|
64
64
|
try {
|
|
@@ -107,9 +107,7 @@ export function registerProjectSourcesTool(server) {
|
|
|
107
107
|
dryRun: parsed.dryRun,
|
|
108
108
|
config: browserConfig,
|
|
109
109
|
log: (message) => {
|
|
110
|
-
|
|
111
|
-
.sendLoggingMessage({ level: "info", data: { text: message } })
|
|
112
|
-
.catch(() => undefined);
|
|
110
|
+
context.mcpReq.log("info", { text: message }).catch(() => undefined);
|
|
113
111
|
},
|
|
114
112
|
});
|
|
115
113
|
const output = result.status === "dry-run"
|
|
@@ -44,8 +44,8 @@ export function registerSessionsTool(server) {
|
|
|
44
44
|
server.registerTool("sessions", {
|
|
45
45
|
title: "List or fetch oracle sessions",
|
|
46
46
|
description: "Inspect Oracle session history stored under `ORACLE_HOME_DIR` (shared with the CLI). List recent sessions or fetch one by id/slug (optionally including metadata + request + log).",
|
|
47
|
-
inputSchema: sessionsInputShape,
|
|
48
|
-
outputSchema: sessionsOutputShape,
|
|
47
|
+
inputSchema: z.object(sessionsInputShape),
|
|
48
|
+
outputSchema: z.object(sessionsOutputShape),
|
|
49
49
|
}, async (input) => {
|
|
50
50
|
const textContent = (text) => [{ type: "text", text }];
|
|
51
51
|
const { id, hours = 24, limit = 100, includeAll = false, detail = false, } = sessionsInputSchema.parse(input);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { realpathSync, watch } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { sessionStore } from "../../sessionStore.js";
|
|
4
|
+
import { waitInputSchema } from "../types.js";
|
|
5
|
+
import { consultArtifactSummaryShape, consultImageSummaryShape, consultModelSummaryShape, readSessionLogTail, summarizeArtifactsForConsult, summarizeImageArtifactsForConsult, summarizeModelRunsForConsult, } from "./consult.js";
|
|
6
|
+
const TERMINAL_SESSION_STATUSES = new Set(["completed", "partial", "error", "cancelled"]);
|
|
7
|
+
const DEFAULT_FALLBACK_INTERVAL_MS = 1_000;
|
|
8
|
+
const waitInputShape = {
|
|
9
|
+
id: z.string().min(1, "Session id is required.").describe("Oracle session id or slug."),
|
|
10
|
+
timeoutMs: z
|
|
11
|
+
.number()
|
|
12
|
+
.int()
|
|
13
|
+
.nonnegative()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("How long to wait for a terminal session state. Omit to wait indefinitely; 0 returns an immediate snapshot. A wait timeout never cancels the Oracle session."),
|
|
16
|
+
};
|
|
17
|
+
const waitOutputShape = {
|
|
18
|
+
sessionId: z.string(),
|
|
19
|
+
status: z.string(),
|
|
20
|
+
waitStatus: z.enum(["terminal", "timed_out"]),
|
|
21
|
+
timedOut: z.boolean(),
|
|
22
|
+
cancelled: z.boolean(),
|
|
23
|
+
output: z.string(),
|
|
24
|
+
models: z.array(consultModelSummaryShape).optional(),
|
|
25
|
+
artifacts: z.array(consultArtifactSummaryShape).optional(),
|
|
26
|
+
images: z.array(consultImageSummaryShape).optional(),
|
|
27
|
+
};
|
|
28
|
+
function abortReason(signal) {
|
|
29
|
+
return signal.reason instanceof Error
|
|
30
|
+
? signal.reason
|
|
31
|
+
: new DOMException("Oracle session wait was cancelled.", "AbortError");
|
|
32
|
+
}
|
|
33
|
+
function throwIfAborted(signal) {
|
|
34
|
+
if (signal?.aborted) {
|
|
35
|
+
throw abortReason(signal);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function isTerminalSessionStatus(status) {
|
|
39
|
+
return TERMINAL_SESSION_STATUSES.has(status);
|
|
40
|
+
}
|
|
41
|
+
export function createSessionChangeSource(directory) {
|
|
42
|
+
let watcher;
|
|
43
|
+
let notified = false;
|
|
44
|
+
let pendingWake;
|
|
45
|
+
const onWake = () => {
|
|
46
|
+
if (!pendingWake) {
|
|
47
|
+
notified = true;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const wake = pendingWake;
|
|
51
|
+
pendingWake = undefined;
|
|
52
|
+
wake();
|
|
53
|
+
};
|
|
54
|
+
const onError = () => {
|
|
55
|
+
const failedWatcher = watcher;
|
|
56
|
+
watcher = undefined;
|
|
57
|
+
failedWatcher?.off("change", onWake);
|
|
58
|
+
failedWatcher?.off("error", onError);
|
|
59
|
+
failedWatcher?.close();
|
|
60
|
+
onWake();
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
// Windows short-path aliases can trigger a native libuv prefix assertion.
|
|
64
|
+
watcher = watch(realpathSync.native(directory));
|
|
65
|
+
watcher.on("change", onWake);
|
|
66
|
+
watcher.on("error", onError);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// A timer fallback below keeps waits correct when filesystem notifications
|
|
70
|
+
// are unavailable or the session directory is on an unsupported volume.
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
wait(delayMs, signal) {
|
|
74
|
+
throwIfAborted(signal);
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
let settled = false;
|
|
77
|
+
const finish = (error) => {
|
|
78
|
+
if (settled)
|
|
79
|
+
return;
|
|
80
|
+
settled = true;
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
if (pendingWake === wake)
|
|
83
|
+
pendingWake = undefined;
|
|
84
|
+
signal?.removeEventListener("abort", onAbort);
|
|
85
|
+
if (error)
|
|
86
|
+
reject(error);
|
|
87
|
+
else
|
|
88
|
+
resolve();
|
|
89
|
+
};
|
|
90
|
+
const wake = () => finish();
|
|
91
|
+
const onAbort = () => finish(abortReason(signal));
|
|
92
|
+
const timer = setTimeout(wake, delayMs);
|
|
93
|
+
pendingWake = wake;
|
|
94
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
95
|
+
if (signal?.aborted) {
|
|
96
|
+
onAbort();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (notified) {
|
|
100
|
+
notified = false;
|
|
101
|
+
queueMicrotask(wake);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
close() {
|
|
106
|
+
watcher?.off("change", onWake);
|
|
107
|
+
watcher?.off("error", onError);
|
|
108
|
+
watcher?.close();
|
|
109
|
+
watcher = undefined;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const defaultWaitDeps = {
|
|
114
|
+
readSession: (id) => sessionStore.readSession(id),
|
|
115
|
+
getSessionDir: async (id) => (await sessionStore.getPaths(id)).dir,
|
|
116
|
+
createChangeSource: createSessionChangeSource,
|
|
117
|
+
now: Date.now,
|
|
118
|
+
};
|
|
119
|
+
export async function waitForSessionTerminal({ id, timeoutMs, signal, fallbackIntervalMs = DEFAULT_FALLBACK_INTERVAL_MS, }, deps = defaultWaitDeps) {
|
|
120
|
+
throwIfAborted(signal);
|
|
121
|
+
const deadline = timeoutMs === undefined ? undefined : deps.now() + timeoutMs;
|
|
122
|
+
let metadata = await deps.readSession(id);
|
|
123
|
+
if (!metadata) {
|
|
124
|
+
throw new Error(`Session "${id}" not found.`);
|
|
125
|
+
}
|
|
126
|
+
if (isTerminalSessionStatus(metadata.status)) {
|
|
127
|
+
return { metadata, waitStatus: "terminal", timedOut: false };
|
|
128
|
+
}
|
|
129
|
+
if (timeoutMs === 0) {
|
|
130
|
+
return { metadata, waitStatus: "timed_out", timedOut: true };
|
|
131
|
+
}
|
|
132
|
+
const directory = await deps.getSessionDir(id);
|
|
133
|
+
const changes = deps.createChangeSource(directory);
|
|
134
|
+
try {
|
|
135
|
+
// Close the read/watch race: the session may have completed while the
|
|
136
|
+
// filesystem watcher was being installed.
|
|
137
|
+
metadata = (await deps.readSession(id)) ?? metadata;
|
|
138
|
+
while (!isTerminalSessionStatus(metadata.status)) {
|
|
139
|
+
throwIfAborted(signal);
|
|
140
|
+
const remaining = deadline === undefined ? undefined : deadline - deps.now();
|
|
141
|
+
if (remaining !== undefined && remaining <= 0) {
|
|
142
|
+
return { metadata, waitStatus: "timed_out", timedOut: true };
|
|
143
|
+
}
|
|
144
|
+
const delayMs = Math.max(1, Math.min(fallbackIntervalMs, remaining ?? fallbackIntervalMs));
|
|
145
|
+
await changes.wait(delayMs, signal);
|
|
146
|
+
metadata = (await deps.readSession(id)) ?? metadata;
|
|
147
|
+
}
|
|
148
|
+
return { metadata, waitStatus: "terminal", timedOut: false };
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
changes.close();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export async function runWaitTool(input, extra) {
|
|
155
|
+
const { id, timeoutMs } = waitInputSchema.parse(input);
|
|
156
|
+
const result = await waitForSessionTerminal({ id, timeoutMs, signal: extra?.signal });
|
|
157
|
+
const { metadata } = result;
|
|
158
|
+
const logTail = (await readSessionLogTail(metadata.id, 4_000)) ?? "";
|
|
159
|
+
const summary = `Session ${metadata.id} (${metadata.status}; wait=${result.waitStatus})`;
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: [summary, logTail || "(log empty)"].join("\n") }],
|
|
162
|
+
structuredContent: {
|
|
163
|
+
sessionId: metadata.id,
|
|
164
|
+
status: metadata.status,
|
|
165
|
+
waitStatus: result.waitStatus,
|
|
166
|
+
timedOut: result.timedOut,
|
|
167
|
+
cancelled: metadata.status === "cancelled",
|
|
168
|
+
output: logTail,
|
|
169
|
+
models: summarizeModelRunsForConsult(metadata.models),
|
|
170
|
+
artifacts: summarizeArtifactsForConsult(metadata.artifacts),
|
|
171
|
+
images: summarizeImageArtifactsForConsult(metadata.artifacts),
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export function registerWaitTool(server) {
|
|
176
|
+
server.registerTool("wait", {
|
|
177
|
+
title: "Wait for an oracle session",
|
|
178
|
+
description: "Wait for an existing Oracle session to reach a terminal state without agent-side polling. Omit timeoutMs to wait indefinitely, or set a bounded caller wait. Wait timeout or request cancellation never cancels the Oracle session; call wait again with the same id to continue waiting.",
|
|
179
|
+
inputSchema: z.object(waitInputShape),
|
|
180
|
+
outputSchema: z.object(waitOutputShape),
|
|
181
|
+
}, async (input, context) => runWaitTool(input, { signal: context.mcpReq.signal }));
|
|
182
|
+
}
|
package/dist/src/mcp/types.js
CHANGED
|
@@ -23,12 +23,13 @@ export const consultInputSchema = z
|
|
|
23
23
|
browserBundleFormat: z.enum(["auto", "text", "zip"]).optional(),
|
|
24
24
|
browserThinkingTime: browserThinkingTimeInputSchema.optional(),
|
|
25
25
|
browserModelStrategy: z.enum(["select", "current", "ignore"]).optional(),
|
|
26
|
-
browserResearchMode: z.enum(["deep"]).optional(),
|
|
26
|
+
browserResearchMode: z.enum(["search", "deep"]).optional(),
|
|
27
27
|
browserArchive: z.enum(["auto", "always", "never"]).optional(),
|
|
28
28
|
browserFollowUps: z.array(z.string()).optional(),
|
|
29
29
|
browserKeepBrowser: z.boolean().optional(),
|
|
30
30
|
generateImage: z.string().optional(),
|
|
31
31
|
outputPath: z.string().optional(),
|
|
32
|
+
waitForCompletion: z.boolean().optional(),
|
|
32
33
|
dryRun: z.boolean().optional(),
|
|
33
34
|
search: z.boolean().optional(),
|
|
34
35
|
slug: z.string().optional(),
|
|
@@ -41,3 +42,9 @@ export const sessionsInputSchema = z.object({
|
|
|
41
42
|
includeAll: z.boolean().optional(),
|
|
42
43
|
detail: z.boolean().optional(),
|
|
43
44
|
});
|
|
45
|
+
export const waitInputSchema = z
|
|
46
|
+
.object({
|
|
47
|
+
id: z.string().min(1, "Session id is required."),
|
|
48
|
+
timeoutMs: z.number().int().nonnegative().optional(),
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
@@ -30,6 +30,20 @@ const countTokensAnthropic = (input) => {
|
|
|
30
30
|
// supported limit at the base-rate boundary until cost estimation supports tiers.
|
|
31
31
|
const GPT_5_6_BASE_RATE_INPUT_LIMIT = 272_000;
|
|
32
32
|
export const MODEL_CONFIGS = {
|
|
33
|
+
"gpt-6-astra": {
|
|
34
|
+
model: "gpt-6-astra",
|
|
35
|
+
provider: "openai",
|
|
36
|
+
tokenizer: countTokensGpt5,
|
|
37
|
+
// Conservative base-rate budget, not Astra's maximum context window.
|
|
38
|
+
// https://developers.openai.com/api/docs/models/gpt-6-astra
|
|
39
|
+
inputLimit: 272_000,
|
|
40
|
+
pricing: {
|
|
41
|
+
inputPerToken: 10 / 1_000_000,
|
|
42
|
+
outputPerToken: 50 / 1_000_000,
|
|
43
|
+
},
|
|
44
|
+
reasoning: { effort: "xhigh" },
|
|
45
|
+
searchToolType: "web_search",
|
|
46
|
+
},
|
|
33
47
|
"gpt-5.6": {
|
|
34
48
|
model: "gpt-5.6",
|
|
35
49
|
provider: "openai",
|
|
@@ -26,6 +26,17 @@ export class BrowserAutomationError extends OracleUserError {
|
|
|
26
26
|
this.name = "BrowserAutomationError";
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The caller went away. Distinct from a failure: nothing was wrong with the run,
|
|
31
|
+
* it simply stopped being wanted, and a caller that reads this should not retry
|
|
32
|
+
* or report a problem.
|
|
33
|
+
*/
|
|
34
|
+
export class BrowserRunCancelledError extends OracleUserError {
|
|
35
|
+
constructor(message = "Browser run cancelled: the caller disconnected.") {
|
|
36
|
+
super("browser-automation", message, { stage: "run-cancelled" });
|
|
37
|
+
this.name = "BrowserRunCancelledError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
29
40
|
export class PromptValidationError extends OracleUserError {
|
|
30
41
|
constructor(message, details, cause) {
|
|
31
42
|
super("prompt-validation", message, details, cause);
|
|
@@ -75,7 +75,8 @@ function buildResolvedProviderRoute(input) {
|
|
|
75
75
|
});
|
|
76
76
|
const provider = state.provider;
|
|
77
77
|
const isAzureOpenAI = state.isAzureOpenAI;
|
|
78
|
-
|
|
78
|
+
// Azure owns the endpoint, including metadata lookups before request dispatch.
|
|
79
|
+
let baseUrl = isAzureOpenAI ? undefined : input.baseUrl?.trim();
|
|
79
80
|
const providerQualifiedOpenRouterCandidate = !isAzureOpenAI && providerMode !== "openai" && input.model.includes("/");
|
|
80
81
|
if (baseUrl &&
|
|
81
82
|
providerQualifiedOpenRouterCandidate &&
|
|
@@ -83,7 +84,7 @@ function buildResolvedProviderRoute(input) {
|
|
|
83
84
|
!isCustomBaseUrl(baseUrl)) {
|
|
84
85
|
baseUrl = undefined;
|
|
85
86
|
}
|
|
86
|
-
if (!baseUrl) {
|
|
87
|
+
if (!baseUrl && !isAzureOpenAI) {
|
|
87
88
|
let envBaseUrl;
|
|
88
89
|
if (input.model.startsWith("grok")) {
|
|
89
90
|
envBaseUrl = env.XAI_BASE_URL?.trim() || "https://api.x.ai/v1";
|