@steipete/oracle 0.17.3 → 0.19.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.
Files changed (161) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +75 -64
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/archiveConversation.js +31 -7
  5. package/dist/src/browser/actions/assistantResponse.js +43 -1
  6. package/dist/src/browser/actions/attachmentContext.js +239 -0
  7. package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
  8. package/dist/src/browser/actions/attachmentEvidence.js +78 -0
  9. package/dist/src/browser/actions/attachmentProgress.js +41 -0
  10. package/dist/src/browser/actions/attachmentPrompt.js +130 -0
  11. package/dist/src/browser/actions/attachments.js +306 -355
  12. package/dist/src/browser/actions/deepResearch.js +2 -1
  13. package/dist/src/browser/actions/modelSelection.js +13 -3
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +361 -186
  16. package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
  17. package/dist/src/browser/actions/thinkingStatus.js +2 -5
  18. package/dist/src/browser/actions/thinkingTime.js +267 -15
  19. package/dist/src/browser/attachRunning.js +46 -2
  20. package/dist/src/browser/attachmentValidation.js +53 -0
  21. package/dist/src/browser/cancellation.js +105 -0
  22. package/dist/src/browser/chatgptImages.js +2 -0
  23. package/dist/src/browser/chatgptThrottle.js +104 -0
  24. package/dist/src/browser/chromeLifecycle.js +267 -43
  25. package/dist/src/browser/config.js +15 -3
  26. package/dist/src/browser/detect.js +1 -1
  27. package/dist/src/browser/index.js +447 -263
  28. package/dist/src/browser/modelDisplay.js +9 -0
  29. package/dist/src/browser/pageActions.js +2 -2
  30. package/dist/src/browser/policies.js +6 -2
  31. package/dist/src/browser/profileState.js +114 -9
  32. package/dist/src/browser/projectSourcesRunner.js +88 -17
  33. package/dist/src/browser/prompt.js +272 -113
  34. package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
  35. package/dist/src/browser/reattach.js +14 -4
  36. package/dist/src/browser/sessionRunner.js +71 -11
  37. package/dist/src/browser/tabLeaseRegistry.js +414 -38
  38. package/dist/src/browser/utils.js +20 -2
  39. package/dist/src/cli/browserConfig.js +40 -15
  40. package/dist/src/cli/browserDefaults.js +13 -0
  41. package/dist/src/cli/browserTabs.js +12 -29
  42. package/dist/src/cli/detachedSession.js +69 -0
  43. package/dist/src/cli/dryRun.js +58 -47
  44. package/dist/src/cli/harvestIntegrity.js +140 -0
  45. package/dist/src/cli/projectSources.js +5 -3
  46. package/dist/src/cli/serveBrowserConfig.js +24 -0
  47. package/dist/src/cli/sessionDisplay.js +9 -2
  48. package/dist/src/cli/sessionRunner.js +91 -7
  49. package/dist/src/config.js +1 -0
  50. package/dist/src/gemini-web/client.js +67 -21
  51. package/dist/src/gemini-web/executor.js +4 -0
  52. package/dist/src/gemini-web/http.js +15 -0
  53. package/dist/src/gemini-web/models.js +15 -11
  54. package/dist/src/mcp/server.js +11 -14
  55. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  56. package/dist/src/mcp/tools/consult.js +97 -26
  57. package/dist/src/mcp/tools/projectSources.js +4 -6
  58. package/dist/src/mcp/tools/sessionResources.js +1 -1
  59. package/dist/src/mcp/tools/sessions.js +2 -2
  60. package/dist/src/mcp/tools/wait.js +182 -0
  61. package/dist/src/mcp/types.js +7 -0
  62. package/dist/src/oracle/errors.js +11 -0
  63. package/dist/src/oracle/providerRoutePlan.js +3 -2
  64. package/dist/src/remote/client.js +70 -6
  65. package/dist/src/remote/health.js +6 -2
  66. package/dist/src/remote/runSlots.js +83 -0
  67. package/dist/src/remote/server.js +351 -72
  68. package/dist/src/sessionManager.js +36 -1
  69. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  70. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  71. package/package.json +25 -23
  72. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  73. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  74. package/dist/bin/oracle.js +0 -683
  75. package/dist/docs-site/.nojekyll +0 -0
  76. package/dist/docs-site/CNAME +0 -1
  77. package/dist/docs-site/RELEASING.html +0 -410
  78. package/dist/docs-site/agents.html +0 -374
  79. package/dist/docs-site/anthropic.html +0 -368
  80. package/dist/docs-site/bridge.html +0 -400
  81. package/dist/docs-site/browser-mode.html +0 -588
  82. package/dist/docs-site/chromium-forks.html +0 -347
  83. package/dist/docs-site/cli-reference.html +0 -346
  84. package/dist/docs-site/configuration.html +0 -436
  85. package/dist/docs-site/favicon.svg +0 -14
  86. package/dist/docs-site/followup.html +0 -368
  87. package/dist/docs-site/gemini.html +0 -379
  88. package/dist/docs-site/grok.html +0 -325
  89. package/dist/docs-site/index.html +0 -360
  90. package/dist/docs-site/install.html +0 -335
  91. package/dist/docs-site/linux.html +0 -321
  92. package/dist/docs-site/llms.txt +0 -43
  93. package/dist/docs-site/manual-tests.html +0 -595
  94. package/dist/docs-site/mcp.html +0 -368
  95. package/dist/docs-site/multimodel.html +0 -364
  96. package/dist/docs-site/mythical-pro-agents.html +0 -360
  97. package/dist/docs-site/notifier.html +0 -338
  98. package/dist/docs-site/openai-endpoints.html +0 -387
  99. package/dist/docs-site/openrouter.html +0 -344
  100. package/dist/docs-site/quickstart.html +0 -369
  101. package/dist/docs-site/refactor/ux.html +0 -532
  102. package/dist/docs-site/sessions.html +0 -388
  103. package/dist/docs-site/social-card.png +0 -0
  104. package/dist/docs-site/social-card.svg +0 -79
  105. package/dist/docs-site/spec.html +0 -363
  106. package/dist/docs-site/testing.html +0 -320
  107. package/dist/docs-site/tui-debug.html +0 -326
  108. package/dist/docs-site/windows-work.html +0 -323
  109. package/dist/docs-site/windows.html +0 -320
  110. package/dist/markdansi/types/index.js +0 -4
  111. package/dist/oracle/bin/oracle-cli.js +0 -472
  112. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  113. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  114. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  115. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  116. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  117. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  118. package/dist/oracle/src/browser/config.js +0 -33
  119. package/dist/oracle/src/browser/constants.js +0 -40
  120. package/dist/oracle/src/browser/cookies.js +0 -210
  121. package/dist/oracle/src/browser/domDebug.js +0 -36
  122. package/dist/oracle/src/browser/index.js +0 -331
  123. package/dist/oracle/src/browser/pageActions.js +0 -5
  124. package/dist/oracle/src/browser/prompt.js +0 -88
  125. package/dist/oracle/src/browser/promptSummary.js +0 -20
  126. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  127. package/dist/oracle/src/browser/types.js +0 -1
  128. package/dist/oracle/src/browser/utils.js +0 -62
  129. package/dist/oracle/src/browserMode.js +0 -1
  130. package/dist/oracle/src/cli/browserConfig.js +0 -44
  131. package/dist/oracle/src/cli/dryRun.js +0 -59
  132. package/dist/oracle/src/cli/engine.js +0 -17
  133. package/dist/oracle/src/cli/errorUtils.js +0 -9
  134. package/dist/oracle/src/cli/help.js +0 -70
  135. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  136. package/dist/oracle/src/cli/options.js +0 -103
  137. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  138. package/dist/oracle/src/cli/rootAlias.js +0 -30
  139. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  140. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  141. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  142. package/dist/oracle/src/heartbeat.js +0 -43
  143. package/dist/oracle/src/oracle/client.js +0 -48
  144. package/dist/oracle/src/oracle/config.js +0 -29
  145. package/dist/oracle/src/oracle/errors.js +0 -101
  146. package/dist/oracle/src/oracle/files.js +0 -220
  147. package/dist/oracle/src/oracle/format.js +0 -33
  148. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  149. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  150. package/dist/oracle/src/oracle/request.js +0 -48
  151. package/dist/oracle/src/oracle/run.js +0 -444
  152. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  153. package/dist/oracle/src/oracle/types.js +0 -1
  154. package/dist/oracle/src/oracle.js +0 -9
  155. package/dist/oracle/src/sessionManager.js +0 -205
  156. package/dist/oracle/src/version.js +0 -39
  157. package/dist/scripts/chrome/browser-tools.js +0 -295
  158. package/dist/src/browser/chromeCookies.js +0 -312
  159. package/dist/src/browser/keytarShim.js +0 -56
  160. package/dist/src/browser/profileSync.js +0 -141
  161. package/dist/src/browser/windowsCookies.js +0 -219
@@ -12,7 +12,7 @@ import { estimateTokenCount } from "../browser/utils.js";
12
12
  import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
13
13
  import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from "./sessionLineage.js";
14
14
  import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
15
- import { formatBrowserModelSelectionEvidence, formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../browser/modelDisplay.js";
15
+ import { formatBrowserModelSelectionEvidence, formatBrowserThinkingSelectionEvidence, formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../browser/modelDisplay.js";
16
16
  const isTty = () => Boolean(process.stdout.isTTY);
17
17
  const dim = (text) => (isTty() ? kleur.dim(text) : text);
18
18
  export const MAX_RENDER_BYTES = 200_000;
@@ -255,6 +255,7 @@ export async function attachSession(sessionId, options) {
255
255
  config: metadata.browser?.config,
256
256
  runtime,
257
257
  modelSelection: metadata.browser?.modelSelection,
258
+ thinkingSelection: metadata.browser?.thinkingSelection,
258
259
  warnings: metadata.browser?.warnings,
259
260
  },
260
261
  artifacts,
@@ -603,7 +604,9 @@ export function formatUserErrorMetadata(metadata) {
603
604
  }
604
605
  export function formatBrowserEvidence(metadata) {
605
606
  const browser = metadata.browser;
606
- if (!browser?.modelSelection && (!browser?.warnings || browser.warnings.length === 0)) {
607
+ if (!browser?.modelSelection &&
608
+ !browser?.thinkingSelection &&
609
+ (!browser?.warnings || browser.warnings.length === 0)) {
607
610
  return null;
608
611
  }
609
612
  const lines = [];
@@ -611,6 +614,10 @@ export function formatBrowserEvidence(metadata) {
611
614
  if (evidence) {
612
615
  lines.push(`model ${formatBrowserModelSelectionEvidence(evidence, metadata.model)}`);
613
616
  }
617
+ const thinkingEvidence = browser.thinkingSelection;
618
+ if (thinkingEvidence) {
619
+ lines.push(`effort ${formatBrowserThinkingSelectionEvidence(thinkingEvidence)}`);
620
+ }
614
621
  for (const warning of browser.warnings ?? []) {
615
622
  lines.push(`warning ${warning.code}: ${warning.message}`);
616
623
  }
@@ -1,4 +1,5 @@
1
1
  import kleur from "kleur";
2
+ import { constants as fsConstants } from "node:fs";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, classifyProviderFailure, } from "../oracle.js";
@@ -23,6 +24,7 @@ import { cwd as getCwd } from "node:process";
23
24
  import { resumeBrowserSession } from "../browser/reattach.js";
24
25
  import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
25
26
  import { estimateTokenCount } from "../browser/utils.js";
27
+ import { computeFileSha256, sanitizeArtifactFilename } from "../browser/artifacts.js";
26
28
  import { formatElapsed } from "../oracle/format.js";
27
29
  import { formatBrowserReattachGuidance } from "./reattachGuidance.js";
28
30
  const isTty = process.stdout.isTTY;
@@ -78,7 +80,13 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
78
80
  cwd,
79
81
  log,
80
82
  }, runnerDeps);
81
- await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
83
+ const writtenOutputPath = await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
84
+ const outputArtifacts = await copyBrowserOutputArtifacts({
85
+ outputPath: writtenOutputPath,
86
+ savedFiles: runOptions.writeArtifacts ? result.savedFiles : undefined,
87
+ log,
88
+ });
89
+ const browserWarnings = [...(result.warnings ?? []), ...outputArtifacts.warnings];
82
90
  await sendSessionNotification({
83
91
  sessionId: sessionMeta.id,
84
92
  sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
@@ -105,9 +113,10 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
105
113
  runtime: result.runtime,
106
114
  archive: result.archive,
107
115
  modelSelection: result.modelSelection,
108
- warnings: result.warnings,
116
+ thinkingSelection: result.thinkingSelection,
117
+ warnings: browserWarnings.length > 0 ? browserWarnings : undefined,
109
118
  },
110
- artifacts: mergeArtifacts(sessionMeta.artifacts, result.artifacts),
119
+ artifacts: mergeArtifacts(sessionMeta.artifacts, mergeArtifacts(result.artifacts, outputArtifacts.artifacts)),
111
120
  response: undefined,
112
121
  transport: undefined,
113
122
  error: undefined,
@@ -848,6 +857,81 @@ function resolveSessionPath(sessionDir, targetPath) {
848
857
  }
849
858
  return path.join(sessionDir, targetPath);
850
859
  }
860
+ async function copyBrowserOutputArtifacts(params) {
861
+ const artifacts = [];
862
+ const warnings = [];
863
+ if (!params.outputPath || !params.savedFiles?.length) {
864
+ return { artifacts, warnings };
865
+ }
866
+ const outputDir = path.dirname(params.outputPath);
867
+ for (const savedFile of params.savedFiles) {
868
+ const filename = sanitizeArtifactFilename(savedFile.filename ?? path.basename(savedFile.path), "artifact.bin");
869
+ let copied;
870
+ try {
871
+ copied = await copyFileWithoutOverwrite(savedFile.path, path.join(outputDir, filename));
872
+ const [stat, sha256] = await Promise.all([fs.stat(copied), computeFileSha256(copied)]);
873
+ if (savedFile.sizeBytes != null && stat.size !== savedFile.sizeBytes) {
874
+ throw new Error(`size mismatch (expected ${savedFile.sizeBytes}, received ${stat.size})`);
875
+ }
876
+ if (savedFile.sha256 && sha256 !== savedFile.sha256) {
877
+ throw new Error(`sha256 mismatch (expected ${savedFile.sha256}, received ${sha256})`);
878
+ }
879
+ const artifact = {
880
+ kind: "file",
881
+ path: copied,
882
+ label: `${savedFile.label ?? filename} (write-output)`,
883
+ mimeType: savedFile.mimeType,
884
+ sizeBytes: stat.size,
885
+ sourceUrl: savedFile.sourceUrl,
886
+ sha256,
887
+ validation: savedFile.validation,
888
+ transfer: { status: "not-needed" },
889
+ origin: { mode: "local" },
890
+ };
891
+ artifacts.push(artifact);
892
+ params.log(dim(`[browser] Saved write-output file artifact to ${copied} sha256=${sha256}`));
893
+ }
894
+ catch (error) {
895
+ if (copied) {
896
+ await fs.unlink(copied).catch(() => undefined);
897
+ }
898
+ const reason = error instanceof Error ? error.message : String(error);
899
+ const message = `Failed to copy saved file artifact ${savedFile.path} beside ${params.outputPath}: ${reason}`;
900
+ params.log(dim(`[browser] ${message}`));
901
+ warnings.push({
902
+ code: "browser-output-artifact-copy-failed",
903
+ severity: "warning",
904
+ message,
905
+ details: {
906
+ sourcePath: savedFile.path,
907
+ outputPath: params.outputPath,
908
+ sha256: savedFile.sha256,
909
+ },
910
+ });
911
+ }
912
+ }
913
+ return { artifacts, warnings };
914
+ }
915
+ async function copyFileWithoutOverwrite(sourcePath, baseTargetPath) {
916
+ const ext = path.extname(baseTargetPath);
917
+ const stem = ext ? path.basename(baseTargetPath, ext) : path.basename(baseTargetPath);
918
+ const dir = path.dirname(baseTargetPath);
919
+ let suffix = 1;
920
+ for (;;) {
921
+ const targetPath = suffix === 1 ? baseTargetPath : path.join(dir, `${stem}-${suffix}${ext}`);
922
+ try {
923
+ await fs.copyFile(sourcePath, targetPath, fsConstants.COPYFILE_EXCL);
924
+ return targetPath;
925
+ }
926
+ catch (error) {
927
+ if (isErrorCode(error, "EEXIST")) {
928
+ suffix += 1;
929
+ continue;
930
+ }
931
+ throw error;
932
+ }
933
+ }
934
+ }
851
935
  async function writeAssistantOutput(targetPath, content, log) {
852
936
  if (!targetPath)
853
937
  return;
@@ -1057,11 +1141,11 @@ export function deriveModelOutputPath(basePath, model) {
1057
1141
  const suffix = ext.length > 0 ? `${stem}.${model}${ext}` : `${stem}.${model}`;
1058
1142
  return path.join(dir, suffix);
1059
1143
  }
1144
+ function isErrorCode(error, expected) {
1145
+ return error instanceof Error && error.code === expected;
1146
+ }
1060
1147
  function isPermissionError(error) {
1061
- if (!(error instanceof Error))
1062
- return false;
1063
- const code = error.code;
1064
- return code === "EACCES" || code === "EPERM";
1148
+ return isErrorCode(error, "EACCES") || isErrorCode(error, "EPERM");
1065
1149
  }
1066
1150
  function buildFallbackPath(original) {
1067
1151
  const ext = path.extname(original);
@@ -135,6 +135,7 @@ function sanitizeProjectConfig(config) {
135
135
  "attachRunning",
136
136
  "timeoutMs",
137
137
  "inputTimeoutMs",
138
+ "approvalWaitMs",
138
139
  "attachmentTimeoutMs",
139
140
  "assistantRecheckDelayMs",
140
141
  "assistantRecheckTimeoutMs",
@@ -1,7 +1,9 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
- import { buildGeminiWebModelHeader, FALLBACK_GEMINI_WEB_MODEL, } from "./models.js";
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
+ import { fetchGeminiWebResource } from "./http.js";
5
+ import { buildGeminiWebModelHeader, FALLBACK_GEMINI_WEB_MODEL, getGeminiWebModelSelection, } from "./models.js";
6
+ const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36";
5
7
  const MODEL_HEADER_NAME = "x-goog-ext-525001261-jspb";
6
8
  const GEMINI_APP_URL = "https://gemini.google.com/app";
7
9
  const GEMINI_STREAM_GENERATE_URL = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate";
@@ -61,7 +63,7 @@ function buildCookieHeader(cookieMap) {
61
63
  }
62
64
  export async function fetchGeminiAccessToken(cookieMap, signal) {
63
65
  const cookieHeader = buildCookieHeader(cookieMap);
64
- const res = await fetch(GEMINI_APP_URL, {
66
+ const res = await fetchGeminiWebResource(GEMINI_APP_URL, {
65
67
  redirect: "follow",
66
68
  signal,
67
69
  headers: {
@@ -112,7 +114,7 @@ function ensureFullSizeImageUrl(url) {
112
114
  async function fetchWithCookiePreservingRedirects(url, init, signal, maxRedirects = 10) {
113
115
  let current = url;
114
116
  for (let i = 0; i <= maxRedirects; i += 1) {
115
- const res = await fetch(current, { ...init, redirect: "manual", signal });
117
+ const res = await fetchGeminiWebResource(current, { ...init, redirect: "manual", signal });
116
118
  if (res.status >= 300 && res.status < 400) {
117
119
  const location = res.headers.get("location");
118
120
  if (!location)
@@ -146,7 +148,7 @@ async function uploadGeminiFile(filePath, signal) {
146
148
  const mimeType = resolveGeminiUploadMimeType(absPath);
147
149
  const form = new FormData();
148
150
  form.append("file", new Blob([data], { type: mimeType }), fileName);
149
- const res = await fetch(GEMINI_UPLOAD_URL, {
151
+ const res = await fetchGeminiWebResource(GEMINI_UPLOAD_URL, {
150
152
  method: "POST",
151
153
  redirect: "follow",
152
154
  signal,
@@ -162,18 +164,38 @@ async function uploadGeminiFile(filePath, signal) {
162
164
  }
163
165
  return { id: text, name: fileName, mimeType };
164
166
  }
165
- function buildGeminiFReqPayload(prompt, uploaded, chatMetadata) {
166
- const promptPayload = uploaded.length > 0
167
- ? [
168
- prompt,
169
- 0,
170
- null,
171
- // Format: [[[fileId, 1, null, "mimeType"], "filename", ...]]
172
- uploaded.map((file) => [[file.id, 1, null, file.mimeType], file.name]),
173
- ]
174
- : [prompt];
175
- const innerList = [promptPayload, null, chatMetadata ?? null];
176
- return JSON.stringify([null, JSON.stringify(innerList)]);
167
+ function buildGeminiFReqPayload(prompt, uploaded, chatMetadata, model, requestId) {
168
+ const request = Array(97).fill(null);
169
+ request[0] = [
170
+ prompt,
171
+ 0,
172
+ null,
173
+ uploaded.map((file) => [[file.id, 1, null, file.mimeType], file.name]),
174
+ null,
175
+ null,
176
+ 0,
177
+ ];
178
+ request[1] = ["en"];
179
+ request[2] = chatMetadata ?? ["", "", "", null, null, null, null, null, null, ""];
180
+ request[6] = [1];
181
+ request[7] = 1;
182
+ request[10] = 1;
183
+ request[11] = 0;
184
+ request[17] = [[0]];
185
+ request[18] = 0;
186
+ request[27] = 1;
187
+ request[30] = [4];
188
+ request[41] = [1];
189
+ request[53] = 0;
190
+ request[59] = requestId;
191
+ request[61] = [];
192
+ request[68] = 2;
193
+ const selection = getGeminiWebModelSelection(model);
194
+ request[79] = selection.modelCode;
195
+ request[80] = selection.thinkingCode;
196
+ request[91] = 0;
197
+ request[96] = chatMetadata == null ? 1 : 0;
198
+ return JSON.stringify([null, JSON.stringify(request)]);
177
199
  }
178
200
  export function parseGeminiStreamGenerateResponse(rawText) {
179
201
  const responseJson = JSON.parse(trimGeminiJsonEnvelope(rawText));
@@ -274,11 +296,12 @@ export async function runGeminiWebOnce(input) {
274
296
  }
275
297
  uploaded.push(await uploadGeminiFile(file, input.signal));
276
298
  }
277
- const fReq = buildGeminiFReqPayload(input.prompt, uploaded, input.chatMetadata ?? null);
299
+ const requestId = randomUUID().toUpperCase();
300
+ const fReq = buildGeminiFReqPayload(input.prompt, uploaded, input.chatMetadata ?? null, input.model, requestId);
278
301
  const params = new URLSearchParams();
279
302
  params.set("at", at);
280
303
  params.set("f.req", fReq);
281
- const res = await fetch(GEMINI_STREAM_GENERATE_URL, {
304
+ const res = await fetchGeminiWebResource(GEMINI_STREAM_GENERATE_URL, {
282
305
  method: "POST",
283
306
  redirect: "follow",
284
307
  signal: input.signal,
@@ -290,6 +313,9 @@ export async function runGeminiWebOnce(input) {
290
313
  "user-agent": USER_AGENT,
291
314
  cookie: cookieHeader,
292
315
  [MODEL_HEADER_NAME]: buildGeminiWebModelHeader(input.model),
316
+ "x-goog-ext-73010989-jspb": "[0]",
317
+ "x-goog-ext-73010990-jspb": "[0]",
318
+ "x-goog-ext-525005358-jspb": JSON.stringify([requestId, 1]),
293
319
  },
294
320
  body: params.toString(),
295
321
  });
@@ -338,10 +364,30 @@ export async function runGeminiWebOnce(input) {
338
364
  export async function runGeminiWebWithFallback(input) {
339
365
  const attempt = await runGeminiWebOnce(input);
340
366
  if (isGeminiModelUnavailable(attempt.errorCode) && input.model !== FALLBACK_GEMINI_WEB_MODEL) {
367
+ if (input.allowModelFallback === false) {
368
+ throw new Error(`Requested Gemini web model ${input.model} is unavailable and model fallback is disabled.`);
369
+ }
341
370
  const fallback = await runGeminiWebOnce({ ...input, model: FALLBACK_GEMINI_WEB_MODEL });
342
- return { ...fallback, effectiveModel: FALLBACK_GEMINI_WEB_MODEL };
371
+ return {
372
+ ...assertGeminiWebRunSucceeded(fallback),
373
+ effectiveModel: FALLBACK_GEMINI_WEB_MODEL,
374
+ };
375
+ }
376
+ return { ...assertGeminiWebRunSucceeded(attempt), effectiveModel: input.model };
377
+ }
378
+ function assertGeminiWebRunSucceeded(output) {
379
+ if (output.errorCode !== undefined) {
380
+ throw new Error(`Gemini web request failed with error code ${output.errorCode}.`);
381
+ }
382
+ if (output.errorMessage) {
383
+ throw new Error(`Gemini web request failed: ${output.errorMessage}`);
384
+ }
385
+ if (!output.text.trim() &&
386
+ output.images.length === 0 &&
387
+ extractGgdlUrls(output.rawResponseText).length === 0) {
388
+ throw new Error("Gemini web request ended without a response.");
343
389
  }
344
- return { ...attempt, effectiveModel: input.model };
390
+ return output;
345
391
  }
346
392
  export async function saveFirstGeminiImageFromOutput(output, cookieMap, outputPath, signal) {
347
393
  const generatedOrWeb = output.images.find((img) => img.kind === "generated") ?? output.images[0];
@@ -325,6 +325,7 @@ export function createGeminiWebExecutor(geminiOptions) {
325
325
  cookieMap: cookieResult.cookieMap,
326
326
  chatMetadata: null,
327
327
  signal: controller.signal,
328
+ allowModelFallback: geminiOptions.allowModelFallback,
328
329
  });
329
330
  const editPrompt = `Use image generation tool to ${prompt}`;
330
331
  const out = await runGeminiWebWithFallback({
@@ -334,6 +335,7 @@ export function createGeminiWebExecutor(geminiOptions) {
334
335
  cookieMap: cookieResult.cookieMap,
335
336
  chatMetadata: intro.metadata,
336
337
  signal: controller.signal,
338
+ allowModelFallback: geminiOptions.allowModelFallback,
337
339
  });
338
340
  response = {
339
341
  text: out.text ?? null,
@@ -357,6 +359,7 @@ export function createGeminiWebExecutor(geminiOptions) {
357
359
  cookieMap: cookieResult.cookieMap,
358
360
  chatMetadata: null,
359
361
  signal: controller.signal,
362
+ allowModelFallback: geminiOptions.allowModelFallback,
360
363
  });
361
364
  response = {
362
365
  text: out.text ?? null,
@@ -379,6 +382,7 @@ export function createGeminiWebExecutor(geminiOptions) {
379
382
  cookieMap: cookieResult.cookieMap,
380
383
  chatMetadata: null,
381
384
  signal: controller.signal,
385
+ allowModelFallback: geminiOptions.allowModelFallback,
382
386
  });
383
387
  response = {
384
388
  text: out.text ?? null,
@@ -0,0 +1,15 @@
1
+ export async function fetchGeminiWebResource(url, init = {}) {
2
+ try {
3
+ return await fetch(url, init);
4
+ }
5
+ catch (error) {
6
+ const cause = error instanceof Error ? error.cause : undefined;
7
+ if (cause &&
8
+ typeof cause === "object" &&
9
+ "code" in cause &&
10
+ cause.code === "UND_ERR_HEADERS_OVERFLOW") {
11
+ throw new Error("Gemini response headers exceed Node's configured limit. Start Oracle with NODE_OPTIONS=--max-http-header-size=65536 (preserving any existing Node options) and retry.", { cause: error });
12
+ }
13
+ throw error;
14
+ }
15
+ }
@@ -6,29 +6,29 @@ const MODEL_SPECS = {
6
6
  hash: "1d44b34bcaa1c04d",
7
7
  modelCode: 6,
8
8
  thinkingCode: 1,
9
+ capacity: 2,
9
10
  },
10
11
  "gemini-3.5-flash": {
11
12
  hash: "56fdd199312815e2",
12
13
  modelCode: 1,
13
14
  thinkingCode: 1,
15
+ capacity: 2,
14
16
  },
15
17
  "gemini-3.1-pro": {
16
- hash: "797f3d0293f288ad",
18
+ hash: "e6fa609c3fa255c0",
17
19
  modelCode: 3,
18
20
  thinkingCode: 1,
21
+ capacity: 2,
19
22
  },
20
23
  "gemini-3-pro-deep-think": {
21
- hash: "797f3d0293f288ad",
22
- modelCode: 3,
23
- thinkingCode: 3,
24
+ hash: "e051ce1aa80aa576",
25
+ modelCode: 5,
26
+ thinkingCode: 2,
27
+ capacity: 2,
24
28
  },
25
29
  };
26
- let clientId;
27
- function getGeminiWebClientId() {
28
- clientId ??= randomUUID().toUpperCase();
29
- return clientId;
30
- }
31
- export function buildGeminiWebModelHeader(model, webClientId = getGeminiWebClientId()) {
30
+ const clientId = randomUUID().toUpperCase();
31
+ export function buildGeminiWebModelHeader(model, webClientId = clientId) {
32
32
  const spec = MODEL_SPECS[model];
33
33
  return JSON.stringify([
34
34
  1,
@@ -42,7 +42,7 @@ export function buildGeminiWebModelHeader(model, webClientId = getGeminiWebClien
42
42
  [4, 5, 6, 8],
43
43
  null,
44
44
  null,
45
- 3,
45
+ spec.capacity,
46
46
  null,
47
47
  null,
48
48
  spec.modelCode,
@@ -50,6 +50,10 @@ export function buildGeminiWebModelHeader(model, webClientId = getGeminiWebClien
50
50
  webClientId,
51
51
  ]);
52
52
  }
53
+ export function getGeminiWebModelSelection(model) {
54
+ const { modelCode, thinkingCode } = MODEL_SPECS[model];
55
+ return { modelCode, thinkingCode };
56
+ }
53
57
  export function resolveGeminiWebModel(desiredModel, log) {
54
58
  const desired = typeof desiredModel === "string" ? desiredModel.trim() : "";
55
59
  if (!desired)
@@ -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/sdk/server/mcp.js";
6
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
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 async function startMcpServer() {
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
- const transport = new StdioServerTransport();
28
- transport.onerror = (error) => {
29
- console.error("MCP transport error:", error);
30
- };
31
- const closed = new Promise((resolve) => {
32
- transport.onclose = () => {
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, { server: server.server });
119
+ const result = await runConsultTool(consultInput, { log: context.mcpReq.log });
120
120
  if (result.isError || !result.structuredContent) {
121
121
  return result;
122
122
  }