@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.
Files changed (41) hide show
  1. package/README.md +76 -4
  2. package/dist/bin/oracle-cli.js +188 -7
  3. package/dist/src/browser/actions/modelSelection.js +60 -8
  4. package/dist/src/browser/actions/navigation.js +2 -1
  5. package/dist/src/browser/constants.js +1 -1
  6. package/dist/src/browser/index.js +73 -19
  7. package/dist/src/browser/providerDomFlow.js +17 -0
  8. package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
  9. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +245 -0
  10. package/dist/src/browser/providers/index.js +2 -0
  11. package/dist/src/cli/browserConfig.js +12 -6
  12. package/dist/src/cli/detach.js +5 -2
  13. package/dist/src/cli/fileSize.js +11 -0
  14. package/dist/src/cli/help.js +3 -3
  15. package/dist/src/cli/markdownBundle.js +5 -1
  16. package/dist/src/cli/options.js +40 -3
  17. package/dist/src/cli/runOptions.js +11 -3
  18. package/dist/src/cli/sessionDisplay.js +91 -2
  19. package/dist/src/cli/sessionLineage.js +56 -0
  20. package/dist/src/cli/sessionRunner.js +20 -2
  21. package/dist/src/cli/sessionTable.js +2 -1
  22. package/dist/src/cli/tui/index.js +2 -0
  23. package/dist/src/gemini-web/browserSessionManager.js +76 -0
  24. package/dist/src/gemini-web/client.js +16 -5
  25. package/dist/src/gemini-web/executionClients.js +1 -0
  26. package/dist/src/gemini-web/executionMode.js +18 -0
  27. package/dist/src/gemini-web/executor.js +273 -120
  28. package/dist/src/mcp/tools/consult.js +34 -21
  29. package/dist/src/oracle/client.js +42 -13
  30. package/dist/src/oracle/config.js +43 -7
  31. package/dist/src/oracle/errors.js +2 -2
  32. package/dist/src/oracle/files.js +20 -5
  33. package/dist/src/oracle/gemini.js +3 -0
  34. package/dist/src/oracle/request.js +7 -2
  35. package/dist/src/oracle/run.js +22 -12
  36. package/dist/src/sessionManager.js +4 -0
  37. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  38. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  39. package/package.json +18 -18
  40. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  41. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -152,11 +152,20 @@ export function parseDurationOption(value, label) {
152
152
  }
153
153
  return parsed;
154
154
  }
155
+ function isGeminiDeepThinkAlias(normalized) {
156
+ return ((normalized.includes('gemini') && normalized.includes('deep')) ||
157
+ normalized.includes('deep-think') ||
158
+ normalized.includes('deep_think') ||
159
+ normalized.includes('deepthink'));
160
+ }
155
161
  export function resolveApiModel(modelValue) {
156
162
  const normalized = normalizeModelOption(modelValue).toLowerCase();
157
163
  if (normalized in MODEL_CONFIGS) {
158
164
  return normalized;
159
165
  }
166
+ if (normalized.includes('/')) {
167
+ return normalized;
168
+ }
160
169
  if (normalized.includes('grok')) {
161
170
  return 'grok-4.1';
162
171
  }
@@ -166,6 +175,12 @@ export function resolveApiModel(modelValue) {
166
175
  if (normalized.includes('claude') && normalized.includes('opus')) {
167
176
  return 'claude-4.1-opus';
168
177
  }
178
+ if (normalized.includes('5.4') && normalized.includes('pro')) {
179
+ return 'gpt-5.4-pro';
180
+ }
181
+ if (normalized.includes('5.4')) {
182
+ return 'gpt-5.4';
183
+ }
169
184
  if (normalized === 'claude' || normalized === 'sonnet' || /(^|\b)sonnet(\b|$)/.test(normalized)) {
170
185
  return 'claude-4.5-sonnet';
171
186
  }
@@ -190,11 +205,17 @@ export function resolveApiModel(modelValue) {
190
205
  }
191
206
  return 'gpt-5.1-codex';
192
207
  }
208
+ if (isGeminiDeepThinkAlias(normalized)) {
209
+ throw new InvalidArgumentError('Gemini Deep Think is browser-only today. Use --engine browser --model gemini-3-deep-think.');
210
+ }
193
211
  if (normalized.includes('gemini')) {
212
+ if (normalized.includes('3.1') || normalized.includes('3_1')) {
213
+ return 'gemini-3.1-pro';
214
+ }
194
215
  return 'gemini-3-pro';
195
216
  }
196
217
  if (normalized.includes('pro')) {
197
- return 'gpt-5.2-pro';
218
+ return DEFAULT_MODEL;
198
219
  }
199
220
  // Passthrough for custom/OpenRouter model IDs.
200
221
  return normalized;
@@ -207,6 +228,9 @@ export function inferModelFromLabel(modelValue) {
207
228
  if (normalized in MODEL_CONFIGS) {
208
229
  return normalized;
209
230
  }
231
+ if (normalized.includes('/')) {
232
+ return normalized;
233
+ }
210
234
  if (normalized.includes('grok')) {
211
235
  return 'grok-4.1';
212
236
  }
@@ -219,12 +243,24 @@ export function inferModelFromLabel(modelValue) {
219
243
  if (normalized.includes('codex')) {
220
244
  return 'gpt-5.1-codex';
221
245
  }
246
+ if (isGeminiDeepThinkAlias(normalized)) {
247
+ return 'gemini-3-pro-deep-think';
248
+ }
222
249
  if (normalized.includes('gemini')) {
250
+ if (normalized.includes('3.1') || normalized.includes('3_1')) {
251
+ return 'gemini-3.1-pro';
252
+ }
223
253
  return 'gemini-3-pro';
224
254
  }
225
255
  if (normalized.includes('classic')) {
226
256
  return 'gpt-5-pro';
227
257
  }
258
+ if ((normalized.includes('5.4') || normalized.includes('5_4')) && normalized.includes('pro')) {
259
+ return 'gpt-5.4-pro';
260
+ }
261
+ if (normalized.includes('5.4') || normalized.includes('5_4')) {
262
+ return 'gpt-5.4';
263
+ }
228
264
  if ((normalized.includes('5.2') || normalized.includes('5_2')) && normalized.includes('pro')) {
229
265
  return 'gpt-5.2-pro';
230
266
  }
@@ -241,14 +277,15 @@ export function inferModelFromLabel(modelValue) {
241
277
  if (normalized.includes('gpt-5') &&
242
278
  normalized.includes('pro') &&
243
279
  !normalized.includes('5.1') &&
244
- !normalized.includes('5.2')) {
280
+ !normalized.includes('5.2') &&
281
+ !normalized.includes('5.4')) {
245
282
  return 'gpt-5-pro';
246
283
  }
247
284
  if ((normalized.includes('5.1') || normalized.includes('5_1')) && normalized.includes('pro')) {
248
285
  return 'gpt-5.1-pro';
249
286
  }
250
287
  if (normalized.includes('pro')) {
251
- return 'gpt-5.2-pro';
288
+ return DEFAULT_MODEL;
252
289
  }
253
290
  if (normalized.includes('5.1') || normalized.includes('5_1')) {
254
291
  return 'gpt-5.1';
@@ -4,6 +4,7 @@ import { normalizeModelOption, inferModelFromLabel, resolveApiModel, normalizeBa
4
4
  import { resolveGeminiModelId } from '../oracle/gemini.js';
5
5
  import { PromptValidationError } from '../oracle/errors.js';
6
6
  import { normalizeChatGptModelForBrowser } from './browserConfig.js';
7
+ import { resolveConfiguredMaxFileSizeBytes } from './fileSize.js';
7
8
  export function resolveRunOptionsFromConfig({ prompt, files = [], model, models, engine, userConfig, env = process.env, }) {
8
9
  const resolvedEngine = resolveEngineWithConfig({ engine, configEngine: userConfig?.engine, env });
9
10
  const browserRequested = engine === 'browser';
@@ -14,27 +15,33 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
14
15
  const inferredModel = resolvedEngine === 'browser' && normalizedRequestedModels.length === 0
15
16
  ? inferModelFromLabel(cliModelArg)
16
17
  : resolveApiModel(cliModelArg);
17
- // Browser engine maps Pro/legacy aliases to the latest ChatGPT picker targets (GPT-5.2 / GPT-5.2 Pro).
18
+ // Browser engine maps Pro/legacy aliases to the latest ChatGPT picker targets (GPT-5.4 / GPT-5.4 Pro).
18
19
  const resolvedModel = resolvedEngine === 'browser' ? normalizeChatGptModelForBrowser(inferredModel) : inferredModel;
19
20
  const isCodex = resolvedModel.startsWith('gpt-5.1-codex');
20
21
  const isClaude = resolvedModel.startsWith('claude');
21
22
  const isGrok = resolvedModel.startsWith('grok');
23
+ const isGeminiApiOnly = resolvedModel === 'gemini-3.1-pro';
22
24
  const engineWasBrowser = resolvedEngine === 'browser';
23
25
  const allModels = normalizedRequestedModels.length > 0
24
26
  ? Array.from(new Set(normalizedRequestedModels.map((entry) => resolveApiModel(entry))))
25
27
  : [resolvedModel];
28
+ const includesGeminiApiOnly = allModels.some((m) => m === 'gemini-3.1-pro');
29
+ if ((browserRequested || browserConfigured) && includesGeminiApiOnly) {
30
+ throw new PromptValidationError('gemini-3.1-pro is API-only today. Use --engine api or switch to gemini-3-pro for Gemini web.', { engine: 'browser', models: allModels });
31
+ }
26
32
  const isBrowserCompatible = (m) => m.startsWith('gpt-') || m.startsWith('gemini');
27
33
  const hasNonBrowserCompatibleTarget = (browserRequested || browserConfigured) && allModels.some((m) => !isBrowserCompatible(m));
28
34
  if (hasNonBrowserCompatibleTarget) {
29
35
  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 });
30
36
  }
31
- const engineCoercedToApi = engineWasBrowser && (isCodex || isClaude || isGrok);
32
- const fixedEngine = isCodex || isClaude || isGrok || normalizedRequestedModels.length > 0 ? 'api' : resolvedEngine;
37
+ const engineCoercedToApi = engineWasBrowser && (isCodex || isClaude || isGrok || isGeminiApiOnly);
38
+ const fixedEngine = isCodex || isClaude || isGrok || isGeminiApiOnly || normalizedRequestedModels.length > 0 ? 'api' : resolvedEngine;
33
39
  const promptWithSuffix = userConfig?.promptSuffix && userConfig.promptSuffix.trim().length > 0
34
40
  ? `${prompt.trim()}\n${userConfig.promptSuffix}`
35
41
  : prompt;
36
42
  const search = userConfig?.search !== 'off';
37
43
  const heartbeatIntervalMs = userConfig?.heartbeatSeconds !== undefined ? userConfig.heartbeatSeconds * 1000 : 30_000;
44
+ const maxFileSizeBytes = resolveConfiguredMaxFileSizeBytes(userConfig, env);
38
45
  const baseUrl = normalizeBaseUrl(userConfig?.apiBaseUrl ??
39
46
  (isClaude ? env.ANTHROPIC_BASE_URL : isGrok ? env.XAI_BASE_URL : env.OPENAI_BASE_URL));
40
47
  const uniqueMultiModels = normalizedRequestedModels.length > 0 ? allModels : [];
@@ -49,6 +56,7 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
49
56
  model: chosenModel,
50
57
  models: uniqueMultiModels.length > 0 ? uniqueMultiModels : undefined,
51
58
  file: files ?? [],
59
+ maxFileSizeBytes,
52
60
  search,
53
61
  heartbeatIntervalMs,
54
62
  filesReport: userConfig?.filesReport,
@@ -7,6 +7,7 @@ import { formatTokenCount, formatTokenValue } from '../oracle/runUtils.js';
7
7
  import { resumeBrowserSession } from '../browser/reattach.js';
8
8
  import { estimateTokenCount } from '../browser/utils.js';
9
9
  import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost } from './sessionTable.js';
10
+ import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from './sessionLineage.js';
10
11
  const isTty = () => Boolean(process.stdout.isTTY);
11
12
  const dim = (text) => (isTty() ? kleur.dim(text) : text);
12
13
  export const MAX_RENDER_BYTES = 200_000;
@@ -34,6 +35,7 @@ export async function showStatus({ hours, includeAll, limit, showExamples = fals
34
35
  const { entries, truncated, total } = sessionStore.filterSessions(metas, { hours, includeAll, limit });
35
36
  const filteredEntries = modelFilter ? entries.filter((entry) => matchesModel(entry, modelFilter)) : entries;
36
37
  const richTty = process.stdout.isTTY && chalk.level > 0;
38
+ const responseOwners = buildResponseOwnerIndex(metas);
37
39
  if (!filteredEntries.length) {
38
40
  console.log(CLEANUP_TIP);
39
41
  if (showExamples) {
@@ -43,8 +45,15 @@ export async function showStatus({ hours, includeAll, limit, showExamples = fals
43
45
  }
44
46
  console.log(chalk.bold('Recent Sessions'));
45
47
  console.log(formatSessionTableHeader(richTty));
46
- for (const entry of filteredEntries) {
47
- console.log(formatSessionTableRow(entry, { rich: richTty }));
48
+ const treeRows = buildStatusTreeRows(filteredEntries, responseOwners);
49
+ for (const row of treeRows) {
50
+ const line = formatSessionTableRow(row.entry, { rich: richTty, displaySlug: row.displaySlug });
51
+ const detachedParent = row.detachedParentLabel != null
52
+ ? richTty
53
+ ? chalk.gray(` <- ${row.detachedParentLabel}`)
54
+ : ` <- ${row.detachedParentLabel}`
55
+ : '';
56
+ console.log(`${line}${detachedParent}`);
48
57
  }
49
58
  if (truncated) {
50
59
  const sessionsDir = sessionStore.sessionsDir();
@@ -148,6 +157,10 @@ export async function attachSession(sessionId, options) {
148
157
  if (reattachLine) {
149
158
  console.log(chalk.blue(reattachLine));
150
159
  }
160
+ const chainLine = await buildSessionChainLine(metadata);
161
+ if (chainLine) {
162
+ console.log(dim(`Chain: ${chainLine}`));
163
+ }
151
164
  console.log(`Created: ${metadata.createdAt}`);
152
165
  console.log(`Status: ${metadata.status}`);
153
166
  if (metadata.models && metadata.models.length > 0) {
@@ -446,6 +459,82 @@ function matchesModel(entry, filter) {
446
459
  const models = entry.models?.map((model) => model.model.toLowerCase()) ?? (entry.model ? [entry.model.toLowerCase()] : []);
447
460
  return models.includes(normalized);
448
461
  }
462
+ function buildStatusTreeRows(entries, responseOwners) {
463
+ const entryById = new Map(entries.map((entry) => [entry.id, entry]));
464
+ const orderIndex = new Map(entries.map((entry, index) => [entry.id, index]));
465
+ const lineageById = new Map();
466
+ const childMap = new Map();
467
+ for (const entry of entries) {
468
+ const lineage = resolveSessionLineage(entry, responseOwners);
469
+ lineageById.set(entry.id, lineage);
470
+ const parentId = lineage?.parentSessionId;
471
+ if (parentId && parentId !== entry.id && entryById.has(parentId)) {
472
+ const siblings = childMap.get(parentId) ?? [];
473
+ siblings.push(entry);
474
+ childMap.set(parentId, siblings);
475
+ }
476
+ }
477
+ for (const siblings of childMap.values()) {
478
+ siblings.sort((a, b) => (orderIndex.get(a.id) ?? 0) - (orderIndex.get(b.id) ?? 0));
479
+ }
480
+ const rows = [];
481
+ const visited = new Set();
482
+ const walkChild = (entry, ancestorHasMore, isLast) => {
483
+ if (visited.has(entry.id)) {
484
+ return;
485
+ }
486
+ visited.add(entry.id);
487
+ const children = childMap.get(entry.id) ?? [];
488
+ const nodeBranch = isLast ? '└─ ' : '├─ ';
489
+ const prefix = `${ancestorHasMore.map((hasMore) => (hasMore ? '│ ' : ' ')).join('')}${nodeBranch}`;
490
+ rows.push({ entry, displaySlug: `${prefix}${entry.id}` });
491
+ children.forEach((child, index) => {
492
+ walkChild(child, [...ancestorHasMore, !isLast], index === children.length - 1);
493
+ });
494
+ };
495
+ const walkRoot = (entry) => {
496
+ if (visited.has(entry.id)) {
497
+ return;
498
+ }
499
+ visited.add(entry.id);
500
+ const lineage = lineageById.get(entry.id);
501
+ const hiddenParent = lineage?.parentSessionId && !entryById.has(lineage.parentSessionId)
502
+ ? `${lineage.parentSessionId} (${abbreviateResponseId(lineage.parentResponseId)})`
503
+ : undefined;
504
+ const children = childMap.get(entry.id) ?? [];
505
+ rows.push({ entry, displaySlug: entry.id, detachedParentLabel: hiddenParent });
506
+ children.forEach((child, index) => {
507
+ walkChild(child, [], index === children.length - 1);
508
+ });
509
+ };
510
+ const roots = entries.filter((entry) => {
511
+ const parentId = lineageById.get(entry.id)?.parentSessionId;
512
+ return !(parentId && parentId !== entry.id && entryById.has(parentId));
513
+ });
514
+ roots.forEach((entry) => {
515
+ walkRoot(entry);
516
+ });
517
+ entries.forEach((entry) => {
518
+ walkRoot(entry);
519
+ });
520
+ return rows;
521
+ }
522
+ async function buildSessionChainLine(metadata) {
523
+ const lineageWithoutLookup = resolveSessionLineage(metadata);
524
+ if (!lineageWithoutLookup) {
525
+ return `root -> ${metadata.id}`;
526
+ }
527
+ if (lineageWithoutLookup.parentSessionId) {
528
+ return `${lineageWithoutLookup.parentSessionId} (${abbreviateResponseId(lineageWithoutLookup.parentResponseId)}) -> ${metadata.id}`;
529
+ }
530
+ const sessions = await sessionStore.listSessions().catch(() => []);
531
+ const responseOwners = buildResponseOwnerIndex(sessions);
532
+ const lineage = resolveSessionLineage(metadata, responseOwners) ?? lineageWithoutLookup;
533
+ if (lineage.parentSessionId) {
534
+ return `${lineage.parentSessionId} (${abbreviateResponseId(lineage.parentResponseId)}) -> ${metadata.id}`;
535
+ }
536
+ return `${abbreviateResponseId(lineage.parentResponseId)} -> ${metadata.id}`;
537
+ }
449
538
  async function buildSessionLogForDisplay(sessionId, fallbackMeta, modelFilter) {
450
539
  const normalizedFilter = modelFilter?.trim().toLowerCase();
451
540
  const freshMetadata = (await sessionStore.readSession(sessionId)) ?? fallbackMeta;
@@ -0,0 +1,56 @@
1
+ function readResponseId(record) {
2
+ if (!record)
3
+ return null;
4
+ const candidate = typeof record.responseId === 'string' ? record.responseId : typeof record.id === 'string' ? record.id : null;
5
+ if (!candidate || !candidate.startsWith('resp_')) {
6
+ return null;
7
+ }
8
+ return candidate;
9
+ }
10
+ export function collectSessionResponseIds(meta) {
11
+ const ids = new Set();
12
+ const rootResponse = readResponseId(meta.response);
13
+ if (rootResponse) {
14
+ ids.add(rootResponse);
15
+ }
16
+ const runs = Array.isArray(meta.models) ? meta.models : [];
17
+ for (const run of runs) {
18
+ const runResponse = readResponseId(run.response);
19
+ if (runResponse) {
20
+ ids.add(runResponse);
21
+ }
22
+ }
23
+ return [...ids];
24
+ }
25
+ export function buildResponseOwnerIndex(sessions) {
26
+ const byResponse = new Map();
27
+ for (const session of sessions) {
28
+ for (const responseId of collectSessionResponseIds(session)) {
29
+ if (!byResponse.has(responseId)) {
30
+ byResponse.set(responseId, session.id);
31
+ }
32
+ }
33
+ }
34
+ return byResponse;
35
+ }
36
+ export function resolveSessionLineage(meta, responseOwners) {
37
+ const previous = meta.options?.previousResponseId?.trim();
38
+ if (!previous) {
39
+ return null;
40
+ }
41
+ let parentSessionId = meta.options?.followupSessionId?.trim();
42
+ if (!parentSessionId && responseOwners) {
43
+ parentSessionId = responseOwners.get(previous);
44
+ }
45
+ if (parentSessionId === meta.id) {
46
+ parentSessionId = undefined;
47
+ }
48
+ return { parentResponseId: previous, parentSessionId };
49
+ }
50
+ export function abbreviateResponseId(responseId, max = 18) {
51
+ if (responseId.length <= max) {
52
+ return responseId;
53
+ }
54
+ const head = Math.max(8, max - 7);
55
+ return `${responseId.slice(0, head)}...${responseId.slice(-4)}`;
56
+ }
@@ -101,7 +101,10 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
101
101
  baseUrl: runOptions.baseUrl,
102
102
  openRouterApiKey: process.env.OPENROUTER_API_KEY,
103
103
  });
104
- const files = await readFiles(runOptions.file ?? [], { cwd });
104
+ const files = await readFiles(runOptions.file ?? [], {
105
+ cwd,
106
+ maxFileSizeBytes: runOptions.maxFileSizeBytes,
107
+ });
105
108
  const promptWithFiles = buildPrompt(runOptions.prompt, files, cwd);
106
109
  const requestBody = buildRequestBody({
107
110
  modelConfig,
@@ -322,6 +325,8 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
322
325
  const userError = asOracleUserError(error);
323
326
  const connectionLost = userError?.category === 'browser-automation' && userError.details?.stage === 'connection-lost';
324
327
  const assistantTimeout = userError?.category === 'browser-automation' && userError.details?.stage === 'assistant-timeout';
328
+ const cloudflareChallenge = userError?.category === 'browser-automation' &&
329
+ userError.details?.stage === 'cloudflare-challenge';
325
330
  if (connectionLost && mode === 'browser') {
326
331
  const runtime = userError.details?.runtime;
327
332
  log(dim('Chrome disconnected before completion; keeping session running for reattach.'));
@@ -381,6 +386,13 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
381
386
  log(dim(`Reattach later with: oracle session ${sessionMeta.id}`));
382
387
  return;
383
388
  }
389
+ if (cloudflareChallenge && mode === 'browser') {
390
+ const details = userError.details;
391
+ log(dim('Cloudflare challenge detected; browser left running so you can complete the check.'));
392
+ if (details?.reuseProfileHint) {
393
+ log(dim(`Reuse this browser profile with: ${details.reuseProfileHint}`));
394
+ }
395
+ }
384
396
  if (userError) {
385
397
  log(dim(`User error (${userError.category}): ${userError.message}`));
386
398
  }
@@ -394,12 +406,18 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
394
406
  if (transportLine) {
395
407
  log(dim(`Transport: ${transportLine}`));
396
408
  }
409
+ const browserRuntime = mode === 'browser' ? userError?.details?.runtime : undefined;
397
410
  await sessionStore.updateSession(sessionMeta.id, {
398
411
  status: 'error',
399
412
  completedAt: new Date().toISOString(),
400
413
  errorMessage: message,
401
414
  mode,
402
- browser: browserConfig ? { config: browserConfig } : undefined,
415
+ browser: browserConfig
416
+ ? {
417
+ config: browserConfig,
418
+ runtime: browserRuntime ?? undefined,
419
+ }
420
+ : undefined,
403
421
  response: responseMetadata,
404
422
  transport: transportMetadata,
405
423
  error: userError
@@ -29,7 +29,8 @@ export function formatSessionTableRow(meta, options) {
29
29
  const costValue = resolveSessionCost(meta);
30
30
  const costRaw = costValue != null ? formatCostTable(costValue) : `${''.padStart(COST_PAD - 1)}-`;
31
31
  const cost = rich ? chalk.gray(costRaw) : costRaw;
32
- const slug = rich ? chalk.cyan(meta.id) : meta.id;
32
+ const slugValue = options?.displaySlug ?? meta.id;
33
+ const slug = rich ? chalk.cyan(slugValue) : slugValue;
33
34
  return `${status} ${model} ${mode} ${timestamp} ${chars} ${cost} ${slug}`;
34
35
  }
35
36
  export function resolveSessionCost(meta) {
@@ -13,6 +13,7 @@ import { formatSessionTableHeader, formatSessionTableRow } from '../sessionTable
13
13
  import { buildBrowserConfig, resolveBrowserModelLabel } from '../browserConfig.js';
14
14
  import { resolveNotificationSettings } from '../notifier.js';
15
15
  import { loadUserConfig } from '../../config.js';
16
+ import { resolveConfiguredMaxFileSizeBytes } from '../fileSize.js';
16
17
  import { formatTokenCount } from '../../oracle/runUtils.js';
17
18
  const isTty = () => Boolean(process.stdout.isTTY && chalk.level > 0);
18
19
  const dim = (text) => (isTty() ? kleur.dim(text) : text);
@@ -374,6 +375,7 @@ async function askOracleFlow(version, userConfig) {
374
375
  prompt: promptWithSuffix,
375
376
  model: answers.model,
376
377
  file: answers.files,
378
+ maxFileSizeBytes: resolveConfiguredMaxFileSizeBytes(userConfig, process.env),
377
379
  models: normalizedMultiModels.length > 1 ? normalizedMultiModels : undefined,
378
380
  slug: answers.slug,
379
381
  filesReport: false,
@@ -0,0 +1,76 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { mkdir } from 'node:fs/promises';
4
+ import { launchChrome, connectWithNewTab, closeTab } from '../browser/chromeLifecycle.js';
5
+ import { resolveBrowserConfig } from '../browser/config.js';
6
+ import { readDevToolsPort, writeDevToolsActivePort, writeChromePid, cleanupStaleProfileState, verifyDevToolsReachable, } from '../browser/profileState.js';
7
+ export async function openGeminiBrowserSession(input) {
8
+ const { browserConfig, keepBrowserDefault, purpose, log } = input;
9
+ const profileDir = browserConfig?.manualLoginProfileDir
10
+ ?? path.join(os.homedir(), '.oracle', 'browser-profile');
11
+ await mkdir(profileDir, { recursive: true });
12
+ const resolvedConfig = resolveBrowserConfig({
13
+ ...browserConfig,
14
+ manualLogin: true,
15
+ manualLoginProfileDir: profileDir,
16
+ keepBrowser: browserConfig?.keepBrowser ?? keepBrowserDefault,
17
+ });
18
+ const keepBrowser = Boolean(resolvedConfig.keepBrowser);
19
+ let port = await readDevToolsPort(profileDir);
20
+ let launchedChrome = null;
21
+ let chromeWasLaunched = false;
22
+ if (port) {
23
+ const probe = await verifyDevToolsReachable({ port });
24
+ if (!probe.ok) {
25
+ log?.(`[gemini-web] Stale DevTools port ${port}; launching fresh Chrome for ${purpose}.`);
26
+ await cleanupStaleProfileState(profileDir, log, { lockRemovalMode: 'if_oracle_pid_dead' });
27
+ port = null;
28
+ }
29
+ }
30
+ if (!port) {
31
+ log?.(`[gemini-web] Launching Chrome for ${purpose}.`);
32
+ launchedChrome = await launchChrome(resolvedConfig, profileDir, log ?? (() => { }));
33
+ port = launchedChrome.port;
34
+ chromeWasLaunched = true;
35
+ await writeDevToolsActivePort(profileDir, port);
36
+ if (launchedChrome.pid) {
37
+ await writeChromePid(profileDir, launchedChrome.pid);
38
+ }
39
+ }
40
+ else {
41
+ log?.(`[gemini-web] Reusing Chrome on port ${port} for ${purpose}.`);
42
+ }
43
+ const connection = await connectWithNewTab(port, log ?? (() => { }), undefined);
44
+ const client = connection.client;
45
+ const targetId = connection.targetId;
46
+ const close = async () => {
47
+ if (keepBrowser) {
48
+ try {
49
+ await client.close();
50
+ }
51
+ catch { /* ignore */ }
52
+ return;
53
+ }
54
+ if (targetId && port) {
55
+ await closeTab(port, targetId, log ?? (() => { })).catch(() => undefined);
56
+ }
57
+ try {
58
+ await client.close();
59
+ }
60
+ catch { /* ignore */ }
61
+ if (chromeWasLaunched && launchedChrome) {
62
+ try {
63
+ launchedChrome.kill();
64
+ }
65
+ catch { /* ignore */ }
66
+ await cleanupStaleProfileState(profileDir, log, { lockRemovalMode: 'never' }).catch(() => undefined);
67
+ }
68
+ };
69
+ return {
70
+ profileDir,
71
+ port,
72
+ client,
73
+ targetId: targetId ?? undefined,
74
+ close,
75
+ };
76
+ }
@@ -4,6 +4,7 @@ const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
4
4
  const MODEL_HEADER_NAME = 'x-goog-ext-525001261-jspb';
5
5
  const MODEL_HEADERS = {
6
6
  'gemini-3-pro': '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4]]',
7
+ 'gemini-3-pro-deep-think': '[1,null,null,null,"e6fa609c3fa255c0",null,null,0,[4],null,null,3]',
7
8
  'gemini-2.5-pro': '[1,null,null,null,"4af6c7f5da75d65d",null,null,0,[4]]',
8
9
  'gemini-2.5-flash': '[1,null,null,null,"9ec249fc9ad08861",null,null,0,[4]]',
9
10
  };
@@ -11,6 +12,16 @@ const GEMINI_APP_URL = 'https://gemini.google.com/app';
11
12
  const GEMINI_STREAM_GENERATE_URL = 'https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate';
12
13
  const GEMINI_UPLOAD_URL = 'https://content-push.googleapis.com/upload';
13
14
  const GEMINI_UPLOAD_PUSH_ID = 'feeds/mcudyrk2a4khkz';
15
+ const GEMINI_UPLOAD_MIME_TYPES = {
16
+ '.bmp': 'image/bmp',
17
+ '.gif': 'image/gif',
18
+ '.jpeg': 'image/jpeg',
19
+ '.jpg': 'image/jpeg',
20
+ '.pdf': 'application/pdf',
21
+ '.png': 'image/png',
22
+ '.svg': 'image/svg+xml',
23
+ '.webp': 'image/webp',
24
+ };
14
25
  function getNestedValue(value, pathParts, fallback) {
15
26
  let current = value;
16
27
  for (const part of pathParts) {
@@ -119,8 +130,9 @@ async function uploadGeminiFile(filePath, signal) {
119
130
  const absPath = path.resolve(process.cwd(), filePath);
120
131
  const data = await readFile(absPath);
121
132
  const fileName = path.basename(absPath);
133
+ const mimeType = GEMINI_UPLOAD_MIME_TYPES[path.extname(absPath).toLowerCase()] ?? 'application/octet-stream';
122
134
  const form = new FormData();
123
- form.append('file', new Blob([data]), fileName);
135
+ form.append('file', new Blob([data], { type: mimeType }), fileName);
124
136
  const res = await fetch(GEMINI_UPLOAD_URL, {
125
137
  method: 'POST',
126
138
  redirect: 'follow',
@@ -135,7 +147,7 @@ async function uploadGeminiFile(filePath, signal) {
135
147
  if (!res.ok) {
136
148
  throw new Error(`File upload failed: ${res.status} ${res.statusText} (${text.slice(0, 200)})`);
137
149
  }
138
- return { id: text, name: fileName };
150
+ return { id: text, name: fileName, mimeType };
139
151
  }
140
152
  function buildGeminiFReqPayload(prompt, uploaded, chatMetadata) {
141
153
  const promptPayload = uploaded.length > 0
@@ -143,9 +155,8 @@ function buildGeminiFReqPayload(prompt, uploaded, chatMetadata) {
143
155
  prompt,
144
156
  0,
145
157
  null,
146
- // Matches gemini-webapi payload format: [[[fileId, 1]]] for a single attachment.
147
- // Keep it extensible for multiple uploads by emitting one [[id, 1]] entry per file.
148
- uploaded.map((file) => [[file.id, 1]]),
158
+ // Format: [[[fileId, 1, null, "mimeType"], "filename", ...]]
159
+ uploaded.map((file) => [[file.id, 1, null, file.mimeType], file.name]),
149
160
  ]
150
161
  : [prompt];
151
162
  const innerList = [promptPayload, null, chatMetadata ?? null];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ export function selectGeminiExecutionMode(input) {
2
+ const reasons = [];
3
+ if (input.model !== 'gemini-3-pro-deep-think') {
4
+ return { mode: 'http', reasons: ['model'] };
5
+ }
6
+ if (input.attachmentPaths.length > 0) {
7
+ reasons.push('attachments');
8
+ }
9
+ if (input.generateImagePath) {
10
+ reasons.push('image-generation');
11
+ }
12
+ if (input.editImagePath) {
13
+ reasons.push('image-edit');
14
+ }
15
+ return reasons.length === 0
16
+ ? { mode: 'dom', reasons: [] }
17
+ : { mode: 'http', reasons };
18
+ }