@steipete/oracle 0.7.3 → 0.7.5
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 +6 -0
- package/dist/bin/oracle-cli.js +3 -1
- package/dist/src/browser/actions/attachments.js +7 -3
- package/dist/src/browser/actions/modelSelection.js +51 -10
- package/dist/src/browser/actions/thinkingTime.js +40 -31
- package/dist/src/browser/config.js +1 -1
- package/dist/src/browser/index.js +51 -9
- package/dist/src/cli/browserConfig.js +26 -16
- package/dist/src/cli/options.js +13 -3
- 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 +2 -2
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
package/README.md
CHANGED
|
@@ -64,6 +64,12 @@ Engine auto-picks API when `OPENAI_API_KEY` is set, otherwise browser; browser i
|
|
|
64
64
|
```
|
|
65
65
|
- Tip: set `browser.chatgptUrl` in config (or `--chatgpt-url`) to a dedicated ChatGPT project folder so browser runs don’t clutter your main history.
|
|
66
66
|
|
|
67
|
+
**Codex skill**
|
|
68
|
+
- Copy the bundled skill from this repo to your Codex skills folder:
|
|
69
|
+
- `mkdir -p ~/.codex/skills`
|
|
70
|
+
- `cp -R skills/oracle ~/.codex/skills/oracle`
|
|
71
|
+
- Then reference it in your `AGENTS.md`/`CLAUDE.md` so Codex loads it.
|
|
72
|
+
|
|
67
73
|
**MCP**
|
|
68
74
|
- Run the stdio server via `oracle-mcp`.
|
|
69
75
|
- Configure clients via [steipete/mcporter](https://github.com/steipete/mcporter) or `.mcp.json`; see [docs/mcp.md](docs/mcp.md) for connection examples.
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -174,7 +174,9 @@ program
|
|
|
174
174
|
.addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
|
|
175
175
|
.addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
|
|
176
176
|
.addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
|
|
177
|
-
.addOption(new Option('--browser-
|
|
177
|
+
.addOption(new Option('--browser-thinking-time <level>', 'Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.')
|
|
178
|
+
.choices(['light', 'standard', 'extended', 'heavy'])
|
|
179
|
+
.hideHelp())
|
|
178
180
|
.addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
|
|
179
181
|
.addOption(new Option('--browser-attachments <mode>', 'How to deliver --file inputs in browser mode: auto (default) pastes inline up to ~60k chars then uploads; never always paste inline; always always upload.')
|
|
180
182
|
.choices(['auto', 'never', 'always'])
|
|
@@ -177,12 +177,15 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
177
177
|
}
|
|
178
178
|
await dom.setFileInputFiles({ nodeId: resultNode.nodeId, files: [attachment.path] });
|
|
179
179
|
await runtime.evaluate({ expression: dispatchEventsFor(idx), returnByValue: true }).catch(() => undefined);
|
|
180
|
-
|
|
180
|
+
await delay(350);
|
|
181
|
+
const probeDeadline = Date.now() + 6500;
|
|
182
|
+
const pokeIntervalMs = 1200;
|
|
181
183
|
let lastPoke = 0;
|
|
184
|
+
let seenInputHasFile = false;
|
|
182
185
|
while (Date.now() < probeDeadline) {
|
|
183
186
|
// ChatGPT's composer can take a moment to hydrate the file-input onChange handler after navigation/model switches.
|
|
184
187
|
// If the UI hasn't reacted yet, poke the input a few times to ensure the handler fires once it's mounted.
|
|
185
|
-
if (Date.now() - lastPoke >
|
|
188
|
+
if (!seenInputHasFile && Date.now() - lastPoke > pokeIntervalMs) {
|
|
186
189
|
lastPoke = Date.now();
|
|
187
190
|
await runtime.evaluate({ expression: dispatchEventsFor(idx), returnByValue: true }).catch(() => undefined);
|
|
188
191
|
}
|
|
@@ -198,6 +201,7 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
198
201
|
composerText: typeof snapshot.composerText === 'string' ? snapshot.composerText : '',
|
|
199
202
|
};
|
|
200
203
|
const inputHasFile = finalSnapshot.inputNames.some((name) => name.toLowerCase().includes(expectedName.toLowerCase()));
|
|
204
|
+
seenInputHasFile = seenInputHasFile || inputHasFile;
|
|
201
205
|
const expectedLower = expectedName.toLowerCase();
|
|
202
206
|
const expectedNoExt = expectedLower.replace(/\.[a-z0-9]{1,10}$/i, '');
|
|
203
207
|
const uiAcknowledged = finalSnapshot.chipCount > baselineChipCount ||
|
|
@@ -209,7 +213,7 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
209
213
|
break;
|
|
210
214
|
}
|
|
211
215
|
}
|
|
212
|
-
await delay(
|
|
216
|
+
await delay(250);
|
|
213
217
|
}
|
|
214
218
|
const inputHasFile = finalSnapshot?.inputNames?.some((name) => name.toLowerCase().includes(expectedName.toLowerCase())) ?? false;
|
|
215
219
|
const uiAcknowledged = (finalSnapshot?.chipCount ?? 0) > baselineChipCount;
|
|
@@ -97,6 +97,10 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
97
97
|
if (wantsPro && !normalizedLabel.includes(' pro')) return false;
|
|
98
98
|
if (wantsInstant && !normalizedLabel.includes('instant')) return false;
|
|
99
99
|
if (wantsThinking && !normalizedLabel.includes('thinking')) return false;
|
|
100
|
+
// Also reject if button has variants we DON'T want
|
|
101
|
+
if (!wantsPro && normalizedLabel.includes(' pro')) return false;
|
|
102
|
+
if (!wantsInstant && normalizedLabel.includes('instant')) return false;
|
|
103
|
+
if (!wantsThinking && normalizedLabel.includes('thinking')) return false;
|
|
100
104
|
return true;
|
|
101
105
|
};
|
|
102
106
|
|
|
@@ -172,14 +176,21 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
172
176
|
return 0;
|
|
173
177
|
}
|
|
174
178
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (
|
|
179
|
+
// Exact testid matches take priority over substring matches
|
|
180
|
+
const exactMatch = TEST_IDS.find((id) => id && normalizedTestId === id);
|
|
181
|
+
if (exactMatch) {
|
|
182
|
+
score += 1500;
|
|
183
|
+
if (exactMatch.startsWith('model-switcher-')) score += 200;
|
|
184
|
+
} else {
|
|
185
|
+
const matches = TEST_IDS.filter((id) => id && normalizedTestId.includes(id));
|
|
186
|
+
if (matches.length > 0) {
|
|
187
|
+
// Prefer the most specific match (longest token) instead of treating any hit as equal.
|
|
188
|
+
// This prevents generic tokens (e.g. "pro") from outweighing version-specific targets.
|
|
189
|
+
const best = matches.reduce((acc, token) => (token.length > acc.length ? token : acc), '');
|
|
190
|
+
score += 200 + Math.min(900, best.length * 25);
|
|
191
|
+
if (best.startsWith('model-switcher-')) score += 120;
|
|
192
|
+
if (best.includes('gpt-')) score += 60;
|
|
193
|
+
}
|
|
183
194
|
}
|
|
184
195
|
}
|
|
185
196
|
if (normalizedText && normalizedTarget) {
|
|
@@ -215,6 +226,22 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
215
226
|
} else if (normalizedText.includes(' pro')) {
|
|
216
227
|
score -= 40;
|
|
217
228
|
}
|
|
229
|
+
// Similarly for Thinking variant
|
|
230
|
+
if (wantsThinking) {
|
|
231
|
+
if (!normalizedText.includes('thinking') && !normalizedTestId.includes('thinking')) {
|
|
232
|
+
score -= 80;
|
|
233
|
+
}
|
|
234
|
+
} else if (normalizedText.includes('thinking') || normalizedTestId.includes('thinking')) {
|
|
235
|
+
score -= 40;
|
|
236
|
+
}
|
|
237
|
+
// Similarly for Instant variant
|
|
238
|
+
if (wantsInstant) {
|
|
239
|
+
if (!normalizedText.includes('instant') && !normalizedTestId.includes('instant')) {
|
|
240
|
+
score -= 80;
|
|
241
|
+
}
|
|
242
|
+
} else if (normalizedText.includes('instant') || normalizedTestId.includes('instant')) {
|
|
243
|
+
score -= 40;
|
|
244
|
+
}
|
|
218
245
|
return Math.max(score, 0);
|
|
219
246
|
};
|
|
220
247
|
|
|
@@ -377,10 +404,24 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
377
404
|
push('gpt5-2', labelTokens);
|
|
378
405
|
push('gpt52', labelTokens);
|
|
379
406
|
push('chatgpt 5.2', labelTokens);
|
|
380
|
-
|
|
407
|
+
// Thinking variant: explicit testid for "Thinking" picker option
|
|
408
|
+
if (base.includes('thinking')) {
|
|
381
409
|
push('thinking', labelTokens);
|
|
382
|
-
|
|
410
|
+
testIdTokens.add('model-switcher-gpt-5-2-thinking');
|
|
411
|
+
testIdTokens.add('gpt-5-2-thinking');
|
|
412
|
+
testIdTokens.add('gpt-5.2-thinking');
|
|
413
|
+
}
|
|
414
|
+
// Instant variant: explicit testid for "Instant" picker option
|
|
415
|
+
if (base.includes('instant')) {
|
|
383
416
|
push('instant', labelTokens);
|
|
417
|
+
testIdTokens.add('model-switcher-gpt-5-2-instant');
|
|
418
|
+
testIdTokens.add('gpt-5-2-instant');
|
|
419
|
+
testIdTokens.add('gpt-5.2-instant');
|
|
420
|
+
}
|
|
421
|
+
// Base 5.2 testids (for "Auto" mode when no suffix specified)
|
|
422
|
+
if (!base.includes('thinking') && !base.includes('instant') && !base.includes('pro')) {
|
|
423
|
+
testIdTokens.add('model-switcher-gpt-5-2');
|
|
424
|
+
}
|
|
384
425
|
testIdTokens.add('gpt-5-2');
|
|
385
426
|
testIdTokens.add('gpt5-2');
|
|
386
427
|
testIdTokens.add('gpt52');
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR } from '../constants.js';
|
|
2
2
|
import { logDomFailure } from '../domDebug.js';
|
|
3
3
|
import { buildClickDispatcher } from './domEvents.js';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Selects a specific thinking time level in ChatGPT's composer pill menu.
|
|
6
|
+
* @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
|
|
7
|
+
*/
|
|
8
|
+
export async function ensureThinkingTime(Runtime, level, logger) {
|
|
9
|
+
const result = await evaluateThinkingTimeSelection(Runtime, level);
|
|
10
|
+
const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
|
|
6
11
|
switch (result?.status) {
|
|
7
|
-
case 'already-
|
|
8
|
-
logger(`Thinking time: ${result.label ??
|
|
12
|
+
case 'already-selected':
|
|
13
|
+
logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
|
|
9
14
|
return;
|
|
10
15
|
case 'switched':
|
|
11
|
-
logger(`Thinking time: ${result.label ??
|
|
16
|
+
logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
|
|
12
17
|
return;
|
|
13
18
|
case 'chip-not-found': {
|
|
14
19
|
await logDomFailure(Runtime, logger, 'thinking-chip');
|
|
@@ -18,33 +23,35 @@ export async function ensureExtendedThinking(Runtime, logger) {
|
|
|
18
23
|
await logDomFailure(Runtime, logger, 'thinking-time-menu');
|
|
19
24
|
throw new Error('Unable to find the Thinking time dropdown menu.');
|
|
20
25
|
}
|
|
21
|
-
case '
|
|
22
|
-
await logDomFailure(Runtime, logger,
|
|
23
|
-
throw new Error(
|
|
26
|
+
case 'option-not-found': {
|
|
27
|
+
await logDomFailure(Runtime, logger, `${level}-option`);
|
|
28
|
+
throw new Error(`Unable to find the ${capitalizedLevel} option in the Thinking time menu.`);
|
|
24
29
|
}
|
|
25
30
|
default: {
|
|
26
31
|
await logDomFailure(Runtime, logger, 'thinking-time-unknown');
|
|
27
|
-
throw new Error(
|
|
32
|
+
throw new Error(`Unknown error selecting ${capitalizedLevel} thinking time.`);
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
/**
|
|
32
|
-
* Best-effort selection of
|
|
37
|
+
* Best-effort selection of a thinking time level in ChatGPT's composer pill menu.
|
|
33
38
|
* Safe by default: if the pill/menu/option isn't present, we continue without throwing.
|
|
39
|
+
* @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
|
|
34
40
|
*/
|
|
35
|
-
export async function
|
|
41
|
+
export async function ensureThinkingTimeIfAvailable(Runtime, level, logger) {
|
|
36
42
|
try {
|
|
37
|
-
const result = await evaluateThinkingTimeSelection(Runtime);
|
|
43
|
+
const result = await evaluateThinkingTimeSelection(Runtime, level);
|
|
44
|
+
const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
|
|
38
45
|
switch (result?.status) {
|
|
39
|
-
case 'already-
|
|
40
|
-
logger(`Thinking time: ${result.label ??
|
|
46
|
+
case 'already-selected':
|
|
47
|
+
logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
|
|
41
48
|
return true;
|
|
42
49
|
case 'switched':
|
|
43
|
-
logger(`Thinking time: ${result.label ??
|
|
50
|
+
logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
|
|
44
51
|
return true;
|
|
45
52
|
case 'chip-not-found':
|
|
46
53
|
case 'menu-not-found':
|
|
47
|
-
case '
|
|
54
|
+
case 'option-not-found':
|
|
48
55
|
if (logger.verbose) {
|
|
49
56
|
logger(`Thinking time: ${result.status.replaceAll('-', ' ')}; continuing with default.`);
|
|
50
57
|
}
|
|
@@ -65,22 +72,24 @@ export async function ensureExtendedThinkingIfAvailable(Runtime, logger) {
|
|
|
65
72
|
return false;
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
|
-
async function evaluateThinkingTimeSelection(Runtime) {
|
|
75
|
+
async function evaluateThinkingTimeSelection(Runtime, level) {
|
|
69
76
|
const outcome = await Runtime.evaluate({
|
|
70
|
-
expression: buildThinkingTimeExpression(),
|
|
77
|
+
expression: buildThinkingTimeExpression(level),
|
|
71
78
|
awaitPromise: true,
|
|
72
79
|
returnByValue: true,
|
|
73
80
|
});
|
|
74
81
|
return outcome.result?.value;
|
|
75
82
|
}
|
|
76
|
-
function buildThinkingTimeExpression() {
|
|
83
|
+
function buildThinkingTimeExpression(level) {
|
|
77
84
|
const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
|
|
78
85
|
const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
|
|
86
|
+
const targetLevelLiteral = JSON.stringify(level.toLowerCase());
|
|
79
87
|
return `(async () => {
|
|
80
88
|
${buildClickDispatcher()}
|
|
81
89
|
|
|
82
90
|
const MENU_CONTAINER_SELECTOR = ${menuContainerLiteral};
|
|
83
91
|
const MENU_ITEM_SELECTOR = ${menuItemLiteral};
|
|
92
|
+
const TARGET_LEVEL = ${targetLevelLiteral};
|
|
84
93
|
|
|
85
94
|
const CHIP_SELECTORS = [
|
|
86
95
|
'[data-testid="composer-footer-actions"] button[aria-haspopup="menu"]',
|
|
@@ -136,11 +145,11 @@ function buildThinkingTimeExpression() {
|
|
|
136
145
|
return null;
|
|
137
146
|
};
|
|
138
147
|
|
|
139
|
-
const
|
|
148
|
+
const findTargetOption = (menu) => {
|
|
140
149
|
const items = menu.querySelectorAll(MENU_ITEM_SELECTOR);
|
|
141
150
|
for (const item of items) {
|
|
142
151
|
const text = normalize(item.textContent ?? '');
|
|
143
|
-
if (text.includes(
|
|
152
|
+
if (text.includes(TARGET_LEVEL)) {
|
|
144
153
|
return item;
|
|
145
154
|
}
|
|
146
155
|
}
|
|
@@ -167,24 +176,24 @@ function buildThinkingTimeExpression() {
|
|
|
167
176
|
return;
|
|
168
177
|
}
|
|
169
178
|
|
|
170
|
-
const
|
|
171
|
-
if (!
|
|
172
|
-
resolve({ status: '
|
|
179
|
+
const targetOption = findTargetOption(menu);
|
|
180
|
+
if (!targetOption) {
|
|
181
|
+
resolve({ status: 'option-not-found' });
|
|
173
182
|
return;
|
|
174
183
|
}
|
|
175
184
|
|
|
176
185
|
const alreadySelected =
|
|
177
|
-
optionIsSelected(
|
|
178
|
-
optionIsSelected(
|
|
179
|
-
const label =
|
|
180
|
-
dispatchClickSequence(
|
|
181
|
-
resolve({ status: alreadySelected ? 'already-
|
|
186
|
+
optionIsSelected(targetOption) ||
|
|
187
|
+
optionIsSelected(targetOption.querySelector?.('[aria-checked="true"], [data-state="checked"], [data-state="selected"]'));
|
|
188
|
+
const label = targetOption.textContent?.trim?.() || null;
|
|
189
|
+
dispatchClickSequence(targetOption);
|
|
190
|
+
resolve({ status: alreadySelected ? 'already-selected' : 'switched', label });
|
|
182
191
|
};
|
|
183
192
|
|
|
184
193
|
setTimeout(attempt, INITIAL_WAIT_MS);
|
|
185
194
|
});
|
|
186
195
|
})()`;
|
|
187
196
|
}
|
|
188
|
-
export function buildThinkingTimeExpressionForTest() {
|
|
189
|
-
return buildThinkingTimeExpression();
|
|
197
|
+
export function buildThinkingTimeExpressionForTest(level = 'extended') {
|
|
198
|
+
return buildThinkingTimeExpression(level);
|
|
190
199
|
}
|
|
@@ -24,7 +24,6 @@ export const DEFAULT_BROWSER_CONFIG = {
|
|
|
24
24
|
remoteChrome: null,
|
|
25
25
|
manualLogin: false,
|
|
26
26
|
manualLoginProfileDir: null,
|
|
27
|
-
extendedThinking: false,
|
|
28
27
|
};
|
|
29
28
|
export function resolveBrowserConfig(config) {
|
|
30
29
|
const debugPortEnv = parseDebugPort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT);
|
|
@@ -64,6 +63,7 @@ export function resolveBrowserConfig(config) {
|
|
|
64
63
|
chromeCookiePath: config?.chromeCookiePath ?? DEFAULT_BROWSER_CONFIG.chromeCookiePath,
|
|
65
64
|
debug: config?.debug ?? DEFAULT_BROWSER_CONFIG.debug,
|
|
66
65
|
allowCookieErrors: config?.allowCookieErrors ?? envAllowCookieErrors ?? DEFAULT_BROWSER_CONFIG.allowCookieErrors,
|
|
66
|
+
thinkingTime: config?.thinkingTime,
|
|
67
67
|
manualLogin,
|
|
68
68
|
manualLoginProfileDir: manualLogin ? resolvedProfileDir : null,
|
|
69
69
|
};
|
|
@@ -7,7 +7,7 @@ import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToChro
|
|
|
7
7
|
import { syncCookies } from './cookies.js';
|
|
8
8
|
import { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureModelSelection, submitPrompt, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from './pageActions.js';
|
|
9
9
|
import { uploadAttachmentViaDataTransfer } from './actions/remoteFileTransfer.js';
|
|
10
|
-
import {
|
|
10
|
+
import { ensureThinkingTime } from './actions/thinkingTime.js';
|
|
11
11
|
import { estimateTokenCount, withRetries, delay } from './utils.js';
|
|
12
12
|
import { formatElapsed } from '../oracle/format.js';
|
|
13
13
|
import { CHATGPT_URL } from './constants.js';
|
|
@@ -266,13 +266,15 @@ export async function runBrowserMode(options) {
|
|
|
266
266
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
267
267
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
268
268
|
}
|
|
269
|
-
if
|
|
270
|
-
|
|
269
|
+
// Handle thinking time selection if specified
|
|
270
|
+
const thinkingTime = config.thinkingTime;
|
|
271
|
+
if (thinkingTime) {
|
|
272
|
+
await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
|
|
271
273
|
retries: 2,
|
|
272
274
|
delayMs: 300,
|
|
273
275
|
onRetry: (attempt, error) => {
|
|
274
276
|
if (options.verbose) {
|
|
275
|
-
logger(`[retry]
|
|
277
|
+
logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
276
278
|
}
|
|
277
279
|
},
|
|
278
280
|
}));
|
|
@@ -286,6 +288,7 @@ export async function runBrowserMode(options) {
|
|
|
286
288
|
for (const attachment of submissionAttachments) {
|
|
287
289
|
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
288
290
|
await uploadAttachmentFile({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
291
|
+
await delay(500);
|
|
289
292
|
}
|
|
290
293
|
// Scale timeout based on number of files: base 30s + 15s per additional file
|
|
291
294
|
const baseTimeout = config.inputTimeoutMs ?? 30_000;
|
|
@@ -317,7 +320,7 @@ export async function runBrowserMode(options) {
|
|
|
317
320
|
}
|
|
318
321
|
}
|
|
319
322
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
320
|
-
const answer = await raceWithDisconnect(
|
|
323
|
+
const answer = await raceWithDisconnect(waitForAssistantResponseWithReload(Runtime, Page, config.timeoutMs, logger));
|
|
321
324
|
answerText = answer.text;
|
|
322
325
|
answerHtml = answer.html ?? '';
|
|
323
326
|
const copiedMarkdown = await raceWithDisconnect(withRetries(async () => {
|
|
@@ -663,13 +666,15 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
663
666
|
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
664
667
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
665
668
|
}
|
|
666
|
-
if
|
|
667
|
-
|
|
669
|
+
// Handle thinking time selection if specified
|
|
670
|
+
const thinkingTime = config.thinkingTime;
|
|
671
|
+
if (thinkingTime) {
|
|
672
|
+
await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
|
|
668
673
|
retries: 2,
|
|
669
674
|
delayMs: 300,
|
|
670
675
|
onRetry: (attempt, error) => {
|
|
671
676
|
if (options.verbose) {
|
|
672
|
-
logger(`[retry]
|
|
677
|
+
logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
673
678
|
}
|
|
674
679
|
},
|
|
675
680
|
});
|
|
@@ -684,6 +689,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
684
689
|
for (const attachment of submissionAttachments) {
|
|
685
690
|
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
686
691
|
await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
692
|
+
await delay(500);
|
|
687
693
|
}
|
|
688
694
|
// Scale timeout based on number of files: base 30s + 15s per additional file
|
|
689
695
|
const baseTimeout = config.inputTimeoutMs ?? 30_000;
|
|
@@ -711,7 +717,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
711
717
|
}
|
|
712
718
|
}
|
|
713
719
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
714
|
-
const answer = await
|
|
720
|
+
const answer = await waitForAssistantResponseWithReload(Runtime, Page, config.timeoutMs, logger);
|
|
715
721
|
answerText = answer.text;
|
|
716
722
|
answerHtml = answer.html ?? '';
|
|
717
723
|
const copiedMarkdown = await withRetries(async () => {
|
|
@@ -861,6 +867,42 @@ export function formatThinkingLog(startedAt, now, message, locatorSuffix) {
|
|
|
861
867
|
const statusLabel = message ? ` — ${message}` : '';
|
|
862
868
|
return `${pct}% [${elapsedText} / ~10m]${statusLabel}${locatorSuffix}`;
|
|
863
869
|
}
|
|
870
|
+
async function waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger) {
|
|
871
|
+
try {
|
|
872
|
+
return await waitForAssistantResponse(Runtime, timeoutMs, logger);
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
if (!shouldReloadAfterAssistantError(error)) {
|
|
876
|
+
throw error;
|
|
877
|
+
}
|
|
878
|
+
const conversationUrl = await readConversationUrl(Runtime);
|
|
879
|
+
if (!conversationUrl || !isConversationUrl(conversationUrl)) {
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
logger('Assistant response stalled; reloading conversation and retrying once');
|
|
883
|
+
await Page.navigate({ url: conversationUrl });
|
|
884
|
+
await delay(1000);
|
|
885
|
+
return await waitForAssistantResponse(Runtime, timeoutMs, logger);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function shouldReloadAfterAssistantError(error) {
|
|
889
|
+
if (!(error instanceof Error))
|
|
890
|
+
return false;
|
|
891
|
+
const message = error.message.toLowerCase();
|
|
892
|
+
return message.includes('assistant-response') || message.includes('watchdog') || message.includes('timeout');
|
|
893
|
+
}
|
|
894
|
+
async function readConversationUrl(Runtime) {
|
|
895
|
+
try {
|
|
896
|
+
const currentUrl = await Runtime.evaluate({ expression: 'location.href', returnByValue: true });
|
|
897
|
+
return typeof currentUrl.result?.value === 'string' ? currentUrl.result.value : null;
|
|
898
|
+
}
|
|
899
|
+
catch {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function isConversationUrl(url) {
|
|
904
|
+
return /\/c\/[a-z0-9-]+/i.test(url);
|
|
905
|
+
}
|
|
864
906
|
function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false) {
|
|
865
907
|
let stopped = false;
|
|
866
908
|
let pending = false;
|
|
@@ -5,17 +5,20 @@ import { getOracleHomeDir } from '../oracleHome.js';
|
|
|
5
5
|
const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
|
|
6
6
|
const DEFAULT_BROWSER_INPUT_TIMEOUT_MS = 30_000;
|
|
7
7
|
const DEFAULT_CHROME_PROFILE = 'Default';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
'gpt-5.
|
|
13
|
-
'gpt-5.2'
|
|
14
|
-
|
|
15
|
-
'gpt-5.
|
|
16
|
-
'gpt-5
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
// Ordered array: most specific models first to ensure correct selection.
|
|
9
|
+
// The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
|
|
10
|
+
const BROWSER_MODEL_LABELS = [
|
|
11
|
+
// Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
|
|
12
|
+
['gpt-5.2-thinking', 'GPT-5.2 Thinking'],
|
|
13
|
+
['gpt-5.2-instant', 'GPT-5.2 Instant'],
|
|
14
|
+
['gpt-5.2-pro', 'GPT-5.2 Pro'],
|
|
15
|
+
['gpt-5.1-pro', 'GPT-5.2 Pro'],
|
|
16
|
+
['gpt-5-pro', 'GPT-5.2 Pro'],
|
|
17
|
+
// Base models last (least specific)
|
|
18
|
+
['gpt-5.2', 'GPT-5.2'], // Selects "Auto" in ChatGPT UI
|
|
19
|
+
['gpt-5.1', 'GPT-5.2'], // Legacy alias → Auto
|
|
20
|
+
['gemini-3-pro', 'Gemini 3 Pro'],
|
|
21
|
+
];
|
|
19
22
|
export function normalizeChatGptModelForBrowser(model) {
|
|
20
23
|
const normalized = model.toLowerCase();
|
|
21
24
|
if (!normalized.startsWith('gpt-') || normalized.includes('codex')) {
|
|
@@ -25,10 +28,11 @@ export function normalizeChatGptModelForBrowser(model) {
|
|
|
25
28
|
if (normalized === 'gpt-5-pro' || normalized === 'gpt-5.1-pro' || normalized.endsWith('-pro')) {
|
|
26
29
|
return 'gpt-5.2-pro';
|
|
27
30
|
}
|
|
28
|
-
//
|
|
29
|
-
if (normalized === 'gpt-5.2-instant') {
|
|
30
|
-
return
|
|
31
|
+
// Explicit model variants: keep as-is (they have their own browser labels)
|
|
32
|
+
if (normalized === 'gpt-5.2-thinking' || normalized === 'gpt-5.2-instant') {
|
|
33
|
+
return normalized;
|
|
31
34
|
}
|
|
35
|
+
// Legacy aliases: map to base GPT-5.2 (Auto)
|
|
32
36
|
if (normalized === 'gpt-5.1') {
|
|
33
37
|
return 'gpt-5.2';
|
|
34
38
|
}
|
|
@@ -86,7 +90,7 @@ export async function buildBrowserConfig(options) {
|
|
|
86
90
|
// Allow cookie failures by default so runs can continue without Chrome/Keychain secrets.
|
|
87
91
|
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
88
92
|
remoteChrome,
|
|
89
|
-
|
|
93
|
+
thinkingTime: options.browserThinkingTime,
|
|
90
94
|
};
|
|
91
95
|
}
|
|
92
96
|
function selectBrowserPort(options) {
|
|
@@ -100,7 +104,13 @@ function selectBrowserPort(options) {
|
|
|
100
104
|
}
|
|
101
105
|
export function mapModelToBrowserLabel(model) {
|
|
102
106
|
const normalized = normalizeChatGptModelForBrowser(model);
|
|
103
|
-
|
|
107
|
+
// Iterate ordered array to find first match (most specific first)
|
|
108
|
+
for (const [key, label] of BROWSER_MODEL_LABELS) {
|
|
109
|
+
if (key === normalized) {
|
|
110
|
+
return label;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return DEFAULT_MODEL_TARGET;
|
|
104
114
|
}
|
|
105
115
|
export function resolveBrowserModelLabel(input, model) {
|
|
106
116
|
const trimmed = input?.trim?.() ?? '';
|
package/dist/src/cli/options.js
CHANGED
|
@@ -187,6 +187,13 @@ export function inferModelFromLabel(modelValue) {
|
|
|
187
187
|
if ((normalized.includes('5.2') || normalized.includes('5_2')) && normalized.includes('pro')) {
|
|
188
188
|
return 'gpt-5.2-pro';
|
|
189
189
|
}
|
|
190
|
+
// Browser-only: pass through 5.2 thinking/instant variants for browser label mapping
|
|
191
|
+
if ((normalized.includes('5.2') || normalized.includes('5_2')) && normalized.includes('thinking')) {
|
|
192
|
+
return 'gpt-5.2-thinking';
|
|
193
|
+
}
|
|
194
|
+
if ((normalized.includes('5.2') || normalized.includes('5_2')) && normalized.includes('instant')) {
|
|
195
|
+
return 'gpt-5.2-instant';
|
|
196
|
+
}
|
|
190
197
|
if (normalized.includes('5.0') || normalized.includes('5-pro')) {
|
|
191
198
|
return 'gpt-5-pro';
|
|
192
199
|
}
|
|
@@ -205,8 +212,11 @@ export function inferModelFromLabel(modelValue) {
|
|
|
205
212
|
if (normalized.includes('5.1') || normalized.includes('5_1')) {
|
|
206
213
|
return 'gpt-5.1';
|
|
207
214
|
}
|
|
208
|
-
if (normalized.includes('
|
|
209
|
-
return 'gpt-5.
|
|
215
|
+
if (normalized.includes('thinking')) {
|
|
216
|
+
return 'gpt-5.2-thinking';
|
|
217
|
+
}
|
|
218
|
+
if (normalized.includes('instant') || normalized.includes('fast')) {
|
|
219
|
+
return 'gpt-5.2-instant';
|
|
210
220
|
}
|
|
211
|
-
return 'gpt-5.
|
|
221
|
+
return 'gpt-5.2';
|
|
212
222
|
}
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "CLI wrapper around OpenAI Responses API with GPT-5.2 Pro (via gpt-5.1-pro alias), GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/bin/oracle-cli.js",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"shiki": "^3.20.0",
|
|
84
84
|
"sqlite3": "^5.1.7",
|
|
85
85
|
"toasted-notifier": "^10.1.0",
|
|
86
|
-
"tokentally": "
|
|
86
|
+
"tokentally": "^0.1.1",
|
|
87
87
|
"zod": "^4.2.1"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
Binary file
|
|
Binary file
|