@steipete/oracle 0.5.6 → 0.6.1
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 +3 -3
- package/dist/bin/oracle-cli.js +8 -2
- package/dist/src/browser/actions/assistantResponse.js +65 -6
- package/dist/src/browser/actions/modelSelection.js +22 -0
- package/dist/src/browser/actions/promptComposer.js +67 -3
- package/dist/src/browser/actions/thinkingTime.js +190 -0
- package/dist/src/browser/config.js +1 -0
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +106 -74
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/profileState.js +171 -0
- package/dist/src/browser/prompt.js +63 -19
- package/dist/src/browser/sessionRunner.js +10 -6
- package/dist/src/cli/browserConfig.js +6 -2
- package/dist/src/cli/sessionDisplay.js +8 -1
- package/dist/src/cli/sessionRunner.js +2 -8
- package/dist/src/cli/tui/index.js +1 -0
- package/dist/src/config.js +2 -3
- package/dist/src/oracle/config.js +38 -3
- package/dist/src/oracle/errors.js +3 -3
- package/dist/src/oracle/gemini.js +4 -1
- package/dist/src/oracle/run.js +4 -7
- package/dist/src/oracleHome.js +13 -0
- package/dist/src/remote/server.js +17 -11
- package/dist/src/sessionManager.js +10 -8
- package/dist/src/sessionStore.js +2 -2
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +22 -38
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/vendor/oracle-notifier/README.md +0 -24
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="MIT License"></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
|
-
Oracle bundles your prompt and files so another AI can answer with real context. It speaks GPT-5.1 Pro (default), GPT-5.1 Codex (API-only), GPT-5.1, Gemini 3 Pro, Claude Sonnet 4.5, Claude Opus 4.1, and more—and it can ask one or multiple models in a single run. Browser automation is available; API remains the most reliable path, and `--copy` is an easy manual fallback.
|
|
14
|
+
Oracle bundles your prompt and files so another AI can answer with real context. It speaks GPT-5.1 Pro (default alias to GPT-5.2 Pro on the API), GPT-5.1 Codex (API-only), GPT-5.1, GPT-5.2, Gemini 3 Pro, Claude Sonnet 4.5, Claude Opus 4.1, and more—and it can ask one or multiple models in a single run. Browser automation is available; API remains the most reliable path, and `--copy` is an easy manual fallback.
|
|
15
15
|
|
|
16
16
|
## Quick start
|
|
17
17
|
|
|
@@ -95,7 +95,7 @@ npx -y @steipete/oracle oracle-mcp
|
|
|
95
95
|
| `-p, --prompt <text>` | Required prompt. |
|
|
96
96
|
| `-f, --file <paths...>` | Attach files/dirs (globs + `!` excludes). |
|
|
97
97
|
| `-e, --engine <api\|browser>` | Choose API or browser (browser is experimental). |
|
|
98
|
-
| `-m, --model <name>` | Built-ins (`gpt-5.1-pro` default, `gpt-5-pro`, `gpt-5.1`, `gpt-5.1-codex`, `gemini-3-pro`, `claude-4.5-sonnet`, `claude-4.1-opus`) plus any OpenRouter id (e.g., `minimax/minimax-m2`, `openai/gpt-4o-mini`). |
|
|
98
|
+
| `-m, --model <name>` | Built-ins (`gpt-5.1-pro` default, `gpt-5-pro`, `gpt-5.1`, `gpt-5.1-codex`, `gpt-5.2`, `gpt-5.2-instant`, `gpt-5.2-pro`, `gemini-3-pro`, `claude-4.5-sonnet`, `claude-4.1-opus`) plus any OpenRouter id (e.g., `minimax/minimax-m2`, `openai/gpt-4o-mini`). |
|
|
99
99
|
| `--models <list>` | Comma-separated API models (mix built-ins and OpenRouter ids) for multi-model runs. |
|
|
100
100
|
| `--base-url <url>` | Point API runs at LiteLLM/Azure/OpenRouter/etc. |
|
|
101
101
|
| `--chatgpt-url <url>` | Target a ChatGPT workspace/folder (browser). |
|
|
@@ -131,7 +131,7 @@ Advanced flags
|
|
|
131
131
|
|
|
132
132
|
| Area | Flags |
|
|
133
133
|
| --- | --- |
|
|
134
|
-
| Browser | `--browser-timeout`, `--browser-input-timeout`, `--browser-inline-cookies[(-file)]`, `--browser-inline-files`, `--browser-bundle-files`, `--browser-keep-browser`, `--browser-headless`, `--browser-hide-window`, `--browser-no-cookie-sync`, `--browser-allow-cookie-errors`, `--browser-chrome-path`, `--browser-cookie-path`, `--chatgpt-url` |
|
|
134
|
+
| Browser | `--browser-timeout`, `--browser-input-timeout`, `--browser-inline-cookies[(-file)]`, `--browser-attachments`, `--browser-inline-files`, `--browser-bundle-files`, `--browser-keep-browser`, `--browser-headless`, `--browser-hide-window`, `--browser-no-cookie-sync`, `--browser-allow-cookie-errors`, `--browser-chrome-path`, `--browser-cookie-path`, `--chatgpt-url` |
|
|
135
135
|
| Azure/OpenAI | `--azure-endpoint`, `--azure-deployment`, `--azure-api-version`, `--base-url` |
|
|
136
136
|
|
|
137
137
|
Remote browser example
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -110,7 +110,7 @@ program
|
|
|
110
110
|
.addOption(new Option('--copy-markdown', 'Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.').default(false))
|
|
111
111
|
.addOption(new Option('--copy').hideHelp().default(false))
|
|
112
112
|
.option('-s, --slug <words>', 'Custom session slug (3-5 words).')
|
|
113
|
-
.option('-m, --model <model>', 'Model to target (gpt-5.1-pro default;
|
|
113
|
+
.option('-m, --model <model>', 'Model to target (gpt-5.1-pro default; aliases to gpt-5.2-pro on API. Also gpt-5-pro, gpt-5.1, gpt-5.1-codex API-only, gpt-5.2, gpt-5.2-instant, gpt-5.2-pro, gemini-3-pro, claude-4.5-sonnet, claude-4.1-opus, or ChatGPT labels like "5.2 Thinking" for browser runs).', normalizeModelOption)
|
|
114
114
|
.addOption(new Option('--models <models>', 'Comma-separated API model list to query in parallel (e.g., "gpt-5.1-pro,gemini-3-pro").')
|
|
115
115
|
.argParser(collectModelList)
|
|
116
116
|
.default([]))
|
|
@@ -172,11 +172,15 @@ program
|
|
|
172
172
|
.addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
|
|
173
173
|
.addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
|
|
174
174
|
.addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
|
|
175
|
+
.addOption(new Option('--browser-extended-thinking', 'Select Extended thinking time for GPT-5.2 Thinking model.').hideHelp())
|
|
175
176
|
.addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
|
|
177
|
+
.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.')
|
|
178
|
+
.choices(['auto', 'never', 'always'])
|
|
179
|
+
.default('auto'))
|
|
176
180
|
.addOption(new Option('--remote-chrome <host:port>', 'Connect to remote Chrome DevTools Protocol (e.g., 192.168.1.10:9222 or [2001:db8::1]:9222 for IPv6).'))
|
|
177
181
|
.addOption(new Option('--remote-host <host:port>', 'Delegate browser runs to a remote `oracle serve` instance.'))
|
|
178
182
|
.addOption(new Option('--remote-token <token>', 'Access token for the remote `oracle serve` instance.'))
|
|
179
|
-
.addOption(new Option('--browser-inline-files', '
|
|
183
|
+
.addOption(new Option('--browser-inline-files', 'Alias for --browser-attachments never (force pasting file contents inline).').default(false))
|
|
180
184
|
.addOption(new Option('--browser-bundle-files', 'Bundle all attachments into a single archive before uploading.').default(false))
|
|
181
185
|
.option('--retain-hours <hours>', 'Prune stored sessions older than this many hours before running (set 0 to disable).', parseFloatOption)
|
|
182
186
|
.option('--force', 'Force start a new session even if an identical prompt is already running.', false)
|
|
@@ -317,6 +321,7 @@ function buildRunOptions(options, overrides = {}) {
|
|
|
317
321
|
sessionId: overrides.sessionId ?? options.sessionId,
|
|
318
322
|
verbose: overrides.verbose ?? options.verbose,
|
|
319
323
|
heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
|
|
324
|
+
browserAttachments: overrides.browserAttachments ?? options.browserAttachments ?? 'auto',
|
|
320
325
|
browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
|
|
321
326
|
browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
|
|
322
327
|
background: overrides.background ?? undefined,
|
|
@@ -359,6 +364,7 @@ function buildRunOptionsFromMetadata(metadata) {
|
|
|
359
364
|
sessionId: metadata.id,
|
|
360
365
|
verbose: stored.verbose,
|
|
361
366
|
heartbeatIntervalMs: stored.heartbeatIntervalMs,
|
|
367
|
+
browserAttachments: stored.browserAttachments,
|
|
362
368
|
browserInlineFiles: stored.browserInlineFiles,
|
|
363
369
|
browserBundleFiles: stored.browserBundleFiles,
|
|
364
370
|
background: stored.background,
|
|
@@ -183,7 +183,9 @@ async function pollAssistantCompletion(Runtime, timeoutMs) {
|
|
|
183
183
|
isStopButtonVisible(Runtime),
|
|
184
184
|
isCompletionVisible(Runtime),
|
|
185
185
|
]);
|
|
186
|
-
|
|
186
|
+
// Require at least 2 stable cycles even when completion buttons are visible
|
|
187
|
+
// to ensure DOM text has fully rendered (buttons can appear before text settles)
|
|
188
|
+
if ((completionVisible && stableCycles >= 2) || (!stopVisible && stableCycles >= requiredStableCycles)) {
|
|
187
189
|
return normalized;
|
|
188
190
|
}
|
|
189
191
|
}
|
|
@@ -211,10 +213,36 @@ async function isCompletionVisible(Runtime) {
|
|
|
211
213
|
try {
|
|
212
214
|
const { result } = await Runtime.evaluate({
|
|
213
215
|
expression: `(() => {
|
|
214
|
-
|
|
216
|
+
// Find the LAST assistant turn to check completion status
|
|
217
|
+
// Must match the same logic as buildAssistantExtractor for consistency
|
|
218
|
+
const ASSISTANT_SELECTOR = '${ASSISTANT_ROLE_SELECTOR}';
|
|
219
|
+
const isAssistantTurn = (node) => {
|
|
220
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
221
|
+
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
222
|
+
if (role === 'assistant') return true;
|
|
223
|
+
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
224
|
+
if (testId.includes('assistant')) return true;
|
|
225
|
+
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const turns = Array.from(document.querySelectorAll('${CONVERSATION_TURN_SELECTOR}'));
|
|
229
|
+
let lastAssistantTurn = null;
|
|
230
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
231
|
+
if (isAssistantTurn(turns[i])) {
|
|
232
|
+
lastAssistantTurn = turns[i];
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!lastAssistantTurn) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
// Check if the last assistant turn has finished action buttons (copy, thumbs up/down, share)
|
|
240
|
+
if (lastAssistantTurn.querySelector('${FINISHED_ACTIONS_SELECTOR}')) {
|
|
215
241
|
return true;
|
|
216
242
|
}
|
|
217
|
-
|
|
243
|
+
// Also check for "Done" text in the last assistant turn's markdown
|
|
244
|
+
const markdowns = lastAssistantTurn.querySelectorAll('.markdown');
|
|
245
|
+
return Array.from(markdowns).some((n) => (n.textContent || '').trim() === 'Done');
|
|
218
246
|
})()`,
|
|
219
247
|
returnByValue: true,
|
|
220
248
|
});
|
|
@@ -257,12 +285,27 @@ function buildAssistantSnapshotExpression() {
|
|
|
257
285
|
}
|
|
258
286
|
function buildResponseObserverExpression(timeoutMs) {
|
|
259
287
|
const selectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
|
|
288
|
+
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
289
|
+
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
260
290
|
return `(() => {
|
|
261
291
|
${buildClickDispatcher()}
|
|
262
292
|
const SELECTORS = ${selectorsLiteral};
|
|
263
293
|
const STOP_SELECTOR = '${STOP_BUTTON_SELECTOR}';
|
|
264
294
|
const FINISHED_SELECTOR = '${FINISHED_ACTIONS_SELECTOR}';
|
|
295
|
+
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
296
|
+
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
265
297
|
const settleDelayMs = 800;
|
|
298
|
+
|
|
299
|
+
// Helper to detect assistant turns - matches buildAssistantExtractor logic
|
|
300
|
+
const isAssistantTurn = (node) => {
|
|
301
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
302
|
+
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
303
|
+
if (role === 'assistant') return true;
|
|
304
|
+
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
305
|
+
if (testId.includes('assistant')) return true;
|
|
306
|
+
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
307
|
+
};
|
|
308
|
+
|
|
266
309
|
${buildAssistantExtractor('extractFromTurns')}
|
|
267
310
|
|
|
268
311
|
const captureViaObserver = () =>
|
|
@@ -307,6 +350,24 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
307
350
|
}, ${timeoutMs});
|
|
308
351
|
});
|
|
309
352
|
|
|
353
|
+
// Check if the last assistant turn has finished (scoped to avoid detecting old turns)
|
|
354
|
+
const isLastAssistantTurnFinished = () => {
|
|
355
|
+
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
356
|
+
let lastAssistantTurn = null;
|
|
357
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
358
|
+
if (isAssistantTurn(turns[i])) {
|
|
359
|
+
lastAssistantTurn = turns[i];
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!lastAssistantTurn) return false;
|
|
364
|
+
// Check for action buttons in this specific turn
|
|
365
|
+
if (lastAssistantTurn.querySelector(FINISHED_SELECTOR)) return true;
|
|
366
|
+
// Check for "Done" text in this turn's markdown
|
|
367
|
+
const markdowns = lastAssistantTurn.querySelectorAll('.markdown');
|
|
368
|
+
return Array.from(markdowns).some((n) => (n.textContent || '').trim() === 'Done');
|
|
369
|
+
};
|
|
370
|
+
|
|
310
371
|
const waitForSettle = async (snapshot) => {
|
|
311
372
|
const settleWindowMs = 5000;
|
|
312
373
|
const settleIntervalMs = 400;
|
|
@@ -321,9 +382,7 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
321
382
|
lastLength = refreshed.text?.length ?? lastLength;
|
|
322
383
|
}
|
|
323
384
|
const stopVisible = Boolean(document.querySelector(STOP_SELECTOR));
|
|
324
|
-
const finishedVisible =
|
|
325
|
-
Boolean(document.querySelector(FINISHED_SELECTOR)) ||
|
|
326
|
-
Array.from(document.querySelectorAll('.markdown')).some((n) => (n.textContent || '').trim() === 'Done');
|
|
385
|
+
const finishedVisible = isLastAssistantTurnFinished();
|
|
327
386
|
|
|
328
387
|
if (!stopVisible || finishedVisible) {
|
|
329
388
|
break;
|
|
@@ -248,6 +248,23 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
248
248
|
testIdTokens.add('gpt5-0');
|
|
249
249
|
testIdTokens.add('gpt50');
|
|
250
250
|
}
|
|
251
|
+
// Numeric variations (5.2 ↔ 52 ↔ gpt-5-2)
|
|
252
|
+
if (base.includes('5.2') || base.includes('5-2') || base.includes('52')) {
|
|
253
|
+
push('5.2', labelTokens);
|
|
254
|
+
push('gpt-5.2', labelTokens);
|
|
255
|
+
push('gpt5.2', labelTokens);
|
|
256
|
+
push('gpt-5-2', labelTokens);
|
|
257
|
+
push('gpt5-2', labelTokens);
|
|
258
|
+
push('gpt52', labelTokens);
|
|
259
|
+
push('chatgpt 5.2', labelTokens);
|
|
260
|
+
if (base.includes('thinking'))
|
|
261
|
+
push('thinking', labelTokens);
|
|
262
|
+
if (base.includes('instant'))
|
|
263
|
+
push('instant', labelTokens);
|
|
264
|
+
testIdTokens.add('gpt-5-2');
|
|
265
|
+
testIdTokens.add('gpt5-2');
|
|
266
|
+
testIdTokens.add('gpt52');
|
|
267
|
+
}
|
|
251
268
|
// Pro / research variants
|
|
252
269
|
if (base.includes('pro')) {
|
|
253
270
|
push('proresearch', labelTokens);
|
|
@@ -263,6 +280,11 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
263
280
|
testIdTokens.add('gpt-5-0-pro');
|
|
264
281
|
testIdTokens.add('gpt50pro');
|
|
265
282
|
}
|
|
283
|
+
if (base.includes('5.2') || base.includes('5-2') || base.includes('52')) {
|
|
284
|
+
testIdTokens.add('gpt-5.2-pro');
|
|
285
|
+
testIdTokens.add('gpt-5-2-pro');
|
|
286
|
+
testIdTokens.add('gpt52pro');
|
|
287
|
+
}
|
|
266
288
|
testIdTokens.add('pro');
|
|
267
289
|
testIdTokens.add('proresearch');
|
|
268
290
|
}
|
|
@@ -2,6 +2,7 @@ import { INPUT_SELECTORS, PROMPT_PRIMARY_SELECTOR, PROMPT_FALLBACK_SELECTOR, SEN
|
|
|
2
2
|
import { delay } from '../utils.js';
|
|
3
3
|
import { logDomFailure } from '../domDebug.js';
|
|
4
4
|
import { buildClickDispatcher } from './domEvents.js';
|
|
5
|
+
import { BrowserAutomationError } from '../../oracle/errors.js';
|
|
5
6
|
const ENTER_KEY_EVENT = {
|
|
6
7
|
key: 'Enter',
|
|
7
8
|
code: 'Enter',
|
|
@@ -70,9 +71,11 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
70
71
|
})()`,
|
|
71
72
|
returnByValue: true,
|
|
72
73
|
});
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
74
|
+
const editorTextRaw = verification.result?.value?.editorText ?? '';
|
|
75
|
+
const fallbackValueRaw = verification.result?.value?.fallbackValue ?? '';
|
|
76
|
+
const editorTextTrimmed = editorTextRaw?.trim?.() ?? '';
|
|
77
|
+
const fallbackValueTrimmed = fallbackValueRaw?.trim?.() ?? '';
|
|
78
|
+
if (!editorTextTrimmed && !fallbackValueTrimmed) {
|
|
76
79
|
await runtime.evaluate({
|
|
77
80
|
expression: `(() => {
|
|
78
81
|
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
@@ -90,6 +93,30 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
90
93
|
})()`,
|
|
91
94
|
});
|
|
92
95
|
}
|
|
96
|
+
const promptLength = prompt.length;
|
|
97
|
+
const postVerification = await runtime.evaluate({
|
|
98
|
+
expression: `(() => {
|
|
99
|
+
const editor = document.querySelector(${primarySelectorLiteral});
|
|
100
|
+
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
101
|
+
return {
|
|
102
|
+
editorText: editor?.innerText ?? '',
|
|
103
|
+
fallbackValue: fallback?.value ?? '',
|
|
104
|
+
};
|
|
105
|
+
})()`,
|
|
106
|
+
returnByValue: true,
|
|
107
|
+
});
|
|
108
|
+
const observedEditor = postVerification.result?.value?.editorText ?? '';
|
|
109
|
+
const observedFallback = postVerification.result?.value?.fallbackValue ?? '';
|
|
110
|
+
const observedLength = Math.max(observedEditor.length, observedFallback.length);
|
|
111
|
+
if (promptLength >= 50_000 && observedLength > 0 && observedLength < promptLength - 2_000) {
|
|
112
|
+
await logDomFailure(runtime, logger, 'prompt-too-large');
|
|
113
|
+
throw new BrowserAutomationError('Prompt appears truncated in the composer (likely too large).', {
|
|
114
|
+
stage: 'submit-prompt',
|
|
115
|
+
code: 'prompt-too-large',
|
|
116
|
+
promptLength,
|
|
117
|
+
observedLength,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
93
120
|
const clicked = await attemptSendButton(runtime, logger, deps?.attachmentNames);
|
|
94
121
|
if (!clicked) {
|
|
95
122
|
await input.dispatchKeyEvent({
|
|
@@ -110,6 +137,35 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
110
137
|
await verifyPromptCommitted(runtime, prompt, 30_000, logger);
|
|
111
138
|
await clickAnswerNowIfPresent(runtime, logger);
|
|
112
139
|
}
|
|
140
|
+
export async function clearPromptComposer(Runtime, logger) {
|
|
141
|
+
const primarySelectorLiteral = JSON.stringify(PROMPT_PRIMARY_SELECTOR);
|
|
142
|
+
const fallbackSelectorLiteral = JSON.stringify(PROMPT_FALLBACK_SELECTOR);
|
|
143
|
+
const result = await Runtime.evaluate({
|
|
144
|
+
expression: `(() => {
|
|
145
|
+
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
146
|
+
const editor = document.querySelector(${primarySelectorLiteral});
|
|
147
|
+
let cleared = false;
|
|
148
|
+
if (fallback) {
|
|
149
|
+
fallback.value = '';
|
|
150
|
+
fallback.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
|
|
151
|
+
fallback.dispatchEvent(new Event('change', { bubbles: true }));
|
|
152
|
+
cleared = true;
|
|
153
|
+
}
|
|
154
|
+
if (editor) {
|
|
155
|
+
editor.textContent = '';
|
|
156
|
+
editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
|
|
157
|
+
cleared = true;
|
|
158
|
+
}
|
|
159
|
+
return { cleared };
|
|
160
|
+
})()`,
|
|
161
|
+
returnByValue: true,
|
|
162
|
+
});
|
|
163
|
+
if (!result.result?.value?.cleared) {
|
|
164
|
+
await logDomFailure(Runtime, logger, 'clear-composer');
|
|
165
|
+
throw new Error('Failed to clear prompt composer');
|
|
166
|
+
}
|
|
167
|
+
await delay(250);
|
|
168
|
+
}
|
|
113
169
|
async function waitForDomReady(Runtime, logger) {
|
|
114
170
|
const deadline = Date.now() + 10_000;
|
|
115
171
|
while (Date.now() < deadline) {
|
|
@@ -264,5 +320,13 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger) {
|
|
|
264
320
|
}).then((res) => JSON.stringify(res?.result?.value)).catch(() => 'unavailable')}`);
|
|
265
321
|
await logDomFailure(Runtime, logger, 'prompt-commit');
|
|
266
322
|
}
|
|
323
|
+
if (prompt.trim().length >= 50_000) {
|
|
324
|
+
throw new BrowserAutomationError('Prompt did not appear in conversation before timeout (likely too large).', {
|
|
325
|
+
stage: 'submit-prompt',
|
|
326
|
+
code: 'prompt-too-large',
|
|
327
|
+
promptLength: prompt.trim().length,
|
|
328
|
+
timeoutMs,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
267
331
|
throw new Error('Prompt did not appear in conversation before timeout (send may have failed)');
|
|
268
332
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR } from '../constants.js';
|
|
2
|
+
import { logDomFailure } from '../domDebug.js';
|
|
3
|
+
import { buildClickDispatcher } from './domEvents.js';
|
|
4
|
+
export async function ensureExtendedThinking(Runtime, logger) {
|
|
5
|
+
const result = await evaluateThinkingTimeSelection(Runtime);
|
|
6
|
+
switch (result?.status) {
|
|
7
|
+
case 'already-extended':
|
|
8
|
+
logger(`Thinking time: ${result.label ?? 'Extended'} (already selected)`);
|
|
9
|
+
return;
|
|
10
|
+
case 'switched':
|
|
11
|
+
logger(`Thinking time: ${result.label ?? 'Extended'}`);
|
|
12
|
+
return;
|
|
13
|
+
case 'chip-not-found': {
|
|
14
|
+
await logDomFailure(Runtime, logger, 'thinking-chip');
|
|
15
|
+
throw new Error('Unable to find the Thinking chip button in the composer area.');
|
|
16
|
+
}
|
|
17
|
+
case 'menu-not-found': {
|
|
18
|
+
await logDomFailure(Runtime, logger, 'thinking-time-menu');
|
|
19
|
+
throw new Error('Unable to find the Thinking time dropdown menu.');
|
|
20
|
+
}
|
|
21
|
+
case 'extended-not-found': {
|
|
22
|
+
await logDomFailure(Runtime, logger, 'extended-option');
|
|
23
|
+
throw new Error('Unable to find the Extended option in the Thinking time menu.');
|
|
24
|
+
}
|
|
25
|
+
default: {
|
|
26
|
+
await logDomFailure(Runtime, logger, 'thinking-time-unknown');
|
|
27
|
+
throw new Error('Unknown error selecting Extended thinking time.');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Best-effort selection of the "Extended" thinking-time option in ChatGPT's composer pill menu.
|
|
33
|
+
* Safe by default: if the pill/menu/option isn't present, we continue without throwing.
|
|
34
|
+
*/
|
|
35
|
+
export async function ensureExtendedThinkingIfAvailable(Runtime, logger) {
|
|
36
|
+
try {
|
|
37
|
+
const result = await evaluateThinkingTimeSelection(Runtime);
|
|
38
|
+
switch (result?.status) {
|
|
39
|
+
case 'already-extended':
|
|
40
|
+
logger(`Thinking time: ${result.label ?? 'Extended'} (already selected)`);
|
|
41
|
+
return true;
|
|
42
|
+
case 'switched':
|
|
43
|
+
logger(`Thinking time: ${result.label ?? 'Extended'}`);
|
|
44
|
+
return true;
|
|
45
|
+
case 'chip-not-found':
|
|
46
|
+
case 'menu-not-found':
|
|
47
|
+
case 'extended-not-found':
|
|
48
|
+
if (logger.verbose) {
|
|
49
|
+
logger(`Thinking time: ${result.status.replaceAll('-', ' ')}; continuing with default.`);
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
default:
|
|
53
|
+
if (logger.verbose) {
|
|
54
|
+
logger('Thinking time: unknown outcome; continuing with default.');
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
if (logger.verbose) {
|
|
62
|
+
logger(`Thinking time selection failed (${message}); continuing with default.`);
|
|
63
|
+
await logDomFailure(Runtime, logger, 'thinking-time');
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function evaluateThinkingTimeSelection(Runtime) {
|
|
69
|
+
const outcome = await Runtime.evaluate({
|
|
70
|
+
expression: buildThinkingTimeExpression(),
|
|
71
|
+
awaitPromise: true,
|
|
72
|
+
returnByValue: true,
|
|
73
|
+
});
|
|
74
|
+
return outcome.result?.value;
|
|
75
|
+
}
|
|
76
|
+
function buildThinkingTimeExpression() {
|
|
77
|
+
const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
|
|
78
|
+
const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
|
|
79
|
+
return `(async () => {
|
|
80
|
+
${buildClickDispatcher()}
|
|
81
|
+
|
|
82
|
+
const MENU_CONTAINER_SELECTOR = ${menuContainerLiteral};
|
|
83
|
+
const MENU_ITEM_SELECTOR = ${menuItemLiteral};
|
|
84
|
+
|
|
85
|
+
const CHIP_SELECTORS = [
|
|
86
|
+
'[data-testid="composer-footer-actions"] button[aria-haspopup="menu"]',
|
|
87
|
+
'button.__composer-pill[aria-haspopup="menu"]',
|
|
88
|
+
'.__composer-pill-composite button[aria-haspopup="menu"]',
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const INITIAL_WAIT_MS = 150;
|
|
92
|
+
const MAX_WAIT_MS = 10000;
|
|
93
|
+
|
|
94
|
+
const normalize = (value) => (value || '')
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
97
|
+
.replace(/\\s+/g, ' ')
|
|
98
|
+
.trim();
|
|
99
|
+
|
|
100
|
+
const findThinkingChip = () => {
|
|
101
|
+
for (const selector of CHIP_SELECTORS) {
|
|
102
|
+
const buttons = document.querySelectorAll(selector);
|
|
103
|
+
for (const btn of buttons) {
|
|
104
|
+
const aria = normalize(btn.getAttribute?.('aria-label') ?? '');
|
|
105
|
+
const text = normalize(btn.textContent ?? '');
|
|
106
|
+
if (aria.includes('thinking') || text.includes('thinking')) {
|
|
107
|
+
return btn;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const chip = findThinkingChip();
|
|
115
|
+
if (!chip) {
|
|
116
|
+
return { status: 'chip-not-found' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
dispatchClickSequence(chip);
|
|
120
|
+
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
const start = performance.now();
|
|
123
|
+
|
|
124
|
+
const findMenu = () => {
|
|
125
|
+
const menus = document.querySelectorAll(MENU_CONTAINER_SELECTOR + ', [role="group"]');
|
|
126
|
+
for (const menu of menus) {
|
|
127
|
+
const label = menu.querySelector?.('.__menu-label, [class*="menu-label"]');
|
|
128
|
+
if (normalize(label?.textContent ?? '').includes('thinking time')) {
|
|
129
|
+
return menu;
|
|
130
|
+
}
|
|
131
|
+
const text = normalize(menu.textContent ?? '');
|
|
132
|
+
if (text.includes('standard') && text.includes('extended')) {
|
|
133
|
+
return menu;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const findExtendedOption = (menu) => {
|
|
140
|
+
const items = menu.querySelectorAll(MENU_ITEM_SELECTOR);
|
|
141
|
+
for (const item of items) {
|
|
142
|
+
const text = normalize(item.textContent ?? '');
|
|
143
|
+
if (text.includes('extended')) {
|
|
144
|
+
return item;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const optionIsSelected = (node) => {
|
|
151
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
152
|
+
const ariaChecked = node.getAttribute('aria-checked');
|
|
153
|
+
const dataState = (node.getAttribute('data-state') || '').toLowerCase();
|
|
154
|
+
if (ariaChecked === 'true') return true;
|
|
155
|
+
if (dataState === 'checked' || dataState === 'selected' || dataState === 'on') return true;
|
|
156
|
+
return false;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const attempt = () => {
|
|
160
|
+
const menu = findMenu();
|
|
161
|
+
if (!menu) {
|
|
162
|
+
if (performance.now() - start > MAX_WAIT_MS) {
|
|
163
|
+
resolve({ status: 'menu-not-found' });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
setTimeout(attempt, 100);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const extendedOption = findExtendedOption(menu);
|
|
171
|
+
if (!extendedOption) {
|
|
172
|
+
resolve({ status: 'extended-not-found' });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const alreadySelected =
|
|
177
|
+
optionIsSelected(extendedOption) ||
|
|
178
|
+
optionIsSelected(extendedOption.querySelector?.('[aria-checked="true"], [data-state="checked"], [data-state="selected"]'));
|
|
179
|
+
const label = extendedOption.textContent?.trim?.() || null;
|
|
180
|
+
dispatchClickSequence(extendedOption);
|
|
181
|
+
resolve({ status: alreadySelected ? 'already-extended' : 'switched', label });
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
setTimeout(attempt, INITIAL_WAIT_MS);
|
|
185
|
+
});
|
|
186
|
+
})()`;
|
|
187
|
+
}
|
|
188
|
+
export function buildThinkingTimeExpressionForTest() {
|
|
189
|
+
return buildThinkingTimeExpression();
|
|
190
|
+
}
|
|
@@ -24,6 +24,7 @@ export const DEFAULT_BROWSER_CONFIG = {
|
|
|
24
24
|
remoteChrome: null,
|
|
25
25
|
manualLogin: false,
|
|
26
26
|
manualLoginProfileDir: null,
|
|
27
|
+
extendedThinking: false,
|
|
27
28
|
};
|
|
28
29
|
export function resolveBrowserConfig(config) {
|
|
29
30
|
const debugPortEnv = parseDebugPort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const CHATGPT_URL = 'https://chatgpt.com/';
|
|
2
|
-
export const DEFAULT_MODEL_TARGET = 'ChatGPT 5.
|
|
2
|
+
export const DEFAULT_MODEL_TARGET = 'ChatGPT 5.2';
|
|
3
3
|
export const COOKIE_URLS = ['https://chatgpt.com', 'https://chat.openai.com', 'https://atlas.openai.com'];
|
|
4
4
|
export const INPUT_SELECTORS = [
|
|
5
5
|
'textarea[data-id="prompt-textarea"]',
|