@steipete/oracle 0.13.0 → 0.14.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 +20 -9
- package/dist/bin/oracle-cli.js +63 -63
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +1 -0
- package/dist/docs-site/RELEASING.html +410 -0
- package/dist/docs-site/agents.html +374 -0
- package/dist/docs-site/anthropic.html +368 -0
- package/dist/docs-site/bridge.html +400 -0
- package/dist/docs-site/browser-mode.html +593 -0
- package/dist/docs-site/chromium-forks.html +347 -0
- package/dist/docs-site/cli-reference.html +346 -0
- package/dist/docs-site/configuration.html +452 -0
- package/dist/docs-site/favicon.svg +14 -0
- package/dist/docs-site/followup.html +375 -0
- package/dist/docs-site/gemini.html +383 -0
- package/dist/docs-site/grok.html +325 -0
- package/dist/docs-site/index.html +360 -0
- package/dist/docs-site/install.html +335 -0
- package/dist/docs-site/linux.html +321 -0
- package/dist/docs-site/llms.txt +43 -0
- package/dist/docs-site/manual-tests.html +596 -0
- package/dist/docs-site/mcp.html +391 -0
- package/dist/docs-site/multimodel.html +364 -0
- package/dist/docs-site/mythical-pro-agents.html +360 -0
- package/dist/docs-site/notifier.html +338 -0
- package/dist/docs-site/openai-endpoints.html +387 -0
- package/dist/docs-site/openrouter.html +344 -0
- package/dist/docs-site/quickstart.html +369 -0
- package/dist/docs-site/refactor/ux.html +532 -0
- package/dist/docs-site/sessions.html +388 -0
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +79 -0
- package/dist/docs-site/spec.html +363 -0
- package/dist/docs-site/testing.html +320 -0
- package/dist/docs-site/tui-debug.html +326 -0
- package/dist/docs-site/windows-work.html +323 -0
- package/dist/docs-site/windows.html +320 -0
- package/dist/src/browser/actions/assistantResponse.js +91 -9
- package/dist/src/browser/actions/deepResearch.js +180 -52
- package/dist/src/browser/actions/modelSelection.js +13 -11
- package/dist/src/browser/actions/navigation.js +294 -65
- package/dist/src/browser/actions/promptComposer.js +50 -4
- package/dist/src/browser/actions/thinkingTime.js +308 -89
- package/dist/src/browser/artifacts.js +19 -0
- package/dist/src/browser/chatgptFiles.js +797 -0
- package/dist/src/browser/chatgptImages.js +130 -3
- package/dist/src/browser/chromeLifecycle.js +4 -0
- package/dist/src/browser/config.js +2 -0
- package/dist/src/browser/index.js +347 -39
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/policies.js +2 -6
- package/dist/src/browser/projectSourcesRunner.js +14 -2
- package/dist/src/browser/prompt.js +11 -15
- package/dist/src/browser/reattach.js +6 -2
- package/dist/src/browser/reattachability.js +22 -10
- package/dist/src/browser/recoverConversation.js +73 -0
- package/dist/src/browser/sessionRunner.js +4 -1
- package/dist/src/cli/browserConfig.js +4 -1
- package/dist/src/cli/browserTabs.js +129 -54
- package/dist/src/cli/followup.js +72 -0
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +16 -0
- package/dist/src/cli/reattachGuidance.js +8 -0
- package/dist/src/cli/runOptions.js +2 -12
- package/dist/src/cli/sessionCommand.js +4 -0
- package/dist/src/cli/sessionDisplay.js +17 -3
- package/dist/src/cli/sessionLineage.js +7 -4
- package/dist/src/cli/sessionRunner.js +16 -1
- package/dist/src/gemini-web/client.js +5 -10
- package/dist/src/gemini-web/executor.js +1 -24
- package/dist/src/gemini-web/models.js +83 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/chatgptImage.js +127 -0
- package/dist/src/mcp/tools/consult.js +261 -167
- package/dist/src/mcp/types.js +2 -0
- package/dist/src/mcp/utils.js +87 -1
- package/dist/src/oracle/config.js +27 -1
- package/dist/src/oracle/files.js +4 -6
- package/dist/src/oracle/geminiModels.js +2 -0
- package/dist/src/oracle/markdown.js +36 -3
- package/dist/src/oracle/modelResolver.js +21 -13
- package/dist/src/oracle/promptAssembly.js +2 -4
- package/dist/src/oracle/request.js +3 -5
- package/dist/src/oracle/tokenStats.js +5 -1
- package/dist/src/oracle.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- 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 +19 -28
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -28,16 +28,11 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
28
28
|
const isCodex = apiModel.startsWith("gpt-5.1-codex");
|
|
29
29
|
const isClaude = apiModel.startsWith("claude");
|
|
30
30
|
const isGrok = apiModel.startsWith("grok");
|
|
31
|
-
const isGeminiApiOnly = apiModel === "gemini-3.1-pro";
|
|
32
31
|
const engineWasBrowser = resolvedEngine === "browser";
|
|
33
32
|
const allModels = normalizedRequestedModels.length > 0
|
|
34
33
|
? Array.from(new Set(normalizedRequestedModels.map((entry) => resolveApiModel(entry))))
|
|
35
34
|
: [apiModel];
|
|
36
35
|
const browserCompatibilityModels = normalizedRequestedModels.length > 0 ? allModels : [browserModel];
|
|
37
|
-
const includesGeminiApiOnly = allModels.some((m) => m === "gemini-3.1-pro");
|
|
38
|
-
if (browserEngineRequested && includesGeminiApiOnly) {
|
|
39
|
-
throw new PromptValidationError("gemini-3.1-pro is API-only today. Use --engine api or switch to gemini-3-pro for Gemini web.", { engine: "browser", models: allModels });
|
|
40
|
-
}
|
|
41
36
|
const isBrowserCompatible = (m) => m.startsWith("gpt-") || m.startsWith("gemini");
|
|
42
37
|
const hasNonBrowserCompatibleTarget = browserEngineRequested && browserCompatibilityModels.some((m) => !isBrowserCompatible(m));
|
|
43
38
|
if (hasNonBrowserCompatibleTarget) {
|
|
@@ -47,13 +42,8 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
47
42
|
const azureAutoApi = Boolean(azure?.endpoint) &&
|
|
48
43
|
!browserEngineRequested &&
|
|
49
44
|
allModels.some(isAzureOpenAICandidateModel);
|
|
50
|
-
const engineCoercedToApi = engineWasBrowser && (isCodex || isClaude || isGrok ||
|
|
51
|
-
const fixedEngine = isCodex ||
|
|
52
|
-
isClaude ||
|
|
53
|
-
isGrok ||
|
|
54
|
-
isGeminiApiOnly ||
|
|
55
|
-
azureAutoApi ||
|
|
56
|
-
normalizedRequestedModels.length > 0
|
|
45
|
+
const engineCoercedToApi = engineWasBrowser && (isCodex || isClaude || isGrok || azureAutoApi);
|
|
46
|
+
const fixedEngine = isCodex || isClaude || isGrok || azureAutoApi || normalizedRequestedModels.length > 0
|
|
57
47
|
? "api"
|
|
58
48
|
: resolvedEngine;
|
|
59
49
|
// Browser runs use ChatGPT picker labels/aliases; API runs must keep API model ids intact.
|
|
@@ -109,16 +109,20 @@ export async function handleSessionCommand(sessionId, command, deps = defaultDep
|
|
|
109
109
|
process.exitCode = 1;
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
|
+
// Commander sets `recover: false` when --no-recover is passed; default is `true`.
|
|
113
|
+
const recoverIfMissing = sessionOptions.recover !== false;
|
|
112
114
|
if (harvestRequested) {
|
|
113
115
|
await deps.harvestSessionBrowserOutput(sessionId, {
|
|
114
116
|
writeOutputPath,
|
|
115
117
|
browserTabRef,
|
|
118
|
+
recoverIfMissing,
|
|
116
119
|
});
|
|
117
120
|
return;
|
|
118
121
|
}
|
|
119
122
|
await deps.liveTailSessionBrowserOutput(sessionId, {
|
|
120
123
|
writeOutputPath,
|
|
121
124
|
browserTabRef,
|
|
125
|
+
recoverIfMissing,
|
|
122
126
|
});
|
|
123
127
|
return;
|
|
124
128
|
}
|
|
@@ -628,6 +628,14 @@ function matchesModel(entry, filter) {
|
|
|
628
628
|
(entry.model ? [entry.model.toLowerCase()] : []);
|
|
629
629
|
return models.includes(normalized);
|
|
630
630
|
}
|
|
631
|
+
function formatLineageParentLabel(lineage) {
|
|
632
|
+
if (!lineage?.parentSessionId) {
|
|
633
|
+
return undefined;
|
|
634
|
+
}
|
|
635
|
+
return lineage.parentResponseId
|
|
636
|
+
? `${lineage.parentSessionId} (${abbreviateResponseId(lineage.parentResponseId)})`
|
|
637
|
+
: lineage.parentSessionId;
|
|
638
|
+
}
|
|
631
639
|
function buildStatusTreeRows(entries, responseOwners) {
|
|
632
640
|
const entryById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
633
641
|
const orderIndex = new Map(entries.map((entry, index) => [entry.id, index]));
|
|
@@ -668,7 +676,7 @@ function buildStatusTreeRows(entries, responseOwners) {
|
|
|
668
676
|
visited.add(entry.id);
|
|
669
677
|
const lineage = lineageById.get(entry.id);
|
|
670
678
|
const hiddenParent = lineage?.parentSessionId && !entryById.has(lineage.parentSessionId)
|
|
671
|
-
?
|
|
679
|
+
? formatLineageParentLabel(lineage)
|
|
672
680
|
: undefined;
|
|
673
681
|
const children = childMap.get(entry.id) ?? [];
|
|
674
682
|
rows.push({ entry, displaySlug: entry.id, detachedParentLabel: hiddenParent });
|
|
@@ -694,13 +702,19 @@ async function buildSessionChainLine(metadata) {
|
|
|
694
702
|
return `root -> ${metadata.id}`;
|
|
695
703
|
}
|
|
696
704
|
if (lineageWithoutLookup.parentSessionId) {
|
|
697
|
-
return `${
|
|
705
|
+
return `${formatLineageParentLabel(lineageWithoutLookup)} -> ${metadata.id}`;
|
|
706
|
+
}
|
|
707
|
+
if (!lineageWithoutLookup.parentResponseId) {
|
|
708
|
+
return `root -> ${metadata.id}`;
|
|
698
709
|
}
|
|
699
710
|
const sessions = await sessionStore.listSessions().catch(() => []);
|
|
700
711
|
const responseOwners = buildResponseOwnerIndex(sessions);
|
|
701
712
|
const lineage = resolveSessionLineage(metadata, responseOwners) ?? lineageWithoutLookup;
|
|
702
713
|
if (lineage.parentSessionId) {
|
|
703
|
-
return `${
|
|
714
|
+
return `${formatLineageParentLabel(lineage)} -> ${metadata.id}`;
|
|
715
|
+
}
|
|
716
|
+
if (!lineage.parentResponseId) {
|
|
717
|
+
return `root -> ${metadata.id}`;
|
|
704
718
|
}
|
|
705
719
|
return `${abbreviateResponseId(lineage.parentResponseId)} -> ${metadata.id}`;
|
|
706
720
|
}
|
|
@@ -39,17 +39,20 @@ export function buildResponseOwnerIndex(sessions) {
|
|
|
39
39
|
}
|
|
40
40
|
export function resolveSessionLineage(meta, responseOwners) {
|
|
41
41
|
const previous = meta.options?.previousResponseId?.trim();
|
|
42
|
-
|
|
42
|
+
let parentSessionId = meta.options?.followupSessionId?.trim();
|
|
43
|
+
if (!previous && !parentSessionId) {
|
|
43
44
|
return null;
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
if (!parentSessionId && responseOwners) {
|
|
46
|
+
if (!parentSessionId && previous && responseOwners) {
|
|
47
47
|
parentSessionId = responseOwners.get(previous);
|
|
48
48
|
}
|
|
49
49
|
if (parentSessionId === meta.id) {
|
|
50
50
|
parentSessionId = undefined;
|
|
51
51
|
}
|
|
52
|
-
return {
|
|
52
|
+
return {
|
|
53
|
+
parentResponseId: previous || undefined,
|
|
54
|
+
parentSessionId,
|
|
55
|
+
};
|
|
53
56
|
}
|
|
54
57
|
export function abbreviateResponseId(responseId, max = 18) {
|
|
55
58
|
if (responseId.length <= max) {
|
|
@@ -24,6 +24,7 @@ import { resumeBrowserSession } from "../browser/reattach.js";
|
|
|
24
24
|
import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
|
|
25
25
|
import { estimateTokenCount } from "../browser/utils.js";
|
|
26
26
|
import { formatElapsed } from "../oracle/format.js";
|
|
27
|
+
import { formatBrowserReattachGuidance } from "./reattachGuidance.js";
|
|
27
28
|
const isTty = process.stdout.isTTY;
|
|
28
29
|
const dim = (text) => (isTty ? kleur.dim(text) : text);
|
|
29
30
|
export async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, version, notifications, browserDeps, muteStdout = false, }) {
|
|
@@ -388,6 +389,16 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
388
389
|
userError.details?.stage === "assistant-timeout";
|
|
389
390
|
const cloudflareChallenge = userError?.category === "browser-automation" &&
|
|
390
391
|
userError.details?.stage === "cloudflare-challenge";
|
|
392
|
+
let reattachGuidanceLogged = false;
|
|
393
|
+
const logBrowserReattachGuidance = (runtime) => {
|
|
394
|
+
if (reattachGuidanceLogged || mode !== "browser")
|
|
395
|
+
return;
|
|
396
|
+
if (!hasRecoverableChatGptConversation(runtime) && runtime?.promptSubmitted !== true) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
reattachGuidanceLogged = true;
|
|
400
|
+
log(formatBrowserReattachGuidance(sessionMeta.id));
|
|
401
|
+
};
|
|
391
402
|
if (connectionLost && mode === "browser") {
|
|
392
403
|
const runtime = userError.details
|
|
393
404
|
?.runtime;
|
|
@@ -442,6 +453,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
442
453
|
},
|
|
443
454
|
response: { status: "running", incompleteReason: "chrome-disconnected" },
|
|
444
455
|
});
|
|
456
|
+
logBrowserReattachGuidance(recoverableRuntime);
|
|
445
457
|
return;
|
|
446
458
|
}
|
|
447
459
|
if (assistantTimeout && mode === "browser") {
|
|
@@ -492,7 +504,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
492
504
|
return;
|
|
493
505
|
}
|
|
494
506
|
}
|
|
495
|
-
|
|
507
|
+
logBrowserReattachGuidance(runtime ?? sessionMeta.browser?.runtime);
|
|
496
508
|
return;
|
|
497
509
|
}
|
|
498
510
|
if (cloudflareChallenge && mode === "browser") {
|
|
@@ -518,6 +530,9 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
518
530
|
const browserRuntime = mode === "browser"
|
|
519
531
|
? userError?.details?.runtime
|
|
520
532
|
: undefined;
|
|
533
|
+
if (!cloudflareChallenge) {
|
|
534
|
+
logBrowserReattachGuidance(browserRuntime ?? sessionMeta.browser?.runtime);
|
|
535
|
+
}
|
|
521
536
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
522
537
|
status: "error",
|
|
523
538
|
completedAt: new Date().toISOString(),
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { buildGeminiWebModelHeader, FALLBACK_GEMINI_WEB_MODEL, } from "./models.js";
|
|
3
4
|
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
4
5
|
const MODEL_HEADER_NAME = "x-goog-ext-525001261-jspb";
|
|
5
|
-
const MODEL_HEADERS = {
|
|
6
|
-
"gemini-3-pro": '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4]]',
|
|
7
|
-
"gemini-3-pro-deep-think": '[1,null,null,null,"e6fa609c3fa255c0",null,null,0,[4],null,null,3]',
|
|
8
|
-
"gemini-2.5-pro": '[1,null,null,null,"4af6c7f5da75d65d",null,null,0,[4]]',
|
|
9
|
-
"gemini-2.5-flash": '[1,null,null,null,"9ec249fc9ad08861",null,null,0,[4]]',
|
|
10
|
-
};
|
|
11
6
|
const GEMINI_APP_URL = "https://gemini.google.com/app";
|
|
12
7
|
const GEMINI_STREAM_GENERATE_URL = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate";
|
|
13
8
|
const GEMINI_UPLOAD_URL = "https://content-push.googleapis.com/upload";
|
|
@@ -276,7 +271,7 @@ export async function runGeminiWebOnce(input) {
|
|
|
276
271
|
"x-same-domain": "1",
|
|
277
272
|
"user-agent": USER_AGENT,
|
|
278
273
|
cookie: cookieHeader,
|
|
279
|
-
[MODEL_HEADER_NAME]:
|
|
274
|
+
[MODEL_HEADER_NAME]: buildGeminiWebModelHeader(input.model),
|
|
280
275
|
},
|
|
281
276
|
body: params.toString(),
|
|
282
277
|
});
|
|
@@ -324,9 +319,9 @@ export async function runGeminiWebOnce(input) {
|
|
|
324
319
|
}
|
|
325
320
|
export async function runGeminiWebWithFallback(input) {
|
|
326
321
|
const attempt = await runGeminiWebOnce(input);
|
|
327
|
-
if (isGeminiModelUnavailable(attempt.errorCode) && input.model !==
|
|
328
|
-
const fallback = await runGeminiWebOnce({ ...input, model:
|
|
329
|
-
return { ...fallback, effectiveModel:
|
|
322
|
+
if (isGeminiModelUnavailable(attempt.errorCode) && input.model !== FALLBACK_GEMINI_WEB_MODEL) {
|
|
323
|
+
const fallback = await runGeminiWebOnce({ ...input, model: FALLBACK_GEMINI_WEB_MODEL });
|
|
324
|
+
return { ...fallback, effectiveModel: FALLBACK_GEMINI_WEB_MODEL };
|
|
330
325
|
}
|
|
331
326
|
return { ...attempt, effectiveModel: input.model };
|
|
332
327
|
}
|
|
@@ -4,6 +4,7 @@ import { runProviderDomFlow } from "../browser/providerDomFlow.js";
|
|
|
4
4
|
import { delay } from "../browser/utils.js";
|
|
5
5
|
import { runGeminiWebWithFallback, saveFirstGeminiImageFromOutput } from "./client.js";
|
|
6
6
|
import { geminiDeepThinkDomProvider } from "../browser/providers/index.js";
|
|
7
|
+
import { resolveGeminiWebModel } from "./models.js";
|
|
7
8
|
import { openGeminiBrowserSession } from "./browserSessionManager.js";
|
|
8
9
|
import { selectGeminiExecutionMode } from "./executionMode.js";
|
|
9
10
|
const GEMINI_COOKIE_NAMES = [
|
|
@@ -38,30 +39,6 @@ function resolveInvocationPath(value) {
|
|
|
38
39
|
return undefined;
|
|
39
40
|
return path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
|
|
40
41
|
}
|
|
41
|
-
function resolveGeminiWebModel(desiredModel, log) {
|
|
42
|
-
const desired = typeof desiredModel === "string" ? desiredModel.trim() : "";
|
|
43
|
-
if (!desired)
|
|
44
|
-
return "gemini-3-pro";
|
|
45
|
-
const normalized = desired.toLowerCase().replace(/[_\s]+/g, "-");
|
|
46
|
-
switch (normalized) {
|
|
47
|
-
case "gemini-3-pro":
|
|
48
|
-
case "gemini-3.0-pro":
|
|
49
|
-
return "gemini-3-pro";
|
|
50
|
-
case "gemini-3-deep-think":
|
|
51
|
-
case "gemini-3-pro-deep-think":
|
|
52
|
-
case "gemini-3-pro-deepthink":
|
|
53
|
-
return "gemini-3-pro-deep-think";
|
|
54
|
-
case "gemini-2.5-pro":
|
|
55
|
-
return "gemini-2.5-pro";
|
|
56
|
-
case "gemini-2.5-flash":
|
|
57
|
-
return "gemini-2.5-flash";
|
|
58
|
-
default:
|
|
59
|
-
if (normalized.startsWith("gemini-") || normalized.includes("gemini")) {
|
|
60
|
-
log?.(`[gemini-web] Unsupported Gemini web model "${desired}". Falling back to gemini-3-pro.`);
|
|
61
|
-
}
|
|
62
|
-
return "gemini-3-pro";
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
42
|
function resolveCookieDomain(cookie) {
|
|
66
43
|
const rawDomain = cookie.domain?.trim();
|
|
67
44
|
if (rawDomain) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export const DEFAULT_GEMINI_WEB_MODEL = "gemini-3.1-pro";
|
|
3
|
+
export const FALLBACK_GEMINI_WEB_MODEL = "gemini-3.1-flash-lite";
|
|
4
|
+
const MODEL_SPECS = {
|
|
5
|
+
"gemini-3.1-flash-lite": {
|
|
6
|
+
hash: "1d44b34bcaa1c04d",
|
|
7
|
+
modelCode: 6,
|
|
8
|
+
thinkingCode: 1,
|
|
9
|
+
},
|
|
10
|
+
"gemini-3.5-flash": {
|
|
11
|
+
hash: "56fdd199312815e2",
|
|
12
|
+
modelCode: 1,
|
|
13
|
+
thinkingCode: 1,
|
|
14
|
+
},
|
|
15
|
+
"gemini-3.1-pro": {
|
|
16
|
+
hash: "797f3d0293f288ad",
|
|
17
|
+
modelCode: 3,
|
|
18
|
+
thinkingCode: 1,
|
|
19
|
+
},
|
|
20
|
+
"gemini-3-pro-deep-think": {
|
|
21
|
+
hash: "797f3d0293f288ad",
|
|
22
|
+
modelCode: 3,
|
|
23
|
+
thinkingCode: 3,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
let clientId;
|
|
27
|
+
function getGeminiWebClientId() {
|
|
28
|
+
clientId ??= randomUUID().toUpperCase();
|
|
29
|
+
return clientId;
|
|
30
|
+
}
|
|
31
|
+
export function buildGeminiWebModelHeader(model, webClientId = getGeminiWebClientId()) {
|
|
32
|
+
const spec = MODEL_SPECS[model];
|
|
33
|
+
return JSON.stringify([
|
|
34
|
+
1,
|
|
35
|
+
null,
|
|
36
|
+
null,
|
|
37
|
+
null,
|
|
38
|
+
spec.hash,
|
|
39
|
+
null,
|
|
40
|
+
null,
|
|
41
|
+
1,
|
|
42
|
+
[4, 5, 6, 8],
|
|
43
|
+
null,
|
|
44
|
+
null,
|
|
45
|
+
3,
|
|
46
|
+
null,
|
|
47
|
+
null,
|
|
48
|
+
spec.modelCode,
|
|
49
|
+
spec.thinkingCode,
|
|
50
|
+
webClientId,
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
export function resolveGeminiWebModel(desiredModel, log) {
|
|
54
|
+
const desired = typeof desiredModel === "string" ? desiredModel.trim() : "";
|
|
55
|
+
if (!desired)
|
|
56
|
+
return DEFAULT_GEMINI_WEB_MODEL;
|
|
57
|
+
const normalized = desired.toLowerCase().replace(/[_\s]+/g, "-");
|
|
58
|
+
switch (normalized) {
|
|
59
|
+
case "gemini-3.1-pro":
|
|
60
|
+
case "gemini-3-pro":
|
|
61
|
+
case "gemini-3.0-pro":
|
|
62
|
+
return "gemini-3.1-pro";
|
|
63
|
+
case "gemini-3.5-flash":
|
|
64
|
+
return "gemini-3.5-flash";
|
|
65
|
+
case "gemini-3.1-flash-lite":
|
|
66
|
+
case "gemini-3.1-flashlite":
|
|
67
|
+
return "gemini-3.1-flash-lite";
|
|
68
|
+
case "gemini-3-deep-think":
|
|
69
|
+
case "gemini-3-pro-deep-think":
|
|
70
|
+
case "gemini-3-pro-deepthink":
|
|
71
|
+
case "gemini-3.1-pro-deep-think":
|
|
72
|
+
return "gemini-3-pro-deep-think";
|
|
73
|
+
case "gemini-2.5-pro":
|
|
74
|
+
return "gemini-3.1-pro";
|
|
75
|
+
case "gemini-2.5-flash":
|
|
76
|
+
return "gemini-3.1-flash-lite";
|
|
77
|
+
default:
|
|
78
|
+
if (normalized.startsWith("gemini-") || normalized.includes("gemini")) {
|
|
79
|
+
log?.(`[gemini-web] Unsupported Gemini web model "${desired}". Falling back to ${DEFAULT_GEMINI_WEB_MODEL}.`);
|
|
80
|
+
}
|
|
81
|
+
return DEFAULT_GEMINI_WEB_MODEL;
|
|
82
|
+
}
|
|
83
|
+
}
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
|
|
|
5
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
7
|
import { getCliVersion } from "../version.js";
|
|
8
|
+
import { registerChatGptImageTool } from "./tools/chatgptImage.js";
|
|
8
9
|
import { registerConsultTool } from "./tools/consult.js";
|
|
9
10
|
import { registerProjectSourcesTool } from "./tools/projectSources.js";
|
|
10
11
|
import { registerSessionsTool } from "./tools/sessions.js";
|
|
@@ -19,6 +20,7 @@ export async function startMcpServer() {
|
|
|
19
20
|
},
|
|
20
21
|
});
|
|
21
22
|
registerConsultTool(server);
|
|
23
|
+
registerChatGptImageTool(server);
|
|
22
24
|
registerProjectSourcesTool(server);
|
|
23
25
|
registerSessionsTool(server);
|
|
24
26
|
registerSessionResources(server);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getOracleHomeDir } from "../../oracleHome.js";
|
|
5
|
+
import { consultOutputShape, runConsultTool } from "./consult.js";
|
|
6
|
+
const chatGptImageInputShape = {
|
|
7
|
+
prompt: z.string().min(1, "Prompt is required.").describe("Image generation prompt."),
|
|
8
|
+
files: z
|
|
9
|
+
.array(z.string())
|
|
10
|
+
.default([])
|
|
11
|
+
.describe("Optional reference image/file paths or globs to upload to ChatGPT."),
|
|
12
|
+
outputPath: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("Where to save the first generated image. Defaults to a unique file under ORACLE_HOME_DIR/generated/."),
|
|
16
|
+
aspectRatio: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('Optional requested image aspect ratio, e.g. "1:1", "9:16", or "16:9".'),
|
|
20
|
+
model: z
|
|
21
|
+
.string()
|
|
22
|
+
.refine((value) => !/^(claude|gemini|grok)(?:[-\s]|$)/i.test(value.trim()), {
|
|
23
|
+
message: "chatgpt_image requires a ChatGPT/GPT model.",
|
|
24
|
+
})
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Optional ChatGPT/browser model label or alias. Defaults follow Oracle config."),
|
|
27
|
+
browserModelLabel: z.string().optional().describe("Explicit ChatGPT UI model label to select."),
|
|
28
|
+
browserAttachments: z
|
|
29
|
+
.enum(["auto", "never", "always"])
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('How to deliver files. Defaults to "always" when files are present so reference images are uploaded.'),
|
|
32
|
+
browserThinkingTime: z
|
|
33
|
+
.enum(["light", "standard", "extended", "heavy"])
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Set ChatGPT thinking time when supported by the chosen model."),
|
|
36
|
+
browserModelStrategy: z
|
|
37
|
+
.enum(["select", "current", "ignore"])
|
|
38
|
+
.optional()
|
|
39
|
+
.describe("Model picker strategy. Mirrors the consult tool and CLI browser flag."),
|
|
40
|
+
browserArchive: z
|
|
41
|
+
.enum(["auto", "always", "never"])
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Archive completed ChatGPT conversations after local artifacts are saved."),
|
|
44
|
+
browserKeepBrowser: z
|
|
45
|
+
.boolean()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Keep Chrome running after completion for debugging."),
|
|
48
|
+
dryRun: z
|
|
49
|
+
.boolean()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Preview the resolved image run without touching the browser."),
|
|
52
|
+
slug: z.string().optional().describe("Optional human-friendly session id."),
|
|
53
|
+
};
|
|
54
|
+
const chatGptImageOutputShape = {
|
|
55
|
+
// Mirror the consult output contract so structuredContent stays consistent
|
|
56
|
+
// (images/artifacts/resolved are typed by the shared consult shapes), plus the
|
|
57
|
+
// image-specific echo of the requested path.
|
|
58
|
+
...consultOutputShape,
|
|
59
|
+
requestedOutputPath: z.string(),
|
|
60
|
+
};
|
|
61
|
+
const chatGptImageInputSchema = z.object(chatGptImageInputShape).strict();
|
|
62
|
+
function resolveDefaultImageOutputPath() {
|
|
63
|
+
// Include a random token so concurrent agent calls in the same millisecond do
|
|
64
|
+
// not resolve to the same default path and overwrite each other.
|
|
65
|
+
const unique = `${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
66
|
+
return path.join(getOracleHomeDir(), "generated", `chatgpt-image-${unique}.png`);
|
|
67
|
+
}
|
|
68
|
+
function appendAspectRatio(prompt, aspectRatio) {
|
|
69
|
+
const requestedAspectRatio = aspectRatio?.trim();
|
|
70
|
+
if (!requestedAspectRatio) {
|
|
71
|
+
return prompt.trim();
|
|
72
|
+
}
|
|
73
|
+
return `${prompt.trim()}\n\nCreate the image with aspect ratio ${requestedAspectRatio}.`;
|
|
74
|
+
}
|
|
75
|
+
export function buildChatGptImageConsultInput(input) {
|
|
76
|
+
const files = input.files ?? [];
|
|
77
|
+
const outputPath = input.outputPath?.trim() || resolveDefaultImageOutputPath();
|
|
78
|
+
const browserAttachments = input.browserAttachments ?? (files.length > 0 ? "always" : undefined);
|
|
79
|
+
return {
|
|
80
|
+
prompt: appendAspectRatio(input.prompt, input.aspectRatio),
|
|
81
|
+
files,
|
|
82
|
+
model: input.model,
|
|
83
|
+
engine: "browser",
|
|
84
|
+
browserModelLabel: input.browserModelLabel,
|
|
85
|
+
browserAttachments,
|
|
86
|
+
browserThinkingTime: input.browserThinkingTime,
|
|
87
|
+
browserModelStrategy: input.browserModelStrategy,
|
|
88
|
+
browserArchive: input.browserArchive,
|
|
89
|
+
browserKeepBrowser: input.browserKeepBrowser,
|
|
90
|
+
generateImage: outputPath,
|
|
91
|
+
dryRun: input.dryRun,
|
|
92
|
+
slug: input.slug,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function registerChatGptImageTool(server) {
|
|
96
|
+
server.registerTool("chatgpt_image", {
|
|
97
|
+
title: "Generate an image with ChatGPT",
|
|
98
|
+
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.",
|
|
99
|
+
inputSchema: chatGptImageInputShape,
|
|
100
|
+
outputSchema: chatGptImageOutputShape,
|
|
101
|
+
}, async (input) => {
|
|
102
|
+
const textContent = (text) => [{ type: "text", text }];
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = chatGptImageInputSchema.parse(input);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
isError: true,
|
|
110
|
+
content: textContent(error instanceof Error ? error.message : String(error)),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const consultInput = buildChatGptImageConsultInput(parsed);
|
|
114
|
+
const result = await runConsultTool(consultInput, { server: server.server });
|
|
115
|
+
if (result.isError || !result.structuredContent) {
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
const structuredContent = {
|
|
119
|
+
...result.structuredContent,
|
|
120
|
+
requestedOutputPath: consultInput.generateImage,
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
...result,
|
|
124
|
+
structuredContent,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|