@steipete/oracle 0.8.6 → 0.9.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 +76 -4
- package/dist/bin/oracle-cli.js +188 -7
- package/dist/src/browser/actions/modelSelection.js +60 -8
- package/dist/src/browser/actions/navigation.js +2 -1
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +73 -19
- package/dist/src/browser/providerDomFlow.js +17 -0
- package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +245 -0
- package/dist/src/browser/providers/index.js +2 -0
- package/dist/src/cli/browserConfig.js +12 -6
- package/dist/src/cli/detach.js +5 -2
- package/dist/src/cli/fileSize.js +11 -0
- package/dist/src/cli/help.js +3 -3
- package/dist/src/cli/markdownBundle.js +5 -1
- package/dist/src/cli/options.js +40 -3
- package/dist/src/cli/runOptions.js +11 -3
- package/dist/src/cli/sessionDisplay.js +91 -2
- package/dist/src/cli/sessionLineage.js +56 -0
- package/dist/src/cli/sessionRunner.js +20 -2
- package/dist/src/cli/sessionTable.js +2 -1
- package/dist/src/cli/tui/index.js +2 -0
- package/dist/src/gemini-web/browserSessionManager.js +76 -0
- package/dist/src/gemini-web/client.js +16 -5
- package/dist/src/gemini-web/executionClients.js +1 -0
- package/dist/src/gemini-web/executionMode.js +18 -0
- package/dist/src/gemini-web/executor.js +273 -120
- package/dist/src/mcp/tools/consult.js +34 -21
- package/dist/src/oracle/client.js +42 -13
- package/dist/src/oracle/config.js +43 -7
- package/dist/src/oracle/errors.js +2 -2
- package/dist/src/oracle/files.js +20 -5
- package/dist/src/oracle/gemini.js +3 -0
- package/dist/src/oracle/request.js +7 -2
- package/dist/src/oracle/run.js +22 -12
- package/dist/src/sessionManager.js +4 -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 +18 -18
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { getCookies } from '@steipete/sweet-cookie';
|
|
3
|
+
import { runProviderDomFlow } from '../browser/providerDomFlow.js';
|
|
4
|
+
import { delay } from '../browser/utils.js';
|
|
3
5
|
import { runGeminiWebWithFallback, saveFirstGeminiImageFromOutput } from './client.js';
|
|
6
|
+
import { geminiDeepThinkDomProvider } from '../browser/providers/index.js';
|
|
7
|
+
import { openGeminiBrowserSession } from './browserSessionManager.js';
|
|
8
|
+
import { selectGeminiExecutionMode } from './executionMode.js';
|
|
4
9
|
const GEMINI_COOKIE_NAMES = [
|
|
5
10
|
'__Secure-1PSID',
|
|
6
11
|
'__Secure-1PSIDTS',
|
|
@@ -37,16 +42,21 @@ function resolveGeminiWebModel(desiredModel, log) {
|
|
|
37
42
|
const desired = typeof desiredModel === 'string' ? desiredModel.trim() : '';
|
|
38
43
|
if (!desired)
|
|
39
44
|
return 'gemini-3-pro';
|
|
40
|
-
|
|
45
|
+
const normalized = desired.toLowerCase().replace(/[_\s]+/g, '-');
|
|
46
|
+
switch (normalized) {
|
|
41
47
|
case 'gemini-3-pro':
|
|
42
48
|
case 'gemini-3.0-pro':
|
|
43
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';
|
|
44
54
|
case 'gemini-2.5-pro':
|
|
45
55
|
return 'gemini-2.5-pro';
|
|
46
56
|
case 'gemini-2.5-flash':
|
|
47
57
|
return 'gemini-2.5-flash';
|
|
48
58
|
default:
|
|
49
|
-
if (
|
|
59
|
+
if (normalized.startsWith('gemini-') || normalized.includes('gemini')) {
|
|
50
60
|
log?.(`[gemini-web] Unsupported Gemini web model "${desired}". Falling back to gemini-3-pro.`);
|
|
51
61
|
}
|
|
52
62
|
return 'gemini-3-pro';
|
|
@@ -91,10 +101,97 @@ function buildGeminiCookieMap(cookies) {
|
|
|
91
101
|
function hasRequiredGeminiCookies(cookieMap) {
|
|
92
102
|
return GEMINI_REQUIRED_COOKIES.every((name) => Boolean(cookieMap[name]));
|
|
93
103
|
}
|
|
104
|
+
const GEMINI_CDP_COOKIE_URLS = [
|
|
105
|
+
'https://gemini.google.com',
|
|
106
|
+
'https://accounts.google.com',
|
|
107
|
+
'https://www.google.com',
|
|
108
|
+
];
|
|
109
|
+
async function loadGeminiCookiesFromCDP(browserConfig, log) {
|
|
110
|
+
const session = await openGeminiBrowserSession({
|
|
111
|
+
browserConfig,
|
|
112
|
+
keepBrowserDefault: false,
|
|
113
|
+
purpose: 'Gemini manual-login cookie extraction (no keychain)',
|
|
114
|
+
log,
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
const client = session.client;
|
|
118
|
+
const { Network, Page } = client;
|
|
119
|
+
await Network.enable({});
|
|
120
|
+
await Page.enable();
|
|
121
|
+
log?.('[gemini-web] Navigating to gemini.google.com for sign-in/cookie capture...');
|
|
122
|
+
await Page.navigate({ url: 'https://gemini.google.com' });
|
|
123
|
+
await delay(2_000);
|
|
124
|
+
const pollTimeoutMs = 5 * 60_000;
|
|
125
|
+
const pollIntervalMs = 2_000;
|
|
126
|
+
const deadline = Date.now() + pollTimeoutMs;
|
|
127
|
+
let lastNotice = 0;
|
|
128
|
+
let cookieMap = {};
|
|
129
|
+
while (Date.now() < deadline) {
|
|
130
|
+
const { cookies } = await Network.getCookies({ urls: GEMINI_CDP_COOKIE_URLS });
|
|
131
|
+
cookieMap = buildGeminiCookieMap(cookies);
|
|
132
|
+
if (hasRequiredGeminiCookies(cookieMap)) {
|
|
133
|
+
log?.(`[gemini-web] Extracted ${Object.keys(cookieMap).length} Gemini cookie(s) via CDP.`);
|
|
134
|
+
return { cookieMap, warnings: [] };
|
|
135
|
+
}
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
if (now - lastNotice > 10_000) {
|
|
138
|
+
log?.('[gemini-web] Waiting for Google sign-in... please sign in in the opened Chrome window.');
|
|
139
|
+
lastNotice = now;
|
|
140
|
+
}
|
|
141
|
+
await delay(pollIntervalMs);
|
|
142
|
+
}
|
|
143
|
+
throw new Error('Timed out waiting for Google sign-in (5 minutes). Please sign in and retry.');
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
await session.close();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function runGeminiDeepThinkViaBrowser(prompt, browserConfig, log) {
|
|
150
|
+
const session = await openGeminiBrowserSession({
|
|
151
|
+
browserConfig,
|
|
152
|
+
keepBrowserDefault: true,
|
|
153
|
+
purpose: 'Gemini Deep Think',
|
|
154
|
+
log,
|
|
155
|
+
});
|
|
156
|
+
try {
|
|
157
|
+
const client = session.client;
|
|
158
|
+
const { Runtime, Page } = client;
|
|
159
|
+
if (!Runtime || typeof Runtime.enable !== 'function' || typeof Runtime.evaluate !== 'function') {
|
|
160
|
+
throw new Error('Chrome Runtime domain unavailable for Gemini Deep Think DOM automation.');
|
|
161
|
+
}
|
|
162
|
+
if (!Page || typeof Page.enable !== 'function' || typeof Page.navigate !== 'function') {
|
|
163
|
+
throw new Error('Chrome Page domain unavailable for Gemini Deep Think DOM automation.');
|
|
164
|
+
}
|
|
165
|
+
await Runtime.enable();
|
|
166
|
+
await Page.enable();
|
|
167
|
+
const evaluate = async (expression) => {
|
|
168
|
+
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
169
|
+
return result?.value;
|
|
170
|
+
};
|
|
171
|
+
log?.('[gemini-web] Navigating to gemini.google.com...');
|
|
172
|
+
await Page.navigate({ url: 'https://gemini.google.com/app' });
|
|
173
|
+
await delay(3_000);
|
|
174
|
+
const domResult = await runProviderDomFlow(geminiDeepThinkDomProvider, {
|
|
175
|
+
prompt,
|
|
176
|
+
evaluate,
|
|
177
|
+
delay,
|
|
178
|
+
log,
|
|
179
|
+
state: {
|
|
180
|
+
inputTimeoutMs: browserConfig?.inputTimeoutMs,
|
|
181
|
+
timeoutMs: browserConfig?.timeoutMs,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
log?.(`[gemini-web] Deep Think response received (${domResult.text.length} chars).`);
|
|
185
|
+
return domResult;
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
await session.close();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
94
191
|
async function loadGeminiCookiesFromInline(browserConfig, log) {
|
|
95
192
|
const inline = browserConfig?.inlineCookies;
|
|
96
193
|
if (!inline || inline.length === 0)
|
|
97
|
-
return {};
|
|
194
|
+
return { cookieMap: {}, warnings: [] };
|
|
98
195
|
const cookieMap = buildGeminiCookieMap(inline.filter((cookie) => Boolean(cookie?.name && typeof cookie.value === 'string')));
|
|
99
196
|
if (Object.keys(cookieMap).length > 0) {
|
|
100
197
|
const source = browserConfig?.inlineCookiesSource ?? 'inline';
|
|
@@ -103,7 +200,7 @@ async function loadGeminiCookiesFromInline(browserConfig, log) {
|
|
|
103
200
|
else {
|
|
104
201
|
log?.('[gemini-web] Inline cookie payload provided but no Gemini cookies matched.');
|
|
105
202
|
}
|
|
106
|
-
return cookieMap;
|
|
203
|
+
return { cookieMap, warnings: [] };
|
|
107
204
|
}
|
|
108
205
|
async function loadGeminiCookiesFromChrome(browserConfig, log) {
|
|
109
206
|
try {
|
|
@@ -131,47 +228,52 @@ async function loadGeminiCookiesFromChrome(browserConfig, log) {
|
|
|
131
228
|
}
|
|
132
229
|
const cookieMap = buildGeminiCookieMap(cookies);
|
|
133
230
|
log?.(`[gemini-web] Loaded Gemini cookies from Chrome (node): ${Object.keys(cookieMap).length} cookie(s).`);
|
|
134
|
-
return cookieMap;
|
|
231
|
+
return { cookieMap, warnings };
|
|
135
232
|
}
|
|
136
233
|
catch (error) {
|
|
137
234
|
log?.(`[gemini-web] Failed to load Chrome cookies via node: ${error instanceof Error ? error.message : String(error ?? '')}`);
|
|
138
|
-
return {};
|
|
235
|
+
return { cookieMap: {}, warnings: [] };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function formatGeminiCookieError(warnings) {
|
|
239
|
+
const base = 'Gemini browser mode requires Chrome cookies for google.com (missing __Secure-1PSID/__Secure-1PSIDTS).';
|
|
240
|
+
const guidance = 'Try --browser-manual-login or --browser-inline-cookies-file if local cookie extraction is unavailable.';
|
|
241
|
+
if (warnings.length === 0) {
|
|
242
|
+
return `${base} ${guidance}`;
|
|
139
243
|
}
|
|
244
|
+
return `${base}\nCookie read warnings:\n- ${warnings.join('\n- ')}\n${guidance}`;
|
|
140
245
|
}
|
|
141
|
-
async function loadGeminiCookies(browserConfig, log) {
|
|
142
|
-
const
|
|
143
|
-
const hasInlineRequired = hasRequiredGeminiCookies(
|
|
144
|
-
if (hasInlineRequired
|
|
145
|
-
return
|
|
246
|
+
async function loadGeminiCookies(browserConfig, log, options) {
|
|
247
|
+
const inlineResult = await loadGeminiCookiesFromInline(browserConfig, log);
|
|
248
|
+
const hasInlineRequired = hasRequiredGeminiCookies(inlineResult.cookieMap);
|
|
249
|
+
if (hasInlineRequired) {
|
|
250
|
+
return inlineResult;
|
|
251
|
+
}
|
|
252
|
+
const manualNoKeychain = Boolean(browserConfig?.manualLogin) || Boolean(options?.preferManualNoKeychain);
|
|
253
|
+
if (manualNoKeychain) {
|
|
254
|
+
log?.('[gemini-web] Using manual-login cookie extraction path (no keychain cookie read).');
|
|
255
|
+
const cdpResult = await loadGeminiCookiesFromCDP(browserConfig, log);
|
|
256
|
+
return {
|
|
257
|
+
cookieMap: { ...cdpResult.cookieMap, ...inlineResult.cookieMap },
|
|
258
|
+
warnings: [...inlineResult.warnings, ...cdpResult.warnings],
|
|
259
|
+
};
|
|
146
260
|
}
|
|
147
261
|
if (browserConfig?.cookieSync === false && !hasInlineRequired) {
|
|
148
262
|
log?.('[gemini-web] Cookie sync disabled and inline cookies missing Gemini auth tokens.');
|
|
149
|
-
return
|
|
263
|
+
return inlineResult;
|
|
150
264
|
}
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
265
|
+
const chromeResult = await loadGeminiCookiesFromChrome(browserConfig, log);
|
|
266
|
+
return {
|
|
267
|
+
cookieMap: { ...chromeResult.cookieMap, ...inlineResult.cookieMap },
|
|
268
|
+
warnings: [...inlineResult.warnings, ...chromeResult.warnings],
|
|
269
|
+
};
|
|
154
270
|
}
|
|
155
271
|
export function createGeminiWebExecutor(geminiOptions) {
|
|
156
272
|
return async (runOptions) => {
|
|
157
273
|
const startTime = Date.now();
|
|
158
274
|
const log = runOptions.log;
|
|
159
275
|
log?.('[gemini-web] Starting Gemini web executor (TypeScript)');
|
|
160
|
-
const
|
|
161
|
-
if (!hasRequiredGeminiCookies(cookieMap)) {
|
|
162
|
-
throw new Error('Gemini browser mode requires Chrome cookies for google.com (missing __Secure-1PSID/__Secure-1PSIDTS).');
|
|
163
|
-
}
|
|
164
|
-
const configTimeout = typeof runOptions.config?.timeoutMs === 'number' && Number.isFinite(runOptions.config.timeoutMs)
|
|
165
|
-
? Math.max(1_000, runOptions.config.timeoutMs)
|
|
166
|
-
: null;
|
|
167
|
-
const defaultTimeoutMs = geminiOptions.youtube
|
|
168
|
-
? 240_000
|
|
169
|
-
: geminiOptions.generateImage || geminiOptions.editImage
|
|
170
|
-
? 300_000
|
|
171
|
-
: 120_000;
|
|
172
|
-
const timeoutMs = Math.min(configTimeout ?? defaultTimeoutMs, 600_000);
|
|
173
|
-
const controller = new AbortController();
|
|
174
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
276
|
+
const model = resolveGeminiWebModel(runOptions.config?.desiredModel, log);
|
|
175
277
|
const generateImagePath = resolveInvocationPath(geminiOptions.generateImage);
|
|
176
278
|
const editImagePath = resolveInvocationPath(geminiOptions.editImage);
|
|
177
279
|
const outputPath = resolveInvocationPath(geminiOptions.outputPath);
|
|
@@ -186,100 +288,151 @@ export function createGeminiWebExecutor(geminiOptions) {
|
|
|
186
288
|
if (generateImagePath && !editImagePath) {
|
|
187
289
|
prompt = `Generate an image: ${prompt}`;
|
|
188
290
|
}
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
files: attachmentPaths,
|
|
205
|
-
model,
|
|
206
|
-
cookieMap,
|
|
207
|
-
chatMetadata: intro.metadata,
|
|
208
|
-
signal: controller.signal,
|
|
209
|
-
});
|
|
210
|
-
response = {
|
|
211
|
-
text: out.text ?? null,
|
|
212
|
-
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
213
|
-
has_images: false,
|
|
214
|
-
image_count: 0,
|
|
215
|
-
};
|
|
216
|
-
const resolvedOutputPath = outputPath ?? generateImagePath ?? 'generated.png';
|
|
217
|
-
const imageSave = await saveFirstGeminiImageFromOutput(out, cookieMap, resolvedOutputPath, controller.signal);
|
|
218
|
-
response.has_images = imageSave.saved;
|
|
219
|
-
response.image_count = imageSave.imageCount;
|
|
220
|
-
if (!imageSave.saved) {
|
|
221
|
-
throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
|
|
291
|
+
const modeSelection = selectGeminiExecutionMode({
|
|
292
|
+
model,
|
|
293
|
+
attachmentPaths,
|
|
294
|
+
generateImagePath,
|
|
295
|
+
editImagePath,
|
|
296
|
+
});
|
|
297
|
+
const domClient = {
|
|
298
|
+
mode: 'dom',
|
|
299
|
+
execute: async () => {
|
|
300
|
+
log?.('[gemini-web] Using browser DOM automation for Deep Think.');
|
|
301
|
+
const browserResult = await runGeminiDeepThinkViaBrowser(prompt, runOptions.config, log);
|
|
302
|
+
const tookMs = Date.now() - startTime;
|
|
303
|
+
let answerMarkdown = browserResult.text;
|
|
304
|
+
if (geminiOptions.showThoughts && browserResult.thoughts) {
|
|
305
|
+
answerMarkdown = `## Thinking\n\n${browserResult.thoughts}\n\n## Response\n\n${browserResult.text}`;
|
|
222
306
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
chatMetadata: null,
|
|
231
|
-
signal: controller.signal,
|
|
232
|
-
});
|
|
233
|
-
response = {
|
|
234
|
-
text: out.text ?? null,
|
|
235
|
-
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
236
|
-
has_images: false,
|
|
237
|
-
image_count: 0,
|
|
307
|
+
log?.(`[gemini-web] Completed in ${tookMs}ms`);
|
|
308
|
+
return {
|
|
309
|
+
answerText: browserResult.text,
|
|
310
|
+
answerMarkdown,
|
|
311
|
+
tookMs,
|
|
312
|
+
answerTokens: estimateTokenCount(browserResult.text),
|
|
313
|
+
answerChars: browserResult.text.length,
|
|
238
314
|
};
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
const httpClient = {
|
|
318
|
+
mode: 'http',
|
|
319
|
+
execute: async () => {
|
|
320
|
+
const useNoKeychainPath = Boolean(runOptions.config?.manualLogin);
|
|
321
|
+
const cookieResult = await loadGeminiCookies(runOptions.config, log, { preferManualNoKeychain: useNoKeychainPath });
|
|
322
|
+
if (!hasRequiredGeminiCookies(cookieResult.cookieMap)) {
|
|
323
|
+
throw new Error(formatGeminiCookieError(cookieResult.warnings));
|
|
244
324
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
325
|
+
const configTimeout = typeof runOptions.config?.timeoutMs === 'number' && Number.isFinite(runOptions.config.timeoutMs)
|
|
326
|
+
? Math.max(1_000, runOptions.config.timeoutMs)
|
|
327
|
+
: null;
|
|
328
|
+
const defaultTimeoutMs = geminiOptions.youtube
|
|
329
|
+
? 240_000
|
|
330
|
+
: geminiOptions.generateImage || geminiOptions.editImage
|
|
331
|
+
? 300_000
|
|
332
|
+
: 120_000;
|
|
333
|
+
const timeoutMs = Math.min(configTimeout ?? defaultTimeoutMs, 600_000);
|
|
334
|
+
const controller = new AbortController();
|
|
335
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
336
|
+
let response;
|
|
337
|
+
try {
|
|
338
|
+
if (editImagePath) {
|
|
339
|
+
const intro = await runGeminiWebWithFallback({
|
|
340
|
+
prompt: 'Here is an image to edit',
|
|
341
|
+
files: [editImagePath],
|
|
342
|
+
model,
|
|
343
|
+
cookieMap: cookieResult.cookieMap,
|
|
344
|
+
chatMetadata: null,
|
|
345
|
+
signal: controller.signal,
|
|
346
|
+
});
|
|
347
|
+
const editPrompt = `Use image generation tool to ${prompt}`;
|
|
348
|
+
const out = await runGeminiWebWithFallback({
|
|
349
|
+
prompt: editPrompt,
|
|
350
|
+
files: attachmentPaths,
|
|
351
|
+
model,
|
|
352
|
+
cookieMap: cookieResult.cookieMap,
|
|
353
|
+
chatMetadata: intro.metadata,
|
|
354
|
+
signal: controller.signal,
|
|
355
|
+
});
|
|
356
|
+
response = {
|
|
357
|
+
text: out.text ?? null,
|
|
358
|
+
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
359
|
+
has_images: false,
|
|
360
|
+
image_count: 0,
|
|
361
|
+
};
|
|
362
|
+
const resolvedOutputPath = outputPath ?? generateImagePath ?? 'generated.png';
|
|
363
|
+
const imageSave = await saveFirstGeminiImageFromOutput(out, cookieResult.cookieMap, resolvedOutputPath, controller.signal);
|
|
364
|
+
response.has_images = imageSave.saved;
|
|
365
|
+
response.image_count = imageSave.imageCount;
|
|
366
|
+
if (!imageSave.saved) {
|
|
367
|
+
throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else if (generateImagePath) {
|
|
371
|
+
const out = await runGeminiWebWithFallback({
|
|
372
|
+
prompt,
|
|
373
|
+
files: attachmentPaths,
|
|
374
|
+
model,
|
|
375
|
+
cookieMap: cookieResult.cookieMap,
|
|
376
|
+
chatMetadata: null,
|
|
377
|
+
signal: controller.signal,
|
|
378
|
+
});
|
|
379
|
+
response = {
|
|
380
|
+
text: out.text ?? null,
|
|
381
|
+
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
382
|
+
has_images: false,
|
|
383
|
+
image_count: 0,
|
|
384
|
+
};
|
|
385
|
+
const imageSave = await saveFirstGeminiImageFromOutput(out, cookieResult.cookieMap, generateImagePath, controller.signal);
|
|
386
|
+
response.has_images = imageSave.saved;
|
|
387
|
+
response.image_count = imageSave.imageCount;
|
|
388
|
+
if (!imageSave.saved) {
|
|
389
|
+
throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
const out = await runGeminiWebWithFallback({
|
|
394
|
+
prompt,
|
|
395
|
+
files: attachmentPaths,
|
|
396
|
+
model,
|
|
397
|
+
cookieMap: cookieResult.cookieMap,
|
|
398
|
+
chatMetadata: null,
|
|
399
|
+
signal: controller.signal,
|
|
400
|
+
});
|
|
401
|
+
response = {
|
|
402
|
+
text: out.text ?? null,
|
|
403
|
+
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
404
|
+
has_images: out.images.length > 0,
|
|
405
|
+
image_count: out.images.length,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
clearTimeout(timeout);
|
|
411
|
+
}
|
|
412
|
+
const answerText = response.text ?? '';
|
|
413
|
+
let answerMarkdown = answerText;
|
|
414
|
+
if (geminiOptions.showThoughts && response.thoughts) {
|
|
415
|
+
answerMarkdown = `## Thinking\n\n${response.thoughts}\n\n## Response\n\n${answerText}`;
|
|
416
|
+
}
|
|
417
|
+
if (response.has_images && response.image_count > 0) {
|
|
418
|
+
const imagePath = generateImagePath || outputPath || 'generated.png';
|
|
419
|
+
answerMarkdown += `\n\n*Generated ${response.image_count} image(s). Saved to: ${imagePath}*`;
|
|
420
|
+
}
|
|
421
|
+
const tookMs = Date.now() - startTime;
|
|
422
|
+
log?.(`[gemini-web] Completed in ${tookMs}ms`);
|
|
423
|
+
return {
|
|
424
|
+
answerText,
|
|
425
|
+
answerMarkdown,
|
|
426
|
+
tookMs,
|
|
427
|
+
answerTokens: estimateTokenCount(answerText),
|
|
428
|
+
answerChars: answerText.length,
|
|
260
429
|
};
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
finally {
|
|
264
|
-
clearTimeout(timeout);
|
|
265
|
-
}
|
|
266
|
-
const answerText = response.text ?? '';
|
|
267
|
-
let answerMarkdown = answerText;
|
|
268
|
-
if (geminiOptions.showThoughts && response.thoughts) {
|
|
269
|
-
answerMarkdown = `## Thinking\n\n${response.thoughts}\n\n## Response\n\n${answerText}`;
|
|
270
|
-
}
|
|
271
|
-
if (response.has_images && response.image_count > 0) {
|
|
272
|
-
const imagePath = generateImagePath || outputPath || 'generated.png';
|
|
273
|
-
answerMarkdown += `\n\n*Generated ${response.image_count} image(s). Saved to: ${imagePath}*`;
|
|
274
|
-
}
|
|
275
|
-
const tookMs = Date.now() - startTime;
|
|
276
|
-
log?.(`[gemini-web] Completed in ${tookMs}ms`);
|
|
277
|
-
return {
|
|
278
|
-
answerText,
|
|
279
|
-
answerMarkdown,
|
|
280
|
-
tookMs,
|
|
281
|
-
answerTokens: estimateTokenCount(answerText),
|
|
282
|
-
answerChars: answerText.length,
|
|
430
|
+
},
|
|
283
431
|
};
|
|
432
|
+
if (model === 'gemini-3-pro-deep-think' && modeSelection.mode === 'http') {
|
|
433
|
+
log?.(`[gemini-web] Deep Think DOM path skipped (${modeSelection.reasons.join(', ')} requested); using HTTP/header fallback path.`);
|
|
434
|
+
}
|
|
435
|
+
const executionClient = modeSelection.mode === 'dom' ? domClient : httpClient;
|
|
436
|
+
return executionClient.execute();
|
|
284
437
|
};
|
|
285
438
|
}
|
|
@@ -139,6 +139,31 @@ export function summarizeModelRunsForConsult(runs) {
|
|
|
139
139
|
};
|
|
140
140
|
});
|
|
141
141
|
}
|
|
142
|
+
export function buildConsultBrowserConfig({ userConfig, env, runModel, inputModel, browserModelLabel, browserThinkingTime, browserKeepBrowser, }) {
|
|
143
|
+
const configuredBrowser = userConfig.browser ?? {};
|
|
144
|
+
const envProfileDir = (env.ORACLE_BROWSER_PROFILE_DIR ?? '').trim();
|
|
145
|
+
const hasProfileDir = envProfileDir.length > 0;
|
|
146
|
+
const preferredLabel = (browserModelLabel ?? inputModel)?.trim();
|
|
147
|
+
const isChatGptModel = runModel.startsWith('gpt-') && !runModel.includes('codex');
|
|
148
|
+
const desiredModelLabel = isChatGptModel
|
|
149
|
+
? mapModelToBrowserLabel(runModel)
|
|
150
|
+
: resolveBrowserModelLabel(preferredLabel, runModel);
|
|
151
|
+
const configuredUrl = configuredBrowser.chatgptUrl ?? configuredBrowser.url ?? CHATGPT_URL;
|
|
152
|
+
const manualLogin = hasProfileDir ? true : configuredBrowser.manualLogin ?? false;
|
|
153
|
+
return {
|
|
154
|
+
...configuredBrowser,
|
|
155
|
+
url: configuredUrl,
|
|
156
|
+
chatgptUrl: configuredUrl,
|
|
157
|
+
cookieSync: !manualLogin,
|
|
158
|
+
headless: configuredBrowser.headless ?? false,
|
|
159
|
+
hideWindow: configuredBrowser.hideWindow ?? false,
|
|
160
|
+
keepBrowser: browserKeepBrowser ?? configuredBrowser.keepBrowser ?? false,
|
|
161
|
+
manualLogin,
|
|
162
|
+
manualLoginProfileDir: manualLogin ? ((envProfileDir || configuredBrowser.manualLoginProfileDir) ?? null) : null,
|
|
163
|
+
thinkingTime: browserThinkingTime ?? configuredBrowser.thinkingTime,
|
|
164
|
+
desiredModel: desiredModelLabel || mapModelToBrowserLabel(runModel),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
142
167
|
export function registerConsultTool(server) {
|
|
143
168
|
server.registerTool('consult', {
|
|
144
169
|
title: 'Run an oracle session',
|
|
@@ -185,27 +210,15 @@ export function registerConsultTool(server) {
|
|
|
185
210
|
}
|
|
186
211
|
let browserConfig;
|
|
187
212
|
if (resolvedEngine === 'browser') {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const manualLogin = hasProfileDir;
|
|
198
|
-
browserConfig = {
|
|
199
|
-
url: configuredUrl ?? CHATGPT_URL,
|
|
200
|
-
cookieSync: !manualLogin,
|
|
201
|
-
headless: false,
|
|
202
|
-
hideWindow: false,
|
|
203
|
-
keepBrowser: browserKeepBrowser ?? false,
|
|
204
|
-
manualLogin,
|
|
205
|
-
manualLoginProfileDir: manualLogin ? envProfileDir : null,
|
|
206
|
-
thinkingTime: browserThinkingTime,
|
|
207
|
-
desiredModel: desiredModelLabel || mapModelToBrowserLabel(runOptions.model),
|
|
208
|
-
};
|
|
213
|
+
browserConfig = buildConsultBrowserConfig({
|
|
214
|
+
userConfig,
|
|
215
|
+
env: process.env,
|
|
216
|
+
runModel: runOptions.model,
|
|
217
|
+
inputModel: model,
|
|
218
|
+
browserModelLabel,
|
|
219
|
+
browserThinkingTime,
|
|
220
|
+
browserKeepBrowser,
|
|
221
|
+
});
|
|
209
222
|
}
|
|
210
223
|
const notifications = resolveNotificationSettings({
|
|
211
224
|
cliNotify: undefined,
|
|
@@ -1,34 +1,63 @@
|
|
|
1
|
-
import OpenAI
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { createGeminiClient } from './gemini.js';
|
|
5
5
|
import { createClaudeClient } from './claude.js';
|
|
6
6
|
import { isOpenRouterBaseUrl } from './modelResolver.js';
|
|
7
|
+
/**
|
|
8
|
+
* Known native API base URLs that should still use their dedicated SDKs.
|
|
9
|
+
* Any other custom base URL is treated as an OpenAI-compatible proxy and
|
|
10
|
+
* all models are routed through the chat/completions adapter.
|
|
11
|
+
*/
|
|
12
|
+
const NATIVE_API_HOSTS = [
|
|
13
|
+
'api.openai.com',
|
|
14
|
+
'api.anthropic.com',
|
|
15
|
+
'generativelanguage.googleapis.com',
|
|
16
|
+
'api.x.ai',
|
|
17
|
+
];
|
|
18
|
+
export function isCustomBaseUrl(baseUrl) {
|
|
19
|
+
if (!baseUrl)
|
|
20
|
+
return false;
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(baseUrl);
|
|
23
|
+
return !NATIVE_API_HOSTS.some((host) => url.hostname === host || url.hostname.endsWith(`.${host}`));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function buildAzureResponsesBaseUrl(endpoint) {
|
|
30
|
+
return `${endpoint.replace(/\/+$/, '')}/openai/v1`;
|
|
31
|
+
}
|
|
7
32
|
export function createDefaultClientFactory() {
|
|
8
33
|
const customFactory = loadCustomClientFactory();
|
|
9
34
|
if (customFactory)
|
|
10
35
|
return customFactory;
|
|
11
36
|
return (key, options) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
37
|
+
const openRouter = isOpenRouterBaseUrl(options?.baseUrl);
|
|
38
|
+
const customProxy = isCustomBaseUrl(options?.baseUrl);
|
|
39
|
+
// When using any custom/proxy base URL (OpenRouter, LiteLLM, vLLM, Together, etc.),
|
|
40
|
+
// route ALL models through the OpenAI chat/completions adapter instead of native SDKs
|
|
41
|
+
// which would reject the proxy's API key.
|
|
42
|
+
if (!openRouter && !customProxy) {
|
|
43
|
+
if (options?.model?.startsWith('gemini')) {
|
|
44
|
+
// Gemini client uses its own SDK; allow passing the already-resolved id for transparency/logging.
|
|
45
|
+
return createGeminiClient(key, options.model, options.resolvedModelId);
|
|
46
|
+
}
|
|
47
|
+
if (options?.model?.startsWith('claude')) {
|
|
48
|
+
return createClaudeClient(key, options.model, options.resolvedModelId, options.baseUrl);
|
|
49
|
+
}
|
|
18
50
|
}
|
|
19
51
|
let instance;
|
|
20
|
-
const openRouter = isOpenRouterBaseUrl(options?.baseUrl);
|
|
21
52
|
const defaultHeaders = openRouter ? buildOpenRouterHeaders() : undefined;
|
|
22
53
|
const httpTimeoutMs = typeof options?.httpTimeoutMs === 'number' && Number.isFinite(options.httpTimeoutMs) && options.httpTimeoutMs > 0
|
|
23
54
|
? options.httpTimeoutMs
|
|
24
55
|
: 20 * 60 * 1000;
|
|
25
56
|
if (options?.azure?.endpoint) {
|
|
26
|
-
instance = new
|
|
57
|
+
instance = new OpenAI({
|
|
27
58
|
apiKey: key,
|
|
28
|
-
endpoint: options.azure.endpoint,
|
|
29
|
-
apiVersion: options.azure.apiVersion,
|
|
30
|
-
deployment: options.azure.deployment,
|
|
31
59
|
timeout: httpTimeoutMs,
|
|
60
|
+
baseURL: buildAzureResponsesBaseUrl(options.azure.endpoint),
|
|
32
61
|
});
|
|
33
62
|
}
|
|
34
63
|
else {
|
|
@@ -39,7 +68,7 @@ export function createDefaultClientFactory() {
|
|
|
39
68
|
defaultHeaders,
|
|
40
69
|
});
|
|
41
70
|
}
|
|
42
|
-
if (openRouter) {
|
|
71
|
+
if (openRouter || customProxy) {
|
|
43
72
|
return buildOpenRouterCompletionClient(instance);
|
|
44
73
|
}
|
|
45
74
|
return {
|