@steipete/oracle 0.20.2 → 0.21.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 (67) hide show
  1. package/dist/bin/oracle-cli.js +14 -42
  2. package/dist/docs-site/.nojekyll +0 -0
  3. package/dist/docs-site/CNAME +1 -0
  4. package/dist/docs-site/RELEASING.html +403 -0
  5. package/dist/docs-site/advisor.html +317 -0
  6. package/dist/docs-site/agents.html +375 -0
  7. package/dist/docs-site/anthropic.html +368 -0
  8. package/dist/docs-site/bridge.html +421 -0
  9. package/dist/docs-site/browser-mode.html +636 -0
  10. package/dist/docs-site/chromium-forks.html +355 -0
  11. package/dist/docs-site/cli-reference.html +347 -0
  12. package/dist/docs-site/configuration.html +464 -0
  13. package/dist/docs-site/favicon.svg +14 -0
  14. package/dist/docs-site/followup.html +375 -0
  15. package/dist/docs-site/gemini.html +387 -0
  16. package/dist/docs-site/grok.html +325 -0
  17. package/dist/docs-site/index.html +360 -0
  18. package/dist/docs-site/install.html +335 -0
  19. package/dist/docs-site/linux.html +321 -0
  20. package/dist/docs-site/llms.txt +44 -0
  21. package/dist/docs-site/manual-tests.html +621 -0
  22. package/dist/docs-site/mcp.html +412 -0
  23. package/dist/docs-site/multimodel.html +364 -0
  24. package/dist/docs-site/mythical-pro-agents.html +360 -0
  25. package/dist/docs-site/notifier.html +338 -0
  26. package/dist/docs-site/openai-endpoints.html +417 -0
  27. package/dist/docs-site/openrouter.html +344 -0
  28. package/dist/docs-site/quickstart.html +369 -0
  29. package/dist/docs-site/refactor/ux.html +321 -0
  30. package/dist/docs-site/sessions.html +397 -0
  31. package/dist/docs-site/social-card.png +0 -0
  32. package/dist/docs-site/social-card.svg +79 -0
  33. package/dist/docs-site/spec.html +363 -0
  34. package/dist/docs-site/testing.html +323 -0
  35. package/dist/docs-site/tui-debug.html +326 -0
  36. package/dist/docs-site/windows-work.html +348 -0
  37. package/dist/docs-site/windows.html +320 -0
  38. package/dist/src/browser/actions/promptComposer.js +0 -1
  39. package/dist/src/browser/chatgptConversation.js +309 -0
  40. package/dist/src/browser/config.js +2 -0
  41. package/dist/src/browser/configLogging.js +8 -0
  42. package/dist/src/browser/executor.js +19 -0
  43. package/dist/src/browser/index.js +80 -241
  44. package/dist/src/browser/provider.js +11 -0
  45. package/dist/src/browser/reattach.js +0 -1
  46. package/dist/src/browser/reattachHelpers.js +14 -15
  47. package/dist/src/browser/sessionRunner.js +7 -4
  48. package/dist/src/browser/uiWarnings.js +202 -0
  49. package/dist/src/cli/bridge/claudeConfig.js +0 -5
  50. package/dist/src/cli/browserConfig.js +1 -0
  51. package/dist/src/cli/browserDefaults.js +4 -0
  52. package/dist/src/cli/runOptions.js +2 -2
  53. package/dist/src/cli/sessionDisplay.js +0 -1
  54. package/dist/src/cli/sessionRunner.js +1 -0
  55. package/dist/src/config.js +1 -0
  56. package/dist/src/gemini-web/executor.js +11 -7
  57. package/dist/src/mcp/tools/consult.js +1 -1
  58. package/dist/src/oracle/background.js +0 -2
  59. package/dist/src/remote/client.js +8 -0
  60. package/dist/src/remote/server.js +13 -0
  61. package/dist/src/version.js +0 -1
  62. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  63. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  64. package/package.json +7 -10
  65. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  66. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  67. package/dist/scripts/check.js +0 -21
@@ -1,8 +1,9 @@
1
1
  import chalk from "chalk";
2
2
  import { formatTokenCount } from "../oracle/runUtils.js";
3
3
  import { formatFinishLine } from "../oracle/finishLine.js";
4
- import { runBrowserMode } from "../browserMode.js";
4
+ import { resolveLocalBrowserExecutor } from "./executor.js";
5
5
  import { DEFAULT_BROWSER_CONFIG } from "./config.js";
6
+ import { redactBrowserConfigForDebugLog } from "./configLogging.js";
6
7
  import { assembleBrowserPrompt, cleanupGeneratedBrowserBundles, materializeBrowserFallback, } from "./prompt.js";
7
8
  import { BrowserAutomationError, BrowserRunCancelledError } from "../oracle/errors.js";
8
9
  import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
@@ -57,7 +58,6 @@ function buildBrowserRunWarnings(args) {
57
58
  }
58
59
  export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log, signal }, deps = {}) {
59
60
  const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
60
- const executeBrowser = deps.executeBrowser ?? runBrowserMode;
61
61
  const persistRuntimeHint = deps.persistRuntimeHint ?? (() => { });
62
62
  const inputTimeoutMs = browserConfig.inputTimeoutMs ?? DEFAULT_BROWSER_CONFIG.inputTimeoutMs;
63
63
  let preparationAbandoned = false;
@@ -67,6 +67,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
67
67
  if (signal?.aborted) {
68
68
  throw new BrowserRunCancelledError();
69
69
  }
70
+ const executeBrowser = deps.executeBrowser ?? (await resolveLocalBrowserExecutor(runOptions));
70
71
  try {
71
72
  promptArtifacts = await Promise.race([
72
73
  assemblePrompt(runOptions, { cwd }).then(async (artifacts) => {
@@ -123,9 +124,9 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
123
124
  }
124
125
  async function executeAssembledBrowserSession({ runOptions, browserConfig, log, promptArtifacts, executeBrowser, persistRuntimeHint, signal, }) {
125
126
  if (runOptions.verbose) {
126
- log(chalk.dim(`[verbose] Browser config: ${JSON.stringify({
127
+ log(chalk.dim(`[verbose] Browser config: ${JSON.stringify(redactBrowserConfigForDebugLog({
127
128
  ...browserConfig,
128
- })}`));
129
+ }))}`));
129
130
  log(chalk.dim(`[verbose] Browser prompt length: ${promptArtifacts.composerText.length} chars`));
130
131
  if (promptArtifacts.attachments.length > 0) {
131
132
  const attachmentList = promptArtifacts.attachments
@@ -190,6 +191,7 @@ async function executeAssembledBrowserSession({ runOptions, browserConfig, log,
190
191
  try {
191
192
  browserResult = await executeBrowser({
192
193
  prompt: promptArtifacts.composerText,
194
+ model: runOptions.model,
193
195
  attachments: promptArtifacts.attachments,
194
196
  fallbackSubmission,
195
197
  config: executionBrowserConfig,
@@ -311,6 +313,7 @@ async function executeAssembledBrowserSession({ runOptions, browserConfig, log,
311
313
  archive: browserResult.archive,
312
314
  modelSelection,
313
315
  thinkingSelection,
316
+ providerNativeCapture: browserResult.providerNativeCapture,
314
317
  warnings,
315
318
  answerText,
316
319
  artifacts: savedArtifacts,
@@ -0,0 +1,202 @@
1
+ import { BrowserAutomationError } from "../oracle/errors.js";
2
+ const MAX_CHATGPT_UI_WARNING_CHARS = 300;
3
+ const MAX_CHATGPT_UI_WARNINGS = 3;
4
+ export function classifyChatGptUiWarningText(text) {
5
+ const normalized = text.toLowerCase();
6
+ if (/\btoo many requests\b/.test(normalized) ||
7
+ /\bsending too many requests\b/.test(normalized) ||
8
+ /\btoo quickly\b/.test(normalized) ||
9
+ /\btemporarily limited access\b/.test(normalized) ||
10
+ /\bplease wait a few minutes\b/.test(normalized) ||
11
+ /\brate limit(?:ed)?\b/.test(normalized) ||
12
+ /\bslow down\b/.test(normalized)) {
13
+ return "rate_limit";
14
+ }
15
+ if (/\btemporarily unavailable\b/.test(normalized) ||
16
+ /\bsomething went wrong\b/.test(normalized) ||
17
+ /\bfailed to generate\b/.test(normalized) ||
18
+ /\btry again later\b/.test(normalized)) {
19
+ return "temporary_unavailable";
20
+ }
21
+ if (/\bverify you are human\b/.test(normalized) ||
22
+ /\bunusual activity\b/.test(normalized) ||
23
+ /\bcloudflare\b/.test(normalized) ||
24
+ /\bchallenge\b/.test(normalized) ||
25
+ /\blogin required\b/.test(normalized) ||
26
+ /\bsign in\b/.test(normalized)) {
27
+ return "auth_or_challenge";
28
+ }
29
+ return null;
30
+ }
31
+ function sanitizeChatGptUiWarningText(text) {
32
+ return text
33
+ .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[redacted-email]")
34
+ .replace(/\b((?:access|auth|session)[-_ ]?token|token)\s*[:=]\s*["']?[^\s"',;]+/gi, "$1=[redacted]")
35
+ .replace(/\b(?:sk-(?:ant-|or-)?|xai-)[A-Za-z0-9_-]{8,}\b/g, "[redacted-token]");
36
+ }
37
+ function normalizeUiWarningCandidate(value) {
38
+ if (!value || typeof value !== "object")
39
+ return null;
40
+ const candidate = value;
41
+ const text = typeof candidate.text === "string"
42
+ ? sanitizeChatGptUiWarningText(candidate.text.replace(/\s+/g, " ").trim())
43
+ : "";
44
+ if (!text)
45
+ return null;
46
+ return {
47
+ text: text.slice(0, MAX_CHATGPT_UI_WARNING_CHARS),
48
+ source: typeof candidate.source === "string" ? candidate.source : null,
49
+ role: typeof candidate.role === "string" ? candidate.role : null,
50
+ ariaLive: typeof candidate.ariaLive === "string" ? candidate.ariaLive : null,
51
+ selector: typeof candidate.selector === "string" ? candidate.selector : null,
52
+ };
53
+ }
54
+ export async function collectChatGptUiWarnings(Runtime) {
55
+ try {
56
+ const { result } = await Runtime.evaluate({
57
+ awaitPromise: true,
58
+ returnByValue: true,
59
+ expression: `(() => {
60
+ const warningPattern = /too many requests|sending too many requests|too quickly|temporarily limited access|please wait a few minutes|rate limit|rate limited|slow down|try again later|temporarily unavailable|something went wrong|failed to generate|verify you are human|unusual activity|cloudflare|challenge|login required|sign in/i;
61
+ const selectors = [
62
+ '[role="alert"]',
63
+ '[role="status"]',
64
+ '[role="dialog"]',
65
+ '[aria-live]',
66
+ '[data-testid*="toast" i]',
67
+ '[data-testid*="banner" i]',
68
+ '[data-testid*="error" i]',
69
+ '[class*="toast" i]',
70
+ '[class*="banner" i]'
71
+ ];
72
+ const isVisible = (element) => {
73
+ if (!(element instanceof HTMLElement)) return false;
74
+ let current = element;
75
+ while (current) {
76
+ const currentStyle = window.getComputedStyle(current);
77
+ if (
78
+ !currentStyle ||
79
+ currentStyle.display === 'none' ||
80
+ currentStyle.visibility === 'hidden' ||
81
+ currentStyle.visibility === 'collapse' ||
82
+ Number.parseFloat(currentStyle.opacity || '1') === 0
83
+ ) {
84
+ return false;
85
+ }
86
+ current = current.parentElement;
87
+ }
88
+ const rect = element.getBoundingClientRect();
89
+ return rect.width > 0 && rect.height > 0;
90
+ };
91
+ const describe = (element, source, selector = null) => ({
92
+ text: (element.innerText || element.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 1000),
93
+ source,
94
+ selector,
95
+ role: element.getAttribute('role'),
96
+ ariaLive: element.getAttribute('aria-live')
97
+ });
98
+ const out = [];
99
+ const seen = new Set();
100
+ const warningContainers = [];
101
+ const overlapsWarningContainer = (element) => warningContainers.some((container) => (
102
+ container !== element && (container.contains(element) || element.contains(container))
103
+ ));
104
+ const add = (element, entry) => {
105
+ if (!entry.text || !warningPattern.test(entry.text)) return;
106
+ const key = entry.text + '|' + (entry.role || '') + '|' + (entry.ariaLive || '');
107
+ if (seen.has(key)) return;
108
+ seen.add(key);
109
+ warningContainers.push(element);
110
+ out.push(entry);
111
+ };
112
+ for (const selector of selectors) {
113
+ if (out.length >= 5) break;
114
+ let elements = [];
115
+ try {
116
+ elements = Array.from(document.querySelectorAll(selector));
117
+ } catch {
118
+ elements = [];
119
+ }
120
+ for (const element of elements) {
121
+ if (out.length >= 5) break;
122
+ if (overlapsWarningContainer(element)) continue;
123
+ if (isVisible(element)) add(element, describe(element, 'selector', selector));
124
+ }
125
+ }
126
+ return out.slice(0, 5);
127
+ })()`,
128
+ });
129
+ const rawWarnings = Array.isArray(result?.value) ? result.value : [];
130
+ const warnings = [];
131
+ const seen = new Set();
132
+ for (const raw of rawWarnings) {
133
+ const candidate = normalizeUiWarningCandidate(raw);
134
+ if (!candidate)
135
+ continue;
136
+ const type = classifyChatGptUiWarningText(candidate.text);
137
+ if (!type)
138
+ continue;
139
+ const key = `${type}:${candidate.text}`;
140
+ if (seen.has(key))
141
+ continue;
142
+ seen.add(key);
143
+ warnings.push({
144
+ type,
145
+ message: candidate.text,
146
+ source: candidate.source,
147
+ role: candidate.role,
148
+ ariaLive: candidate.ariaLive,
149
+ selector: candidate.selector,
150
+ });
151
+ if (warnings.length >= MAX_CHATGPT_UI_WARNINGS)
152
+ break;
153
+ }
154
+ return warnings;
155
+ }
156
+ catch {
157
+ return [];
158
+ }
159
+ }
160
+ function formatChatGptUiWarningType(type) {
161
+ switch (type) {
162
+ case "rate_limit":
163
+ return "rate-limit";
164
+ case "temporary_unavailable":
165
+ return "temporary-unavailable";
166
+ case "auth_or_challenge":
167
+ return "authentication/challenge";
168
+ }
169
+ }
170
+ async function createChatGptUiWarningError(params) {
171
+ const [uiWarning] = await collectChatGptUiWarnings(params.Runtime);
172
+ if (!uiWarning)
173
+ return null;
174
+ params.logger(`[browser] ChatGPT UI warning detected (${uiWarning.type}): ${uiWarning.message}`);
175
+ return new BrowserAutomationError(`ChatGPT displayed a ${formatChatGptUiWarningType(uiWarning.type)} warning while waiting for ${params.waitTarget}: ${uiWarning.message}`, {
176
+ stage: params.stage,
177
+ code: "chatgpt-ui-warning",
178
+ uiWarning,
179
+ runtime: params.runtime,
180
+ diagnostics: params.diagnostics,
181
+ }, params.cause);
182
+ }
183
+ export async function throwChatGptUiWarningIfPresent(params) {
184
+ const error = await createChatGptUiWarningError(params);
185
+ if (error)
186
+ throw error;
187
+ }
188
+ export async function createAssistantTimeoutError(params) {
189
+ const warningError = await createChatGptUiWarningError({
190
+ Runtime: params.Runtime,
191
+ logger: params.logger,
192
+ runtime: params.runtime,
193
+ stage: "assistant-timeout",
194
+ waitTarget: "the assistant",
195
+ diagnostics: params.diagnostics,
196
+ cause: params.cause,
197
+ });
198
+ if (!warningError) {
199
+ return new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime: params.runtime, diagnostics: params.diagnostics }, params.cause);
200
+ }
201
+ return warningError;
202
+ }
@@ -31,16 +31,11 @@ export async function runBridgeClaudeConfig(options) {
31
31
  }
32
32
  export function formatClaudeMcpConfig({ oracleHomeDir, browserProfileDir, remoteHost, remoteToken, includeToken, localBrowser = false, }) {
33
33
  const env = {};
34
- // biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
35
34
  env["ORACLE_ENGINE"] = "browser";
36
- // biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
37
35
  env["ORACLE_HOME_DIR"] = oracleHomeDir;
38
- // biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
39
36
  env["ORACLE_BROWSER_PROFILE_DIR"] = browserProfileDir;
40
37
  if (remoteHost && !localBrowser) {
41
- // biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
42
38
  env["ORACLE_REMOTE_HOST"] = remoteHost;
43
- // biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
44
39
  env["ORACLE_REMOTE_TOKEN"] = includeToken ? (remoteToken ?? "<YOUR_TOKEN>") : "<YOUR_TOKEN>";
45
40
  }
46
41
  // Claude Code supports project-scoped `.mcp.json` config files:
@@ -238,6 +238,7 @@ export async function buildBrowserConfig(options) {
238
238
  ? options.browserResearch
239
239
  : "off",
240
240
  archiveConversations: options.browserArchive,
241
+ captureProviderNative: options.browserCaptureProviderNative,
241
242
  };
242
243
  }
243
244
  function assertBrowserModelAvailable(model, modelStrategy) {
@@ -111,6 +111,10 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
111
111
  browser.thinkingTime !== undefined) {
112
112
  options.browserThinkingTime = normalizeThinkingTimeLevel(browser.thinkingTime) ?? undefined;
113
113
  }
114
+ if (isUnset("browserCaptureProviderNative") &&
115
+ typeof browser.captureProviderNative === "boolean") {
116
+ options.browserCaptureProviderNative = browser.captureProviderNative;
117
+ }
114
118
  if (isUnset("browserResearch") && browser.researchMode !== undefined) {
115
119
  options.browserResearch = browser.researchMode;
116
120
  }
@@ -2,6 +2,7 @@ import { DEFAULT_MODEL, MODEL_CONFIGS } from "../oracle.js";
2
2
  import { resolveEngine } from "./engine.js";
3
3
  import { normalizeModelOption, inferModelFromLabel, resolveApiModel, normalizeBaseUrl, } from "./options.js";
4
4
  import { resolveGeminiModelId } from "../oracle/gemini.js";
5
+ import { resolveBrowserProvider } from "../browser/provider.js";
5
6
  import { resolveOverriddenApiModel } from "../oracle/modelResolver.js";
6
7
  import { PromptValidationError } from "../oracle/errors.js";
7
8
  import { normalizeChatGptModelForBrowser, isGpt6ProAlias } from "./browserConfig.js";
@@ -44,8 +45,7 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
44
45
  : resolveApiModel(entry))))
45
46
  : [apiModel];
46
47
  const browserCompatibilityModels = normalizedRequestedModels.length > 0 ? allModels : [browserModel ?? apiModel];
47
- const isBrowserCompatible = (m) => m.startsWith("gpt-") || m.startsWith("gemini");
48
- const hasNonBrowserCompatibleTarget = browserEngineRequested && browserCompatibilityModels.some((m) => !isBrowserCompatible(m));
48
+ const hasNonBrowserCompatibleTarget = browserEngineRequested && browserCompatibilityModels.some((m) => !resolveBrowserProvider(m));
49
49
  if (hasNonBrowserCompatibleTarget) {
50
50
  throw new PromptValidationError("Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.", { engine: "browser", models: allModels });
51
51
  }
@@ -482,7 +482,6 @@ export async function attachSession(sessionId, options) {
482
482
  }
483
483
  };
484
484
  await printNew();
485
- // biome-ignore lint/nursery/noUnnecessaryConditions: deliberate infinite poll
486
485
  while (true) {
487
486
  const latest = await sessionStore.readSession(sessionId);
488
487
  if (!latest) {
@@ -161,6 +161,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
161
161
  archive: result.archive,
162
162
  modelSelection: result.modelSelection,
163
163
  thinkingSelection: result.thinkingSelection,
164
+ providerNativeCapture: result.providerNativeCapture,
164
165
  warnings: browserWarnings.length > 0 ? browserWarnings : undefined,
165
166
  },
166
167
  artifacts: mergeArtifacts(sessionMeta.artifacts, mergeArtifacts(result.artifacts, outputArtifacts.artifacts)),
@@ -131,6 +131,7 @@ function sanitizeProjectConfig(config) {
131
131
  if (config.browser) {
132
132
  sanitized.browser = {};
133
133
  const browser = config.browser;
134
+ // Full-conversation retention is user-owned; never allow captureProviderNative here.
134
135
  const allowedBrowserKeys = [
135
136
  "attachRunning",
136
137
  "timeoutMs",
@@ -252,7 +252,7 @@ export function createGeminiWebExecutor(geminiOptions) {
252
252
  const startTime = Date.now();
253
253
  const log = runOptions.log;
254
254
  log?.("[gemini-web] Starting Gemini web executor (TypeScript)");
255
- const model = resolveGeminiWebModel(runOptions.config?.desiredModel, log);
255
+ const model = resolveGeminiWebModel(runOptions.model ?? runOptions.config?.desiredModel, log);
256
256
  const generateImagePath = resolveInvocationPath(geminiOptions.generateImage);
257
257
  const editImagePath = resolveInvocationPath(geminiOptions.editImage);
258
258
  const outputPath = resolveInvocationPath(geminiOptions.outputPath);
@@ -296,6 +296,7 @@ export function createGeminiWebExecutor(geminiOptions) {
296
296
  const httpClient = {
297
297
  mode: "http",
298
298
  execute: async () => {
299
+ runOptions.signal?.throwIfAborted();
299
300
  const useNoKeychainPath = Boolean(runOptions.config?.manualLogin);
300
301
  const cookieResult = await loadGeminiCookies(runOptions.config, log, {
301
302
  preferManualNoKeychain: useNoKeychainPath,
@@ -315,6 +316,9 @@ export function createGeminiWebExecutor(geminiOptions) {
315
316
  const timeoutMs = Math.min(configTimeout ?? defaultTimeoutMs, 600_000);
316
317
  const controller = new AbortController();
317
318
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
319
+ const deliverySignal = runOptions.signal
320
+ ? AbortSignal.any([controller.signal, runOptions.signal])
321
+ : controller.signal;
318
322
  let response;
319
323
  try {
320
324
  if (editImagePath) {
@@ -324,7 +328,7 @@ export function createGeminiWebExecutor(geminiOptions) {
324
328
  model,
325
329
  cookieMap: cookieResult.cookieMap,
326
330
  chatMetadata: null,
327
- signal: controller.signal,
331
+ signal: deliverySignal,
328
332
  allowModelFallback: geminiOptions.allowModelFallback,
329
333
  });
330
334
  const editPrompt = `Use image generation tool to ${prompt}`;
@@ -334,7 +338,7 @@ export function createGeminiWebExecutor(geminiOptions) {
334
338
  model,
335
339
  cookieMap: cookieResult.cookieMap,
336
340
  chatMetadata: intro.metadata,
337
- signal: controller.signal,
341
+ signal: deliverySignal,
338
342
  allowModelFallback: geminiOptions.allowModelFallback,
339
343
  });
340
344
  response = {
@@ -344,7 +348,7 @@ export function createGeminiWebExecutor(geminiOptions) {
344
348
  image_count: 0,
345
349
  };
346
350
  const resolvedOutputPath = outputPath ?? generateImagePath ?? "generated.png";
347
- const imageSave = await saveFirstGeminiImageFromOutput(out, cookieResult.cookieMap, resolvedOutputPath, controller.signal);
351
+ const imageSave = await saveFirstGeminiImageFromOutput(out, cookieResult.cookieMap, resolvedOutputPath, deliverySignal);
348
352
  response.has_images = imageSave.saved;
349
353
  response.image_count = imageSave.imageCount;
350
354
  if (!imageSave.saved) {
@@ -358,7 +362,7 @@ export function createGeminiWebExecutor(geminiOptions) {
358
362
  model,
359
363
  cookieMap: cookieResult.cookieMap,
360
364
  chatMetadata: null,
361
- signal: controller.signal,
365
+ signal: deliverySignal,
362
366
  allowModelFallback: geminiOptions.allowModelFallback,
363
367
  });
364
368
  response = {
@@ -367,7 +371,7 @@ export function createGeminiWebExecutor(geminiOptions) {
367
371
  has_images: false,
368
372
  image_count: 0,
369
373
  };
370
- const imageSave = await saveFirstGeminiImageFromOutput(out, cookieResult.cookieMap, generateImagePath, controller.signal);
374
+ const imageSave = await saveFirstGeminiImageFromOutput(out, cookieResult.cookieMap, generateImagePath, deliverySignal);
371
375
  response.has_images = imageSave.saved;
372
376
  response.image_count = imageSave.imageCount;
373
377
  if (!imageSave.saved) {
@@ -381,7 +385,7 @@ export function createGeminiWebExecutor(geminiOptions) {
381
385
  model,
382
386
  cookieMap: cookieResult.cookieMap,
383
387
  chatMetadata: null,
384
- signal: controller.signal,
388
+ signal: deliverySignal,
385
389
  allowModelFallback: geminiOptions.allowModelFallback,
386
390
  });
387
391
  response = {
@@ -49,7 +49,7 @@ const consultInputShape = {
49
49
  engine: z
50
50
  .enum(["api", "browser"])
51
51
  .optional()
52
- .describe("Execution engine. `api` uses OpenAI/other providers. `browser` automates the ChatGPT web UI (supports attachments and ChatGPT-only model labels). When omitted, Oracle follows CLI defaults: config/ORACLE_ENGINE first, then `api` when OPENAI_API_KEY is set, otherwise `browser`."),
52
+ .describe("Execution engine. `api` uses provider APIs. `browser` uses ChatGPT automation or the local Gemini web client according to the model. Picker labels apply to ChatGPT only. When omitted, Oracle follows CLI defaults: config/ORACLE_ENGINE first, then `api` when OPENAI_API_KEY is set, otherwise `browser`."),
53
53
  browserModelLabel: z
54
54
  .string()
55
55
  .optional()
@@ -64,7 +64,6 @@ async function pollBackgroundResponse(params) {
64
64
  let response = initialResponse;
65
65
  let firstCycle = true;
66
66
  let lastStatus = response.status;
67
- // biome-ignore lint/nursery/noUnnecessaryConditions: intentional polling loop.
68
67
  while (true) {
69
68
  const status = response.status ?? "completed";
70
69
  // firstCycle toggles immediately; keep for clarity in logs.
@@ -109,7 +108,6 @@ async function pollBackgroundResponse(params) {
109
108
  async function retrieveBackgroundResponseWithRetry(params) {
110
109
  const { client, responseId, wait, now, maxWaitMs, startMark, log } = params;
111
110
  let retries = 0;
112
- // biome-ignore lint/nursery/noUnnecessaryConditions: intentional retry loop
113
111
  while (true) {
114
112
  try {
115
113
  const next = await client.responses.retrieve(responseId);
@@ -11,9 +11,17 @@ import { checkRemoteHealth } from "./health.js";
11
11
  import { parseHostPort } from "../bridge/connection.js";
12
12
  import { BrowserRunCancelledError } from "../oracle/errors.js";
13
13
  import { resolveSiblingImagePath } from "../browser/chatgptImages.js";
14
+ import { resolveBrowserProvider, REMOTE_GEMINI_UNSUPPORTED_MESSAGE } from "../browser/provider.js";
14
15
  export function createRemoteBrowserExecutor({ host, token }) {
15
16
  // Return a drop-in replacement for runBrowserMode so the browser session runner can stay unchanged.
16
17
  return async function remoteBrowserExecutor(options) {
18
+ if (options.model !== undefined && !resolveBrowserProvider(options.model)) {
19
+ throw new Error(`Unsupported browser model: ${options.model}. Remote services support ChatGPT only.`);
20
+ }
21
+ if (resolveBrowserProvider(options.model) === "gemini" ||
22
+ resolveBrowserProvider(options.config?.desiredModel) === "gemini") {
23
+ throw new Error(REMOTE_GEMINI_UNSUPPORTED_MESSAGE);
24
+ }
17
25
  if (options.config?.researchMode === "search") {
18
26
  throw new Error("Web Search is a local browser pilot; --remote-host does not negotiate this capability yet. Use local Chrome or --browser-attach-running.");
19
27
  }
@@ -19,6 +19,7 @@ import { getCookies } from "@steipete/sweet-cookie";
19
19
  import { CHATGPT_URL } from "../browser/constants.js";
20
20
  import { getCliVersion } from "../version.js";
21
21
  import { getOracleHomeDir } from "../oracleHome.js";
22
+ import { resolveBrowserProvider, REMOTE_GEMINI_UNSUPPORTED_MESSAGE } from "../browser/provider.js";
22
23
  import { cleanupStaleProfileState, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "../browser/profileState.js";
23
24
  import { normalizeChatgptUrl } from "../browser/utils.js";
24
25
  import { computeFileSha256, resolveSessionArtifactsDir, sanitizeArtifactFilename, sanitizeArtifactMimeType, validateArtifactFile, } from "../browser/artifacts.js";
@@ -230,6 +231,17 @@ export async function createRemoteServer(options = {}, deps = {}) {
230
231
  await abandon();
231
232
  return;
232
233
  }
234
+ if (resolveBrowserProvider(payload.browserConfig?.desiredModel) === "gemini") {
235
+ await abandon();
236
+ if (!res.destroyed) {
237
+ res.writeHead(400, { "Content-Type": "application/json" });
238
+ res.end(JSON.stringify({
239
+ error: "unsupported_browser_provider",
240
+ message: REMOTE_GEMINI_UNSUPPORTED_MESSAGE,
241
+ }));
242
+ }
243
+ return;
244
+ }
233
245
  if (!admissionEnabled) {
234
246
  signal = payload.options.cancelOnDisconnect === true ? controller.signal : undefined;
235
247
  reserve();
@@ -855,6 +867,7 @@ function sanitizeResult(result, warnings = []) {
855
867
  answerChars: result.answerChars,
856
868
  modelSelection: result.modelSelection,
857
869
  thinkingSelection: result.thinkingSelection,
870
+ providerNativeCapture: result.providerNativeCapture,
858
871
  researchPlan: result.researchPlan,
859
872
  archive: result.archive,
860
873
  tabUrl: result.tabUrl,
@@ -13,7 +13,6 @@ function readVersionFromPackage() {
13
13
  const modulePath = fileURLToPath(import.meta.url);
14
14
  let currentDir = path.dirname(modulePath);
15
15
  const filesystemRoot = path.parse(currentDir).root;
16
- // biome-ignore lint/nursery/noUnnecessaryConditions: deliberate sentinel loop to walk up directories
17
16
  while (true) {
18
17
  const candidate = path.join(currentDir, "package.json");
19
18
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.20.2",
3
+ "version": "0.21.0",
4
4
  "description": "CLI wrapper around OpenAI Responses API with GPT-5.6 Sol, GPT-5.6, GPT-5.5 Pro, GPT-5.5, GPT-5.4, GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
5
5
  "keywords": [],
6
6
  "homepage": "https://askoracle.sh",
@@ -58,8 +58,7 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@anthropic-ai/tokenizer": "^0.0.4",
61
- "@google/genai": "^2.21.0",
62
- "@google/generative-ai": "^0.24.1",
61
+ "@google/genai": "^2.22.0",
63
62
  "@modelcontextprotocol/server": "^2.0.0",
64
63
  "@steipete/sweet-cookie": "^0.4.3",
65
64
  "chalk": "^6.0.0",
@@ -74,23 +73,21 @@
74
73
  "json5": "^2.2.3",
75
74
  "kleur": "^4.1.5",
76
75
  "markdansi": "0.3.3",
77
- "openai": "^7.13.0",
76
+ "openai": "^7.15.0",
78
77
  "osc-progress": "^0.3.3",
79
78
  "qs": "^6.16.0",
80
79
  "shiki": "^4.4.3",
81
80
  "toasted-notifier": "^10.1.0",
82
81
  "tokentally": "^0.1.6",
83
- "zod": "^4.6.1"
82
+ "zod": "^4.6.2"
84
83
  },
85
84
  "devDependencies": {
86
- "@anthropic-ai/tokenizer": "^0.0.4",
87
85
  "@modelcontextprotocol/client": "^2.0.0",
88
86
  "@modelcontextprotocol/sdk": "^1.30.0",
89
87
  "@types/chrome-remote-interface": "^0.34.0",
90
- "@types/inquirer": "^9.0.10",
91
88
  "@types/node": "^26.5.1",
92
89
  "@vitest/coverage-v8": "5.0.0",
93
- "devtools-protocol": "0.0.1694333",
90
+ "devtools-protocol": "0.0.1695952",
94
91
  "es-toolkit": "^1.52.0",
95
92
  "esbuild": "^0.28.2",
96
93
  "oxfmt": "0.67.0",
@@ -98,7 +95,7 @@
98
95
  "puppeteer-core": "^25.10.0",
99
96
  "tsx": "^4.23.13",
100
97
  "typescript": "^7.0.2",
101
- "vitest": "^5.0.0"
98
+ "vitest": "5.0.0"
102
99
  },
103
100
  "devEngines": {
104
101
  "runtime": [
@@ -111,5 +108,5 @@
111
108
  "engines": {
112
109
  "node": ">=24"
113
110
  },
114
- "packageManager": "pnpm@11.25.0"
111
+ "packageManager": "pnpm@11.26.0"
115
112
  }
@@ -1,21 +0,0 @@
1
- #!/usr/bin/env bun
2
- import process from "node:process";
3
- const buildConfig = {
4
- entrypoints: ["./bin/oracle-cli.js"],
5
- outdir: "./.bun-check",
6
- target: "bun",
7
- minify: false,
8
- write: false,
9
- };
10
- const result = await Bun.build(buildConfig);
11
- if (!result.success) {
12
- console.error("Build failed while checking syntax:");
13
- for (const log of result.logs) {
14
- console.error(log.message);
15
- if (log.position) {
16
- console.error(`\tat ${log.position.file}:${log.position.line}:${log.position.column}`);
17
- }
18
- }
19
- process.exit(1);
20
- }
21
- console.log("Syntax OK");