@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
package/dist/src/cli/options.js
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
-
|
|
47
|
-
|
|
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 ?? [], {
|
|
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
|
|
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
|
|
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
|
-
//
|
|
147
|
-
|
|
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
|
+
}
|