@steipete/oracle 0.7.0 → 0.7.2
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/dist/bin/oracle-cli.js +4 -4
- package/dist/src/browser/actions/assistantResponse.js +125 -73
- package/dist/src/browser/actions/attachments.js +307 -130
- package/dist/src/browser/actions/modelSelection.js +127 -7
- package/dist/src/browser/actions/promptComposer.js +60 -59
- package/dist/src/browser/config.js +7 -2
- package/dist/src/browser/constants.js +6 -2
- package/dist/src/browser/index.js +84 -7
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/utils.js +10 -0
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/browserConfig.js +40 -9
- package/dist/src/cli/help.js +3 -3
- package/dist/src/cli/options.js +20 -8
- package/dist/src/cli/runOptions.js +4 -1
- package/dist/src/gemini-web/client.js +17 -11
- package/dist/src/gemini-web/executor.js +82 -62
- package/dist/src/mcp/tools/consult.js +4 -1
- package/dist/src/oracle/config.js +1 -1
- package/dist/src/oracle/run.js +15 -4
- package/package.json +16 -16
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -87,7 +87,7 @@ program.hook('preAction', (thisCommand) => {
|
|
|
87
87
|
});
|
|
88
88
|
program
|
|
89
89
|
.name('oracle')
|
|
90
|
-
.description('One-shot GPT-5.
|
|
90
|
+
.description('One-shot GPT-5.2 Pro / GPT-5.2 / GPT-5.1 Codex tool for hard questions that benefit from large file context and server-side search.')
|
|
91
91
|
.version(VERSION)
|
|
92
92
|
.argument('[prompt]', 'Prompt text (shorthand for --prompt).')
|
|
93
93
|
.option('-p, --prompt <text>', 'User prompt to send to the model.')
|
|
@@ -112,8 +112,8 @@ program
|
|
|
112
112
|
.addOption(new Option('--copy-markdown', 'Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.').default(false))
|
|
113
113
|
.addOption(new Option('--copy').hideHelp().default(false))
|
|
114
114
|
.option('-s, --slug <words>', 'Custom session slug (3-5 words).')
|
|
115
|
-
.option('-m, --model <model>', 'Model to target (gpt-5.
|
|
116
|
-
.addOption(new Option('--models <models>', 'Comma-separated API model list to query in parallel (e.g., "gpt-5.
|
|
115
|
+
.option('-m, --model <model>', 'Model to target (gpt-5.2-pro default; also supports gpt-5.1-pro alias). 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)
|
|
116
|
+
.addOption(new Option('--models <models>', 'Comma-separated API model list to query in parallel (e.g., "gpt-5.2-pro,gemini-3-pro").')
|
|
117
117
|
.argParser(collectModelList)
|
|
118
118
|
.default([]))
|
|
119
119
|
.addOption(new Option('-e, --engine <mode>', 'Execution engine (api | browser). Browser engine: GPT models automate ChatGPT; Gemini models use a cookie-based client for gemini.google.com. If omitted, oracle picks api when OPENAI_API_KEY is set, otherwise browser.').choices(['api', 'browser']))
|
|
@@ -123,7 +123,7 @@ program
|
|
|
123
123
|
.addOption(new Option('--[no-]notify', 'Desktop notification when a session finishes (default on unless CI/SSH).')
|
|
124
124
|
.default(undefined))
|
|
125
125
|
.addOption(new Option('--[no-]notify-sound', 'Play a notification sound on completion (default off).').default(undefined))
|
|
126
|
-
.addOption(new Option('--timeout <seconds|auto>', 'Overall timeout before aborting the API call (auto = 60m for gpt-5.
|
|
126
|
+
.addOption(new Option('--timeout <seconds|auto>', 'Overall timeout before aborting the API call (auto = 60m for gpt-5.2-pro, 120s otherwise).')
|
|
127
127
|
.argParser(parseTimeoutOption)
|
|
128
128
|
.default('auto'))
|
|
129
129
|
.addOption(new Option('--preview [mode]', '(alias) Preview the request without calling the model (summary | json | full). Deprecated: use --dry-run instead.')
|
|
@@ -4,6 +4,7 @@ import { logDomFailure, logConversationSnapshot, buildConversationDebugExpressio
|
|
|
4
4
|
import { buildClickDispatcher } from './domEvents.js';
|
|
5
5
|
const ASSISTANT_POLL_TIMEOUT_ERROR = 'assistant-response-watchdog-timeout';
|
|
6
6
|
export async function waitForAssistantResponse(Runtime, timeoutMs, logger) {
|
|
7
|
+
const start = Date.now();
|
|
7
8
|
logger('Waiting for ChatGPT response');
|
|
8
9
|
const expression = buildResponseObserverExpression(timeoutMs);
|
|
9
10
|
const evaluationPromise = Runtime.evaluate({ expression, awaitPromise: true, returnByValue: true });
|
|
@@ -61,7 +62,24 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger) {
|
|
|
61
62
|
throw new Error('Unable to capture assistant response');
|
|
62
63
|
}
|
|
63
64
|
const refreshed = await refreshAssistantSnapshot(Runtime, parsed, logger);
|
|
64
|
-
|
|
65
|
+
const candidate = refreshed ?? parsed;
|
|
66
|
+
// The evaluation path can race ahead of completion. If ChatGPT is still streaming, wait for the watchdog poller.
|
|
67
|
+
const elapsedMs = Date.now() - start;
|
|
68
|
+
const remainingMs = Math.max(0, timeoutMs - elapsedMs);
|
|
69
|
+
if (remainingMs > 0) {
|
|
70
|
+
const [stopVisible, completionVisible] = await Promise.all([
|
|
71
|
+
isStopButtonVisible(Runtime),
|
|
72
|
+
isCompletionVisible(Runtime),
|
|
73
|
+
]);
|
|
74
|
+
if (stopVisible && !completionVisible) {
|
|
75
|
+
logger('Assistant still generating; waiting for completion');
|
|
76
|
+
const completed = await pollAssistantCompletion(Runtime, remainingMs);
|
|
77
|
+
if (completed) {
|
|
78
|
+
return completed;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return candidate;
|
|
65
83
|
}
|
|
66
84
|
export async function readAssistantSnapshot(Runtime) {
|
|
67
85
|
const { result } = await Runtime.evaluate({ expression: buildAssistantSnapshotExpression(), returnByValue: true });
|
|
@@ -118,11 +136,14 @@ async function parseAssistantEvaluationResult(Runtime, evaluation, timeoutMs, lo
|
|
|
118
136
|
const messageId = typeof result.value.messageId === 'string'
|
|
119
137
|
? (result.value.messageId ?? undefined)
|
|
120
138
|
: undefined;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
139
|
+
const text = cleanAssistantText(String(result.value.text ?? ''));
|
|
140
|
+
const normalized = text.toLowerCase();
|
|
141
|
+
if (normalized.includes('answer now') &&
|
|
142
|
+
(normalized.includes('pro thinking') || normalized.includes('chatgpt said'))) {
|
|
143
|
+
const recovered = await recoverAssistantResponse(Runtime, Math.min(timeoutMs, 10_000), logger);
|
|
144
|
+
return recovered ?? null;
|
|
145
|
+
}
|
|
146
|
+
return { text, html, meta: { turnId, messageId } };
|
|
126
147
|
}
|
|
127
148
|
const fallbackText = typeof result.value === 'string' ? cleanAssistantText(result.value) : '';
|
|
128
149
|
if (!fallbackText) {
|
|
@@ -218,6 +239,8 @@ async function isCompletionVisible(Runtime) {
|
|
|
218
239
|
const ASSISTANT_SELECTOR = '${ASSISTANT_ROLE_SELECTOR}';
|
|
219
240
|
const isAssistantTurn = (node) => {
|
|
220
241
|
if (!(node instanceof HTMLElement)) return false;
|
|
242
|
+
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
243
|
+
if (turnAttr === 'assistant') return true;
|
|
221
244
|
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
222
245
|
if (role === 'assistant') return true;
|
|
223
246
|
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
@@ -257,6 +280,12 @@ function normalizeAssistantSnapshot(snapshot) {
|
|
|
257
280
|
if (!text.trim()) {
|
|
258
281
|
return null;
|
|
259
282
|
}
|
|
283
|
+
const normalized = text.toLowerCase();
|
|
284
|
+
// "Pro thinking" often renders a placeholder turn containing an "Answer now" gate.
|
|
285
|
+
// Treat it as incomplete so browser mode keeps waiting for the real assistant text.
|
|
286
|
+
if (normalized.includes('answer now') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'))) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
260
289
|
return {
|
|
261
290
|
text,
|
|
262
291
|
html: snapshot?.html ?? undefined,
|
|
@@ -295,10 +324,16 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
295
324
|
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
296
325
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
297
326
|
const settleDelayMs = 800;
|
|
327
|
+
const isAnswerNowPlaceholder = (snapshot) => {
|
|
328
|
+
const normalized = String(snapshot?.text ?? '').toLowerCase();
|
|
329
|
+
return normalized.includes('answer now') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'));
|
|
330
|
+
};
|
|
298
331
|
|
|
299
332
|
// Helper to detect assistant turns - matches buildAssistantExtractor logic
|
|
300
333
|
const isAssistantTurn = (node) => {
|
|
301
334
|
if (!(node instanceof HTMLElement)) return false;
|
|
335
|
+
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
336
|
+
if (turnAttr === 'assistant') return true;
|
|
302
337
|
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
303
338
|
if (role === 'assistant') return true;
|
|
304
339
|
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
@@ -313,7 +348,8 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
313
348
|
const deadline = Date.now() + ${timeoutMs};
|
|
314
349
|
let stopInterval = null;
|
|
315
350
|
const observer = new MutationObserver(() => {
|
|
316
|
-
const
|
|
351
|
+
const extractedRaw = extractFromTurns();
|
|
352
|
+
const extracted = extractedRaw && !isAnswerNowPlaceholder(extractedRaw) ? extractedRaw : null;
|
|
317
353
|
if (extracted) {
|
|
318
354
|
observer.disconnect();
|
|
319
355
|
if (stopInterval) {
|
|
@@ -371,27 +407,28 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
371
407
|
const waitForSettle = async (snapshot) => {
|
|
372
408
|
const settleWindowMs = 5000;
|
|
373
409
|
const settleIntervalMs = 400;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
410
|
+
const deadline = Date.now() + settleWindowMs;
|
|
411
|
+
let latest = snapshot;
|
|
412
|
+
let lastLength = snapshot?.text?.length ?? 0;
|
|
413
|
+
while (Date.now() < deadline) {
|
|
414
|
+
await new Promise((resolve) => setTimeout(resolve, settleIntervalMs));
|
|
415
|
+
const refreshed = extractFromTurns();
|
|
416
|
+
if (refreshed && !isAnswerNowPlaceholder(refreshed) && (refreshed.text?.length ?? 0) >= lastLength) {
|
|
417
|
+
latest = refreshed;
|
|
418
|
+
lastLength = refreshed.text?.length ?? lastLength;
|
|
419
|
+
}
|
|
420
|
+
const stopVisible = Boolean(document.querySelector(STOP_SELECTOR));
|
|
421
|
+
const finishedVisible = isLastAssistantTurnFinished();
|
|
386
422
|
|
|
387
|
-
|
|
388
|
-
|
|
423
|
+
if (!stopVisible || finishedVisible) {
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
389
426
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
};
|
|
427
|
+
return latest ?? snapshot;
|
|
428
|
+
};
|
|
393
429
|
|
|
394
|
-
const
|
|
430
|
+
const extractedRaw = extractFromTurns();
|
|
431
|
+
const extracted = extractedRaw && !isAnswerNowPlaceholder(extractedRaw) ? extractedRaw : null;
|
|
395
432
|
if (extracted) {
|
|
396
433
|
return waitForSettle(extracted);
|
|
397
434
|
}
|
|
@@ -407,6 +444,10 @@ function buildAssistantExtractor(functionName) {
|
|
|
407
444
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
408
445
|
const isAssistantTurn = (node) => {
|
|
409
446
|
if (!(node instanceof HTMLElement)) return false;
|
|
447
|
+
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
448
|
+
if (turnAttr === 'assistant') {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
410
451
|
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
411
452
|
if (role === 'assistant') {
|
|
412
453
|
return true;
|
|
@@ -443,11 +484,13 @@ function buildAssistantExtractor(functionName) {
|
|
|
443
484
|
}
|
|
444
485
|
const messageRoot = turn.querySelector(ASSISTANT_SELECTOR) ?? turn;
|
|
445
486
|
expandCollapsibles(messageRoot);
|
|
446
|
-
const preferred =
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
487
|
+
const preferred = messageRoot.querySelector('.markdown') || messageRoot.querySelector('[data-message-content]');
|
|
488
|
+
if (!preferred) {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const innerText = preferred?.innerText ?? '';
|
|
492
|
+
const textContent = preferred?.textContent ?? '';
|
|
493
|
+
const text = innerText.trim().length > 0 ? innerText : textContent;
|
|
451
494
|
const html = preferred?.innerHTML ?? '';
|
|
452
495
|
const messageId = messageRoot.getAttribute('data-message-id');
|
|
453
496
|
const turnId = messageRoot.getAttribute('data-testid');
|
|
@@ -462,7 +505,7 @@ function buildCopyExpression(meta) {
|
|
|
462
505
|
return `(() => {
|
|
463
506
|
${buildClickDispatcher()}
|
|
464
507
|
const BUTTON_SELECTOR = '${COPY_BUTTON_SELECTOR}';
|
|
465
|
-
const TIMEOUT_MS =
|
|
508
|
+
const TIMEOUT_MS = 10000;
|
|
466
509
|
|
|
467
510
|
const locateButton = () => {
|
|
468
511
|
const hint = ${JSON.stringify(meta ?? {})};
|
|
@@ -526,53 +569,62 @@ function buildCopyExpression(meta) {
|
|
|
526
569
|
};
|
|
527
570
|
|
|
528
571
|
return new Promise((resolve) => {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
572
|
+
const deadline = Date.now() + TIMEOUT_MS;
|
|
573
|
+
const waitForButton = () => {
|
|
574
|
+
const button = locateButton();
|
|
575
|
+
if (button) {
|
|
576
|
+
const interception = interceptClipboard();
|
|
577
|
+
let settled = false;
|
|
578
|
+
let pollId = null;
|
|
579
|
+
let timeoutId = null;
|
|
580
|
+
const finish = (payload) => {
|
|
581
|
+
if (settled) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
settled = true;
|
|
585
|
+
if (pollId) {
|
|
586
|
+
clearInterval(pollId);
|
|
587
|
+
}
|
|
588
|
+
if (timeoutId) {
|
|
589
|
+
clearTimeout(timeoutId);
|
|
590
|
+
}
|
|
591
|
+
button.removeEventListener('copy', handleCopy, true);
|
|
592
|
+
interception.restore?.();
|
|
593
|
+
resolve(payload);
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const readIntercepted = () => {
|
|
597
|
+
const markdown = interception.state.text ?? '';
|
|
598
|
+
return { success: Boolean(markdown.trim()), markdown };
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const handleCopy = () => {
|
|
602
|
+
finish(readIntercepted());
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
button.addEventListener('copy', handleCopy, true);
|
|
606
|
+
button.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
607
|
+
dispatchClickSequence(button);
|
|
608
|
+
pollId = setInterval(() => {
|
|
609
|
+
const payload = readIntercepted();
|
|
610
|
+
if (payload.success) {
|
|
611
|
+
finish(payload);
|
|
612
|
+
}
|
|
613
|
+
}, 100);
|
|
614
|
+
timeoutId = setTimeout(() => {
|
|
615
|
+
button.removeEventListener('copy', handleCopy, true);
|
|
616
|
+
finish({ success: false, status: 'timeout' });
|
|
617
|
+
}, TIMEOUT_MS);
|
|
540
618
|
return;
|
|
541
619
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
if (timeoutId) {
|
|
547
|
-
clearTimeout(timeoutId);
|
|
620
|
+
if (Date.now() > deadline) {
|
|
621
|
+
resolve({ success: false, status: 'missing-button' });
|
|
622
|
+
return;
|
|
548
623
|
}
|
|
549
|
-
|
|
550
|
-
interception.restore?.();
|
|
551
|
-
resolve(payload);
|
|
624
|
+
setTimeout(waitForButton, 120);
|
|
552
625
|
};
|
|
553
626
|
|
|
554
|
-
|
|
555
|
-
const markdown = interception.state.text ?? '';
|
|
556
|
-
return { success: Boolean(markdown.trim()), markdown };
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
const handleCopy = () => {
|
|
560
|
-
finish(readIntercepted());
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
button.addEventListener('copy', handleCopy, true);
|
|
564
|
-
button.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
565
|
-
dispatchClickSequence(button);
|
|
566
|
-
pollId = setInterval(() => {
|
|
567
|
-
const payload = readIntercepted();
|
|
568
|
-
if (payload.success) {
|
|
569
|
-
finish(payload);
|
|
570
|
-
}
|
|
571
|
-
}, 100);
|
|
572
|
-
timeoutId = setTimeout(() => {
|
|
573
|
-
button.removeEventListener('copy', handleCopy, true);
|
|
574
|
-
finish({ success: false, status: 'timeout' });
|
|
575
|
-
}, TIMEOUT_MS);
|
|
627
|
+
waitForButton();
|
|
576
628
|
});
|
|
577
629
|
})()`;
|
|
578
630
|
}
|