@steipete/oracle 0.7.0 → 0.7.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/dist/bin/oracle-cli.js +4 -4
- package/dist/src/browser/actions/assistantResponse.js +81 -49
- package/dist/src/browser/actions/attachments.js +37 -3
- package/dist/src/browser/actions/modelSelection.js +94 -5
- package/dist/src/browser/actions/promptComposer.js +22 -14
- package/dist/src/browser/constants.js +6 -2
- package/dist/src/browser/index.js +78 -5
- package/dist/src/cli/browserConfig.js +34 -8
- 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.')
|
|
@@ -218,6 +218,8 @@ async function isCompletionVisible(Runtime) {
|
|
|
218
218
|
const ASSISTANT_SELECTOR = '${ASSISTANT_ROLE_SELECTOR}';
|
|
219
219
|
const isAssistantTurn = (node) => {
|
|
220
220
|
if (!(node instanceof HTMLElement)) return false;
|
|
221
|
+
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
222
|
+
if (turnAttr === 'assistant') return true;
|
|
221
223
|
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
222
224
|
if (role === 'assistant') return true;
|
|
223
225
|
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
@@ -257,6 +259,12 @@ function normalizeAssistantSnapshot(snapshot) {
|
|
|
257
259
|
if (!text.trim()) {
|
|
258
260
|
return null;
|
|
259
261
|
}
|
|
262
|
+
const normalized = text.toLowerCase();
|
|
263
|
+
// "Pro thinking" often renders a placeholder turn containing an "Answer now" gate.
|
|
264
|
+
// Treat it as incomplete so browser mode keeps waiting (and can click the gate).
|
|
265
|
+
if (normalized.includes('answer now') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'))) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
260
268
|
return {
|
|
261
269
|
text,
|
|
262
270
|
html: snapshot?.html ?? undefined,
|
|
@@ -295,10 +303,13 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
295
303
|
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
296
304
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
297
305
|
const settleDelayMs = 800;
|
|
306
|
+
const ANSWER_NOW_LABEL = 'answer now';
|
|
298
307
|
|
|
299
308
|
// Helper to detect assistant turns - matches buildAssistantExtractor logic
|
|
300
309
|
const isAssistantTurn = (node) => {
|
|
301
310
|
if (!(node instanceof HTMLElement)) return false;
|
|
311
|
+
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
312
|
+
if (turnAttr === 'assistant') return true;
|
|
302
313
|
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
303
314
|
if (role === 'assistant') return true;
|
|
304
315
|
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
@@ -330,6 +341,11 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
330
341
|
});
|
|
331
342
|
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
332
343
|
stopInterval = setInterval(() => {
|
|
344
|
+
// Pro thinking can gate the response behind an "Answer now" button. Keep clicking it while present.
|
|
345
|
+
const answerNow = Array.from(document.querySelectorAll('button,span')).find((el) => (el?.textContent || '').trim().toLowerCase() === ANSWER_NOW_LABEL);
|
|
346
|
+
if (answerNow) {
|
|
347
|
+
dispatchClickSequence(answerNow.closest('button') ?? answerNow);
|
|
348
|
+
}
|
|
333
349
|
const stop = document.querySelector(STOP_SELECTOR);
|
|
334
350
|
if (!stop) {
|
|
335
351
|
return;
|
|
@@ -382,9 +398,10 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
382
398
|
lastLength = refreshed.text?.length ?? lastLength;
|
|
383
399
|
}
|
|
384
400
|
const stopVisible = Boolean(document.querySelector(STOP_SELECTOR));
|
|
401
|
+
const answerNowVisible = Boolean(Array.from(document.querySelectorAll('button,span')).find((el) => (el?.textContent || '').trim().toLowerCase() === ANSWER_NOW_LABEL));
|
|
385
402
|
const finishedVisible = isLastAssistantTurnFinished();
|
|
386
403
|
|
|
387
|
-
if (!stopVisible || finishedVisible) {
|
|
404
|
+
if ((!stopVisible && !answerNowVisible) || finishedVisible) {
|
|
388
405
|
break;
|
|
389
406
|
}
|
|
390
407
|
}
|
|
@@ -407,6 +424,10 @@ function buildAssistantExtractor(functionName) {
|
|
|
407
424
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
408
425
|
const isAssistantTurn = (node) => {
|
|
409
426
|
if (!(node instanceof HTMLElement)) return false;
|
|
427
|
+
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
428
|
+
if (turnAttr === 'assistant') {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
410
431
|
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
411
432
|
if (role === 'assistant') {
|
|
412
433
|
return true;
|
|
@@ -443,11 +464,13 @@ function buildAssistantExtractor(functionName) {
|
|
|
443
464
|
}
|
|
444
465
|
const messageRoot = turn.querySelector(ASSISTANT_SELECTOR) ?? turn;
|
|
445
466
|
expandCollapsibles(messageRoot);
|
|
446
|
-
const preferred =
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
467
|
+
const preferred = messageRoot.querySelector('.markdown') || messageRoot.querySelector('[data-message-content]');
|
|
468
|
+
if (!preferred) {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const innerText = preferred?.innerText ?? '';
|
|
472
|
+
const textContent = preferred?.textContent ?? '';
|
|
473
|
+
const text = innerText.trim().length > 0 ? innerText : textContent;
|
|
451
474
|
const html = preferred?.innerHTML ?? '';
|
|
452
475
|
const messageId = messageRoot.getAttribute('data-message-id');
|
|
453
476
|
const turnId = messageRoot.getAttribute('data-testid');
|
|
@@ -462,7 +485,7 @@ function buildCopyExpression(meta) {
|
|
|
462
485
|
return `(() => {
|
|
463
486
|
${buildClickDispatcher()}
|
|
464
487
|
const BUTTON_SELECTOR = '${COPY_BUTTON_SELECTOR}';
|
|
465
|
-
const TIMEOUT_MS =
|
|
488
|
+
const TIMEOUT_MS = 10000;
|
|
466
489
|
|
|
467
490
|
const locateButton = () => {
|
|
468
491
|
const hint = ${JSON.stringify(meta ?? {})};
|
|
@@ -526,53 +549,62 @@ function buildCopyExpression(meta) {
|
|
|
526
549
|
};
|
|
527
550
|
|
|
528
551
|
return new Promise((resolve) => {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
552
|
+
const deadline = Date.now() + TIMEOUT_MS;
|
|
553
|
+
const waitForButton = () => {
|
|
554
|
+
const button = locateButton();
|
|
555
|
+
if (button) {
|
|
556
|
+
const interception = interceptClipboard();
|
|
557
|
+
let settled = false;
|
|
558
|
+
let pollId = null;
|
|
559
|
+
let timeoutId = null;
|
|
560
|
+
const finish = (payload) => {
|
|
561
|
+
if (settled) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
settled = true;
|
|
565
|
+
if (pollId) {
|
|
566
|
+
clearInterval(pollId);
|
|
567
|
+
}
|
|
568
|
+
if (timeoutId) {
|
|
569
|
+
clearTimeout(timeoutId);
|
|
570
|
+
}
|
|
571
|
+
button.removeEventListener('copy', handleCopy, true);
|
|
572
|
+
interception.restore?.();
|
|
573
|
+
resolve(payload);
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const readIntercepted = () => {
|
|
577
|
+
const markdown = interception.state.text ?? '';
|
|
578
|
+
return { success: Boolean(markdown.trim()), markdown };
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const handleCopy = () => {
|
|
582
|
+
finish(readIntercepted());
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
button.addEventListener('copy', handleCopy, true);
|
|
586
|
+
button.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
587
|
+
dispatchClickSequence(button);
|
|
588
|
+
pollId = setInterval(() => {
|
|
589
|
+
const payload = readIntercepted();
|
|
590
|
+
if (payload.success) {
|
|
591
|
+
finish(payload);
|
|
592
|
+
}
|
|
593
|
+
}, 100);
|
|
594
|
+
timeoutId = setTimeout(() => {
|
|
595
|
+
button.removeEventListener('copy', handleCopy, true);
|
|
596
|
+
finish({ success: false, status: 'timeout' });
|
|
597
|
+
}, TIMEOUT_MS);
|
|
540
598
|
return;
|
|
541
599
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
if (timeoutId) {
|
|
547
|
-
clearTimeout(timeoutId);
|
|
600
|
+
if (Date.now() > deadline) {
|
|
601
|
+
resolve({ success: false, status: 'missing-button' });
|
|
602
|
+
return;
|
|
548
603
|
}
|
|
549
|
-
|
|
550
|
-
interception.restore?.();
|
|
551
|
-
resolve(payload);
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
const readIntercepted = () => {
|
|
555
|
-
const markdown = interception.state.text ?? '';
|
|
556
|
-
return { success: Boolean(markdown.trim()), markdown };
|
|
604
|
+
setTimeout(waitForButton, 120);
|
|
557
605
|
};
|
|
558
606
|
|
|
559
|
-
|
|
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);
|
|
607
|
+
waitForButton();
|
|
576
608
|
});
|
|
577
609
|
})()`;
|
|
578
610
|
}
|
|
@@ -214,8 +214,33 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
214
214
|
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
215
215
|
const value = result?.value;
|
|
216
216
|
if (value && !value.uploading) {
|
|
217
|
-
const
|
|
218
|
-
|
|
217
|
+
const attachedNames = (value.attachedNames ?? [])
|
|
218
|
+
.map((name) => name.toLowerCase().replace(/\s+/g, ' ').trim())
|
|
219
|
+
.filter(Boolean);
|
|
220
|
+
const matchesExpected = (expected) => {
|
|
221
|
+
const baseName = expected.split('/').pop()?.split('\\').pop() ?? expected;
|
|
222
|
+
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
223
|
+
const expectedNoExt = normalizedExpected.replace(/\.[a-z0-9]{1,10}$/i, '');
|
|
224
|
+
return attachedNames.some((raw) => {
|
|
225
|
+
if (raw.includes(normalizedExpected))
|
|
226
|
+
return true;
|
|
227
|
+
if (expectedNoExt.length >= 6 && raw.includes(expectedNoExt))
|
|
228
|
+
return true;
|
|
229
|
+
if (raw.includes('…') || raw.includes('...')) {
|
|
230
|
+
const escaped = raw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
231
|
+
const pattern = escaped.replace(/\\…|\\\.\\\.\\\./g, '.*');
|
|
232
|
+
try {
|
|
233
|
+
const re = new RegExp(pattern);
|
|
234
|
+
return re.test(normalizedExpected) || (expectedNoExt.length >= 6 && re.test(expectedNoExt));
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
const missing = expectedNormalized.filter((expected) => !matchesExpected(expected));
|
|
219
244
|
if (missing.length === 0) {
|
|
220
245
|
if (value.state === 'ready') {
|
|
221
246
|
return;
|
|
@@ -223,6 +248,11 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
223
248
|
if (value.state === 'missing' && value.filesAttached) {
|
|
224
249
|
return;
|
|
225
250
|
}
|
|
251
|
+
// If files are attached but button isn't ready yet, give it more time but don't fail immediately
|
|
252
|
+
if (value.filesAttached) {
|
|
253
|
+
await delay(500);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
226
256
|
}
|
|
227
257
|
}
|
|
228
258
|
await delay(250);
|
|
@@ -249,7 +279,11 @@ export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs,
|
|
|
249
279
|
};
|
|
250
280
|
|
|
251
281
|
const turns = Array.from(document.querySelectorAll('article[data-testid^="conversation-turn"]'));
|
|
252
|
-
const userTurns = turns.filter((node) =>
|
|
282
|
+
const userTurns = turns.filter((node) => {
|
|
283
|
+
const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
284
|
+
if (turnAttr === 'user') return true;
|
|
285
|
+
return Boolean(node.querySelector('[data-message-author-role="user"]'));
|
|
286
|
+
});
|
|
253
287
|
const lastUser = userTurns[userTurns.length - 1];
|
|
254
288
|
if (lastUser) {
|
|
255
289
|
const turnMatch = Array.from(lastUser.querySelectorAll('*')).some(matchNode);
|
|
@@ -63,12 +63,41 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
63
63
|
.map((token) => normalizeText(token))
|
|
64
64
|
.filter(Boolean);
|
|
65
65
|
const targetWords = normalizedTarget.split(' ').filter(Boolean);
|
|
66
|
+
const desiredVersion = normalizedTarget.includes('5 2')
|
|
67
|
+
? '5-2'
|
|
68
|
+
: normalizedTarget.includes('5 1')
|
|
69
|
+
? '5-1'
|
|
70
|
+
: normalizedTarget.includes('5 0')
|
|
71
|
+
? '5-0'
|
|
72
|
+
: null;
|
|
73
|
+
const wantsPro = normalizedTarget.includes(' pro') || normalizedTarget.endsWith(' pro') || normalizedTokens.includes('pro');
|
|
74
|
+
const wantsInstant = normalizedTarget.includes('instant');
|
|
75
|
+
const wantsThinking = normalizedTarget.includes('thinking');
|
|
66
76
|
|
|
67
77
|
const button = document.querySelector(BUTTON_SELECTOR);
|
|
68
78
|
if (!button) {
|
|
69
79
|
return { status: 'button-missing' };
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
const getButtonLabel = () => (button.textContent ?? '').trim();
|
|
83
|
+
const buttonMatchesTarget = () => {
|
|
84
|
+
const normalizedLabel = normalizeText(getButtonLabel());
|
|
85
|
+
if (!normalizedLabel) return false;
|
|
86
|
+
if (desiredVersion) {
|
|
87
|
+
if (desiredVersion === '5-2' && !normalizedLabel.includes('5 2')) return false;
|
|
88
|
+
if (desiredVersion === '5-1' && !normalizedLabel.includes('5 1')) return false;
|
|
89
|
+
if (desiredVersion === '5-0' && !normalizedLabel.includes('5 0')) return false;
|
|
90
|
+
}
|
|
91
|
+
if (wantsPro && !normalizedLabel.includes(' pro')) return false;
|
|
92
|
+
if (wantsInstant && !normalizedLabel.includes('instant')) return false;
|
|
93
|
+
if (wantsThinking && !normalizedLabel.includes('thinking')) return false;
|
|
94
|
+
return true;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (buttonMatchesTarget()) {
|
|
98
|
+
return { status: 'already-selected', label: getButtonLabel() };
|
|
99
|
+
}
|
|
100
|
+
|
|
72
101
|
let lastPointerClick = 0;
|
|
73
102
|
const pointerClick = () => {
|
|
74
103
|
if (dispatchClickSequence(button)) {
|
|
@@ -106,8 +135,46 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
106
135
|
}
|
|
107
136
|
let score = 0;
|
|
108
137
|
const normalizedTestId = (testid ?? '').toLowerCase();
|
|
109
|
-
if (normalizedTestId
|
|
110
|
-
|
|
138
|
+
if (normalizedTestId) {
|
|
139
|
+
if (desiredVersion) {
|
|
140
|
+
// data-testid strings have been observed with both dotted and dashed versions (e.g. gpt-5.2-pro vs gpt-5-2-pro).
|
|
141
|
+
const has52 =
|
|
142
|
+
normalizedTestId.includes('5-2') ||
|
|
143
|
+
normalizedTestId.includes('5.2') ||
|
|
144
|
+
normalizedTestId.includes('gpt-5-2') ||
|
|
145
|
+
normalizedTestId.includes('gpt-5.2') ||
|
|
146
|
+
normalizedTestId.includes('gpt52');
|
|
147
|
+
const has51 =
|
|
148
|
+
normalizedTestId.includes('5-1') ||
|
|
149
|
+
normalizedTestId.includes('5.1') ||
|
|
150
|
+
normalizedTestId.includes('gpt-5-1') ||
|
|
151
|
+
normalizedTestId.includes('gpt-5.1') ||
|
|
152
|
+
normalizedTestId.includes('gpt51');
|
|
153
|
+
const has50 =
|
|
154
|
+
normalizedTestId.includes('5-0') ||
|
|
155
|
+
normalizedTestId.includes('5.0') ||
|
|
156
|
+
normalizedTestId.includes('gpt-5-0') ||
|
|
157
|
+
normalizedTestId.includes('gpt-5.0') ||
|
|
158
|
+
normalizedTestId.includes('gpt50');
|
|
159
|
+
const candidateVersion = has52 ? '5-2' : has51 ? '5-1' : has50 ? '5-0' : null;
|
|
160
|
+
// If a candidate advertises a different version, ignore it entirely.
|
|
161
|
+
if (candidateVersion && candidateVersion !== desiredVersion) {
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
// When targeting an explicit version, avoid selecting submenu wrappers that can contain legacy models.
|
|
165
|
+
if (normalizedTestId.includes('submenu') && candidateVersion === null) {
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const matches = TEST_IDS.filter((id) => id && normalizedTestId.includes(id));
|
|
170
|
+
if (matches.length > 0) {
|
|
171
|
+
// Prefer the most specific match (longest token) instead of treating any hit as equal.
|
|
172
|
+
// This prevents generic tokens (e.g. "pro") from outweighing version-specific targets.
|
|
173
|
+
const best = matches.reduce((acc, token) => (token.length > acc.length ? token : acc), '');
|
|
174
|
+
score += 200 + Math.min(900, best.length * 25);
|
|
175
|
+
if (best.startsWith('model-switcher-')) score += 120;
|
|
176
|
+
if (best.includes('gpt-')) score += 60;
|
|
177
|
+
}
|
|
111
178
|
}
|
|
112
179
|
if (normalizedText && normalizedTarget) {
|
|
113
180
|
if (normalizedText === normalizedTarget) {
|
|
@@ -134,6 +201,14 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
134
201
|
}
|
|
135
202
|
score -= missing * 12;
|
|
136
203
|
}
|
|
204
|
+
// If the caller didn't explicitly ask for Pro, prefer non-Pro options when both exist.
|
|
205
|
+
if (wantsPro) {
|
|
206
|
+
if (!normalizedText.includes(' pro')) {
|
|
207
|
+
score -= 80;
|
|
208
|
+
}
|
|
209
|
+
} else if (normalizedText.includes(' pro')) {
|
|
210
|
+
score -= 40;
|
|
211
|
+
}
|
|
137
212
|
return Math.max(score, 0);
|
|
138
213
|
};
|
|
139
214
|
|
|
@@ -153,7 +228,7 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
153
228
|
}
|
|
154
229
|
const label = getOptionLabel(option);
|
|
155
230
|
if (!bestMatch || score > bestMatch.score) {
|
|
156
|
-
bestMatch = { node: option, label, score };
|
|
231
|
+
bestMatch = { node: option, label, score, testid, normalizedText };
|
|
157
232
|
}
|
|
158
233
|
}
|
|
159
234
|
}
|
|
@@ -182,11 +257,25 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
182
257
|
const match = findBestOption();
|
|
183
258
|
if (match) {
|
|
184
259
|
if (optionIsSelected(match.node)) {
|
|
185
|
-
resolve({ status: 'already-selected', label: match.label });
|
|
260
|
+
resolve({ status: 'already-selected', label: getButtonLabel() || match.label });
|
|
186
261
|
return;
|
|
187
262
|
}
|
|
188
263
|
dispatchClickSequence(match.node);
|
|
189
|
-
|
|
264
|
+
// Submenus (e.g. "Legacy models") need a second pass to pick the actual model option.
|
|
265
|
+
// Keep scanning once the submenu opens instead of treating the submenu click as a final switch.
|
|
266
|
+
const isSubmenu = (match.testid ?? '').toLowerCase().includes('submenu');
|
|
267
|
+
if (isSubmenu) {
|
|
268
|
+
setTimeout(attempt, REOPEN_INTERVAL_MS / 2);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// Wait for the top bar label to reflect the requested model; otherwise keep scanning.
|
|
272
|
+
setTimeout(() => {
|
|
273
|
+
if (buttonMatchesTarget()) {
|
|
274
|
+
resolve({ status: 'switched', label: getButtonLabel() || match.label });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
attempt();
|
|
278
|
+
}, Math.max(120, INITIAL_WAIT_MS));
|
|
190
279
|
return;
|
|
191
280
|
}
|
|
192
281
|
if (performance.now() - start > MAX_WAIT_MS) {
|
|
@@ -283,20 +283,28 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger) {
|
|
|
283
283
|
const primarySelectorLiteral = JSON.stringify(PROMPT_PRIMARY_SELECTOR);
|
|
284
284
|
const fallbackSelectorLiteral = JSON.stringify(PROMPT_FALLBACK_SELECTOR);
|
|
285
285
|
const script = `(() => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
286
|
+
const editor = document.querySelector(${primarySelectorLiteral});
|
|
287
|
+
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
288
|
+
const normalize = (value) => {
|
|
289
|
+
let text = value?.toLowerCase?.() ?? '';
|
|
290
|
+
// Strip markdown *markers* but keep content (ChatGPT renders fence markers differently).
|
|
291
|
+
text = text.replace(/\`\`\`[^\\n]*\\n([\\s\\S]*?)\`\`\`/g, ' $1 ');
|
|
292
|
+
text = text.replace(/\`\`\`/g, ' ');
|
|
293
|
+
text = text.replace(/\`([^\`]*)\`/g, '$1');
|
|
294
|
+
return text.replace(/\\s+/g, ' ').trim();
|
|
295
|
+
};
|
|
296
|
+
const normalizedPrompt = normalize(${encodedPrompt});
|
|
297
|
+
const normalizedPromptPrefix = normalizedPrompt.slice(0, 120);
|
|
298
|
+
const CONVERSATION_SELECTOR = ${JSON.stringify(CONVERSATION_TURN_SELECTOR)};
|
|
299
|
+
const articles = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
300
|
+
const normalizedTurns = articles.map((node) => normalize(node?.innerText));
|
|
301
|
+
const userMatched =
|
|
302
|
+
normalizedPrompt.length > 0 && normalizedTurns.some((text) => text.includes(normalizedPrompt));
|
|
303
|
+
const prefixMatched =
|
|
304
|
+
normalizedPromptPrefix.length > 30 &&
|
|
305
|
+
normalizedTurns.some((text) => text.includes(normalizedPromptPrefix));
|
|
306
|
+
const lastTurn = normalizedTurns[normalizedTurns.length - 1] ?? '';
|
|
307
|
+
return {
|
|
300
308
|
userMatched,
|
|
301
309
|
prefixMatched,
|
|
302
310
|
fallbackValue: fallback?.value ?? '',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const CHATGPT_URL = 'https://chatgpt.com/';
|
|
2
|
-
export const DEFAULT_MODEL_TARGET = '
|
|
2
|
+
export const DEFAULT_MODEL_TARGET = 'GPT-5.2 Pro';
|
|
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"]',
|
|
@@ -13,13 +13,17 @@ export const INPUT_SELECTORS = [
|
|
|
13
13
|
];
|
|
14
14
|
export const ANSWER_SELECTORS = [
|
|
15
15
|
'article[data-testid^="conversation-turn"][data-message-author-role="assistant"]',
|
|
16
|
+
'article[data-testid^="conversation-turn"][data-turn="assistant"]',
|
|
16
17
|
'article[data-testid^="conversation-turn"] [data-message-author-role="assistant"]',
|
|
18
|
+
'article[data-testid^="conversation-turn"] [data-turn="assistant"]',
|
|
17
19
|
'article[data-testid^="conversation-turn"] .markdown',
|
|
18
20
|
'[data-message-author-role="assistant"] .markdown',
|
|
21
|
+
'[data-turn="assistant"] .markdown',
|
|
19
22
|
'[data-message-author-role="assistant"]',
|
|
23
|
+
'[data-turn="assistant"]',
|
|
20
24
|
];
|
|
21
25
|
export const CONVERSATION_TURN_SELECTOR = 'article[data-testid^="conversation-turn"]';
|
|
22
|
-
export const ASSISTANT_ROLE_SELECTOR = '[data-message-author-role="assistant"]';
|
|
26
|
+
export const ASSISTANT_ROLE_SELECTOR = '[data-message-author-role="assistant"], [data-turn="assistant"]';
|
|
23
27
|
export const CLOUDFLARE_SCRIPT_SELECTOR = 'script[src*="/challenge-platform/"]';
|
|
24
28
|
export const CLOUDFLARE_TITLE = 'just a moment';
|
|
25
29
|
export const PROMPT_PRIMARY_SELECTOR = '#prompt-textarea';
|
|
@@ -285,7 +285,10 @@ export async function runBrowserMode(options) {
|
|
|
285
285
|
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
286
286
|
await uploadAttachmentFile({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
287
287
|
}
|
|
288
|
-
|
|
288
|
+
// Scale timeout based on number of files: base 30s + 15s per additional file
|
|
289
|
+
const baseTimeout = config.inputTimeoutMs ?? 30_000;
|
|
290
|
+
const perFileTimeout = 15_000;
|
|
291
|
+
const waitBudget = Math.max(baseTimeout, 30_000) + (submissionAttachments.length - 1) * perFileTimeout;
|
|
289
292
|
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
290
293
|
logger('All attachments uploaded');
|
|
291
294
|
}
|
|
@@ -327,10 +330,13 @@ export async function runBrowserMode(options) {
|
|
|
327
330
|
},
|
|
328
331
|
})).catch(() => null);
|
|
329
332
|
answerMarkdown = copiedMarkdown ?? answerText;
|
|
333
|
+
// Helper to normalize text for echo detection (collapse whitespace, lowercase)
|
|
334
|
+
const normalizeForComparison = (text) => text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
330
335
|
// Final sanity check: ensure we didn't accidentally capture the user prompt instead of the assistant turn.
|
|
331
336
|
const finalSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
332
337
|
const finalText = typeof finalSnapshot?.text === 'string' ? finalSnapshot.text.trim() : '';
|
|
333
|
-
if (
|
|
338
|
+
if (!copiedMarkdown &&
|
|
339
|
+
finalText &&
|
|
334
340
|
finalText !== answerMarkdown.trim() &&
|
|
335
341
|
finalText !== promptText.trim() &&
|
|
336
342
|
finalText.length >= answerMarkdown.trim().length) {
|
|
@@ -338,14 +344,26 @@ export async function runBrowserMode(options) {
|
|
|
338
344
|
answerText = finalText;
|
|
339
345
|
answerMarkdown = finalText;
|
|
340
346
|
}
|
|
341
|
-
|
|
347
|
+
// Detect prompt echo using normalized comparison (whitespace-insensitive)
|
|
348
|
+
const normalizedAnswer = normalizeForComparison(answerMarkdown);
|
|
349
|
+
const normalizedPrompt = normalizeForComparison(promptText);
|
|
350
|
+
const promptPrefix = normalizedPrompt.length >= 80
|
|
351
|
+
? normalizedPrompt.slice(0, Math.min(200, normalizedPrompt.length))
|
|
352
|
+
: '';
|
|
353
|
+
const isPromptEcho = normalizedAnswer === normalizedPrompt || (promptPrefix.length > 0 && normalizedAnswer.startsWith(promptPrefix));
|
|
354
|
+
if (isPromptEcho) {
|
|
355
|
+
logger('Detected prompt echo in response; waiting for actual assistant response...');
|
|
342
356
|
const deadline = Date.now() + 8_000;
|
|
343
357
|
let bestText = null;
|
|
344
358
|
let stableCount = 0;
|
|
345
359
|
while (Date.now() < deadline) {
|
|
346
360
|
const snapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
347
361
|
const text = typeof snapshot?.text === 'string' ? snapshot.text.trim() : '';
|
|
348
|
-
|
|
362
|
+
const normalizedText = normalizeForComparison(text);
|
|
363
|
+
const isStillEcho = !text ||
|
|
364
|
+
normalizedText === normalizedPrompt ||
|
|
365
|
+
(promptPrefix.length > 0 && normalizedText.startsWith(promptPrefix));
|
|
366
|
+
if (!isStillEcho) {
|
|
349
367
|
if (!bestText || text.length > bestText.length) {
|
|
350
368
|
bestText = text;
|
|
351
369
|
stableCount = 0;
|
|
@@ -661,7 +679,10 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
661
679
|
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
662
680
|
await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
663
681
|
}
|
|
664
|
-
|
|
682
|
+
// Scale timeout based on number of files: base 30s + 15s per additional file
|
|
683
|
+
const baseTimeout = config.inputTimeoutMs ?? 30_000;
|
|
684
|
+
const perFileTimeout = 15_000;
|
|
685
|
+
const waitBudget = Math.max(baseTimeout, 30_000) + (submissionAttachments.length - 1) * perFileTimeout;
|
|
665
686
|
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
666
687
|
logger('All attachments uploaded');
|
|
667
688
|
}
|
|
@@ -703,6 +724,58 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
703
724
|
},
|
|
704
725
|
}).catch(() => null);
|
|
705
726
|
answerMarkdown = copiedMarkdown ?? answerText;
|
|
727
|
+
// Helper to normalize text for echo detection (collapse whitespace, lowercase)
|
|
728
|
+
const normalizeForComparison = (text) => text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
729
|
+
// Final sanity check: ensure we didn't accidentally capture the user prompt instead of the assistant turn.
|
|
730
|
+
const finalSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
731
|
+
const finalText = typeof finalSnapshot?.text === 'string' ? finalSnapshot.text.trim() : '';
|
|
732
|
+
if (finalText &&
|
|
733
|
+
finalText !== answerMarkdown.trim() &&
|
|
734
|
+
finalText !== promptText.trim() &&
|
|
735
|
+
finalText.length >= answerMarkdown.trim().length) {
|
|
736
|
+
logger('Refreshed assistant response via final DOM snapshot');
|
|
737
|
+
answerText = finalText;
|
|
738
|
+
answerMarkdown = finalText;
|
|
739
|
+
}
|
|
740
|
+
// Detect prompt echo using normalized comparison (whitespace-insensitive)
|
|
741
|
+
const normalizedAnswer = normalizeForComparison(answerMarkdown);
|
|
742
|
+
const normalizedPrompt = normalizeForComparison(promptText);
|
|
743
|
+
const promptPrefix = normalizedPrompt.length >= 80
|
|
744
|
+
? normalizedPrompt.slice(0, Math.min(200, normalizedPrompt.length))
|
|
745
|
+
: '';
|
|
746
|
+
const isPromptEcho = normalizedAnswer === normalizedPrompt || (promptPrefix.length > 0 && normalizedAnswer.startsWith(promptPrefix));
|
|
747
|
+
if (isPromptEcho) {
|
|
748
|
+
logger('Detected prompt echo in response; waiting for actual assistant response...');
|
|
749
|
+
const deadline = Date.now() + 8_000;
|
|
750
|
+
let bestText = null;
|
|
751
|
+
let stableCount = 0;
|
|
752
|
+
while (Date.now() < deadline) {
|
|
753
|
+
const snapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
754
|
+
const text = typeof snapshot?.text === 'string' ? snapshot.text.trim() : '';
|
|
755
|
+
const normalizedText = normalizeForComparison(text);
|
|
756
|
+
const isStillEcho = !text ||
|
|
757
|
+
normalizedText === normalizedPrompt ||
|
|
758
|
+
(promptPrefix.length > 0 && normalizedText.startsWith(promptPrefix));
|
|
759
|
+
if (!isStillEcho) {
|
|
760
|
+
if (!bestText || text.length > bestText.length) {
|
|
761
|
+
bestText = text;
|
|
762
|
+
stableCount = 0;
|
|
763
|
+
}
|
|
764
|
+
else if (text === bestText) {
|
|
765
|
+
stableCount += 1;
|
|
766
|
+
}
|
|
767
|
+
if (stableCount >= 2) {
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
772
|
+
}
|
|
773
|
+
if (bestText) {
|
|
774
|
+
logger('Recovered assistant response after detecting prompt echo');
|
|
775
|
+
answerText = bestText;
|
|
776
|
+
answerMarkdown = bestText;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
706
779
|
stopThinkingMonitor?.();
|
|
707
780
|
const durationMs = Date.now() - startedAt;
|
|
708
781
|
const answerChars = answerText.length;
|
|
@@ -6,19 +6,40 @@ 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
8
|
const BROWSER_MODEL_LABELS = {
|
|
9
|
-
|
|
10
|
-
'gpt-5
|
|
11
|
-
'gpt-5.1': 'GPT-5.
|
|
12
|
-
'gpt-5.
|
|
13
|
-
'gpt-5.2
|
|
9
|
+
// Browser engine supports GPT-5.2 and GPT-5.2 Pro (legacy/Pro aliases normalize to those targets).
|
|
10
|
+
'gpt-5-pro': 'GPT-5.2 Pro',
|
|
11
|
+
'gpt-5.1-pro': 'GPT-5.2 Pro',
|
|
12
|
+
'gpt-5.1': 'GPT-5.2',
|
|
13
|
+
'gpt-5.2': 'GPT-5.2',
|
|
14
|
+
// ChatGPT UI doesn't expose "instant" as a separate picker option; treat it as GPT-5.2 for browser automation.
|
|
15
|
+
'gpt-5.2-instant': 'GPT-5.2',
|
|
14
16
|
'gpt-5.2-pro': 'GPT-5.2 Pro',
|
|
15
17
|
'gemini-3-pro': 'Gemini 3 Pro',
|
|
16
18
|
};
|
|
19
|
+
export function normalizeChatGptModelForBrowser(model) {
|
|
20
|
+
const normalized = model.toLowerCase();
|
|
21
|
+
if (!normalized.startsWith('gpt-') || normalized.includes('codex')) {
|
|
22
|
+
return model;
|
|
23
|
+
}
|
|
24
|
+
// Pro variants: always resolve to the latest Pro model in ChatGPT.
|
|
25
|
+
if (normalized === 'gpt-5-pro' || normalized === 'gpt-5.1-pro' || normalized.endsWith('-pro')) {
|
|
26
|
+
return 'gpt-5.2-pro';
|
|
27
|
+
}
|
|
28
|
+
// Legacy / UI-mismatch variants: map to the closest ChatGPT picker target.
|
|
29
|
+
if (normalized === 'gpt-5.2-instant') {
|
|
30
|
+
return 'gpt-5.2';
|
|
31
|
+
}
|
|
32
|
+
if (normalized === 'gpt-5.1') {
|
|
33
|
+
return 'gpt-5.2';
|
|
34
|
+
}
|
|
35
|
+
return model;
|
|
36
|
+
}
|
|
17
37
|
export async function buildBrowserConfig(options) {
|
|
18
38
|
const desiredModelOverride = options.browserModelLabel?.trim();
|
|
19
39
|
const normalizedOverride = desiredModelOverride?.toLowerCase() ?? '';
|
|
20
40
|
const baseModel = options.model.toLowerCase();
|
|
21
|
-
const
|
|
41
|
+
const isChatGptModel = baseModel.startsWith('gpt-') && !baseModel.includes('codex');
|
|
42
|
+
const shouldUseOverride = !isChatGptModel && normalizedOverride.length > 0 && normalizedOverride !== baseModel;
|
|
22
43
|
const cookieNames = parseCookieNames(options.browserCookieNames ?? process.env.ORACLE_BROWSER_COOKIE_NAMES);
|
|
23
44
|
const inline = await resolveInlineCookies({
|
|
24
45
|
inlineArg: options.browserInlineCookies,
|
|
@@ -51,7 +72,11 @@ export async function buildBrowserConfig(options) {
|
|
|
51
72
|
keepBrowser: options.browserKeepBrowser ? true : undefined,
|
|
52
73
|
manualLogin: options.browserManualLogin ? true : undefined,
|
|
53
74
|
hideWindow: options.browserHideWindow ? true : undefined,
|
|
54
|
-
desiredModel:
|
|
75
|
+
desiredModel: isChatGptModel
|
|
76
|
+
? mapModelToBrowserLabel(options.model)
|
|
77
|
+
: shouldUseOverride
|
|
78
|
+
? desiredModelOverride
|
|
79
|
+
: mapModelToBrowserLabel(options.model),
|
|
55
80
|
debug: options.verbose ? true : undefined,
|
|
56
81
|
// Allow cookie failures by default so runs can continue without Chrome/Keychain secrets.
|
|
57
82
|
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
@@ -69,7 +94,8 @@ function selectBrowserPort(options) {
|
|
|
69
94
|
return candidate;
|
|
70
95
|
}
|
|
71
96
|
export function mapModelToBrowserLabel(model) {
|
|
72
|
-
|
|
97
|
+
const normalized = normalizeChatGptModelForBrowser(model);
|
|
98
|
+
return BROWSER_MODEL_LABELS[normalized] ?? DEFAULT_MODEL_TARGET;
|
|
73
99
|
}
|
|
74
100
|
export function resolveBrowserModelLabel(input, model) {
|
|
75
101
|
const trimmed = input?.trim?.() ?? '';
|
package/dist/src/cli/help.js
CHANGED
|
@@ -38,7 +38,7 @@ export function applyHelpStyling(program, version, isTty) {
|
|
|
38
38
|
program.addHelpText('after', () => renderHelpFooter(program, colors));
|
|
39
39
|
}
|
|
40
40
|
function renderHelpBanner(version, colors) {
|
|
41
|
-
const subtitle = 'Prompt + files required — GPT-5.
|
|
41
|
+
const subtitle = 'Prompt + files required — GPT-5.2 Pro/GPT-5.2 for tough questions with code/file context.';
|
|
42
42
|
return `${colors.banner(`Oracle CLI v${version}`)} ${colors.subtitle(`— ${subtitle}`)}\n`;
|
|
43
43
|
}
|
|
44
44
|
function renderHelpFooter(program, colors) {
|
|
@@ -51,7 +51,7 @@ function renderHelpFooter(program, colors) {
|
|
|
51
51
|
`${colors.bullet('•')} Best results: 6–30 sentences plus key source files; very short prompts often yield generic answers.`,
|
|
52
52
|
`${colors.bullet('•')} Oracle is one-shot: it does not remember prior runs, so start fresh each time with full context.`,
|
|
53
53
|
`${colors.bullet('•')} Run ${colors.accent('--files-report')} to inspect token spend before hitting the API.`,
|
|
54
|
-
`${colors.bullet('•')} Non-preview runs spawn detached sessions (especially gpt-5.
|
|
54
|
+
`${colors.bullet('•')} Non-preview runs spawn detached sessions (especially gpt-5.2-pro API). If the CLI times out, do not re-run — reattach with ${colors.accent('oracle session <slug>')} to resume/inspect the existing run.`,
|
|
55
55
|
`${colors.bullet('•')} Set a memorable 3–5 word slug via ${colors.accent('--slug "<words>"')} to keep session IDs tidy.`,
|
|
56
56
|
`${colors.bullet('•')} Finished sessions auto-hide preamble logs when reattached; raw timestamps remain in the saved log file.`,
|
|
57
57
|
`${colors.bullet('•')} Need hidden flags? Run ${colors.accent(`${program.name()} --help --verbose`)} to list search/token/browser overrides.`,
|
|
@@ -61,7 +61,7 @@ function renderHelpFooter(program, colors) {
|
|
|
61
61
|
const formatExample = (command, description) => `${colors.command(` ${command}`)}\n${colors.muted(` ${description}`)}`;
|
|
62
62
|
const examples = [
|
|
63
63
|
formatExample(`${program.name()} --render --copy --prompt "Review the TS data layer for schema drift" --file "src/**/*.ts,*/*.test.ts"`, 'Build the bundle, print it, and copy it for manual paste into ChatGPT.'),
|
|
64
|
-
formatExample(`${program.name()} --prompt "Cross-check the data layer assumptions" --models gpt-5.
|
|
64
|
+
formatExample(`${program.name()} --prompt "Cross-check the data layer assumptions" --models gpt-5.2-pro,gemini-3-pro --file "src/**/*.ts"`, 'Run multiple API models in one go and aggregate cost/usage.'),
|
|
65
65
|
formatExample(`${program.name()} status --hours 72 --limit 50`, 'Show sessions from the last 72h (capped at 50 entries).'),
|
|
66
66
|
formatExample(`${program.name()} session <sessionId>`, 'Attach to a running/completed session and stream the saved transcript.'),
|
|
67
67
|
formatExample(`${program.name()} --prompt "Ship review" --slug "release-readiness-audit"`, 'Encourage the model to hand you a 3–5 word slug and pass it along with --slug.'),
|
package/dist/src/cli/options.js
CHANGED
|
@@ -137,6 +137,9 @@ export function resolveApiModel(modelValue) {
|
|
|
137
137
|
if (normalized.includes('5-pro') && !normalized.includes('5.1')) {
|
|
138
138
|
return 'gpt-5-pro';
|
|
139
139
|
}
|
|
140
|
+
if (normalized.includes('5.2') && normalized.includes('pro')) {
|
|
141
|
+
return 'gpt-5.2-pro';
|
|
142
|
+
}
|
|
140
143
|
if (normalized.includes('5.1') && normalized.includes('pro')) {
|
|
141
144
|
return 'gpt-5.1-pro';
|
|
142
145
|
}
|
|
@@ -149,6 +152,9 @@ export function resolveApiModel(modelValue) {
|
|
|
149
152
|
if (normalized.includes('gemini')) {
|
|
150
153
|
return 'gemini-3-pro';
|
|
151
154
|
}
|
|
155
|
+
if (normalized.includes('pro')) {
|
|
156
|
+
return 'gpt-5.2-pro';
|
|
157
|
+
}
|
|
152
158
|
// Passthrough for custom/OpenRouter model IDs.
|
|
153
159
|
return normalized;
|
|
154
160
|
}
|
|
@@ -169,12 +175,6 @@ export function inferModelFromLabel(modelValue) {
|
|
|
169
175
|
if (normalized.includes('claude') && normalized.includes('opus')) {
|
|
170
176
|
return 'claude-4.1-opus';
|
|
171
177
|
}
|
|
172
|
-
if (normalized.includes('5.0') || normalized.includes('5-pro')) {
|
|
173
|
-
return 'gpt-5-pro';
|
|
174
|
-
}
|
|
175
|
-
if (normalized.includes('gpt-5') && normalized.includes('pro') && !normalized.includes('5.1')) {
|
|
176
|
-
return 'gpt-5-pro';
|
|
177
|
-
}
|
|
178
178
|
if (normalized.includes('codex')) {
|
|
179
179
|
return 'gpt-5.1-codex';
|
|
180
180
|
}
|
|
@@ -182,13 +182,25 @@ export function inferModelFromLabel(modelValue) {
|
|
|
182
182
|
return 'gemini-3-pro';
|
|
183
183
|
}
|
|
184
184
|
if (normalized.includes('classic')) {
|
|
185
|
-
return 'gpt-5
|
|
185
|
+
return 'gpt-5-pro';
|
|
186
|
+
}
|
|
187
|
+
if ((normalized.includes('5.2') || normalized.includes('5_2')) && normalized.includes('pro')) {
|
|
188
|
+
return 'gpt-5.2-pro';
|
|
189
|
+
}
|
|
190
|
+
if (normalized.includes('5.0') || normalized.includes('5-pro')) {
|
|
191
|
+
return 'gpt-5-pro';
|
|
192
|
+
}
|
|
193
|
+
if (normalized.includes('gpt-5') &&
|
|
194
|
+
normalized.includes('pro') &&
|
|
195
|
+
!normalized.includes('5.1') &&
|
|
196
|
+
!normalized.includes('5.2')) {
|
|
197
|
+
return 'gpt-5-pro';
|
|
186
198
|
}
|
|
187
199
|
if ((normalized.includes('5.1') || normalized.includes('5_1')) && normalized.includes('pro')) {
|
|
188
200
|
return 'gpt-5.1-pro';
|
|
189
201
|
}
|
|
190
202
|
if (normalized.includes('pro')) {
|
|
191
|
-
return 'gpt-5.
|
|
203
|
+
return 'gpt-5.2-pro';
|
|
192
204
|
}
|
|
193
205
|
if (normalized.includes('5.1') || normalized.includes('5_1')) {
|
|
194
206
|
return 'gpt-5.1';
|
|
@@ -3,6 +3,7 @@ import { resolveEngine } from './engine.js';
|
|
|
3
3
|
import { normalizeModelOption, inferModelFromLabel, resolveApiModel, normalizeBaseUrl } from './options.js';
|
|
4
4
|
import { resolveGeminiModelId } from '../oracle/gemini.js';
|
|
5
5
|
import { PromptValidationError } from '../oracle/errors.js';
|
|
6
|
+
import { normalizeChatGptModelForBrowser } from './browserConfig.js';
|
|
6
7
|
export function resolveRunOptionsFromConfig({ prompt, files = [], model, models, engine, userConfig, env = process.env, }) {
|
|
7
8
|
const resolvedEngine = resolveEngineWithConfig({ engine, configEngine: userConfig?.engine, env });
|
|
8
9
|
const browserRequested = engine === 'browser';
|
|
@@ -10,9 +11,11 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
10
11
|
const requestedModelList = Array.isArray(models) ? models : [];
|
|
11
12
|
const normalizedRequestedModels = requestedModelList.map((entry) => normalizeModelOption(entry)).filter(Boolean);
|
|
12
13
|
const cliModelArg = normalizeModelOption(model ?? userConfig?.model) || DEFAULT_MODEL;
|
|
13
|
-
const
|
|
14
|
+
const inferredModel = resolvedEngine === 'browser' && normalizedRequestedModels.length === 0
|
|
14
15
|
? inferModelFromLabel(cliModelArg)
|
|
15
16
|
: resolveApiModel(cliModelArg);
|
|
17
|
+
// Browser engine maps Pro/legacy aliases to the latest ChatGPT picker targets (GPT-5.2 / GPT-5.2 Pro).
|
|
18
|
+
const resolvedModel = resolvedEngine === 'browser' ? normalizeChatGptModelForBrowser(inferredModel) : inferredModel;
|
|
16
19
|
const isCodex = resolvedModel.startsWith('gpt-5.1-codex');
|
|
17
20
|
const isClaude = resolvedModel.startsWith('claude');
|
|
18
21
|
const isGrok = resolvedModel.startsWith('grok');
|
|
@@ -35,10 +35,11 @@ function buildCookieHeader(cookieMap) {
|
|
|
35
35
|
.map(([name, value]) => `${name}=${value}`)
|
|
36
36
|
.join('; ');
|
|
37
37
|
}
|
|
38
|
-
export async function fetchGeminiAccessToken(cookieMap) {
|
|
38
|
+
export async function fetchGeminiAccessToken(cookieMap, signal) {
|
|
39
39
|
const cookieHeader = buildCookieHeader(cookieMap);
|
|
40
40
|
const res = await fetch(GEMINI_APP_URL, {
|
|
41
41
|
redirect: 'follow',
|
|
42
|
+
signal,
|
|
42
43
|
headers: {
|
|
43
44
|
cookie: cookieHeader,
|
|
44
45
|
'user-agent': USER_AGENT,
|
|
@@ -84,10 +85,10 @@ function ensureFullSizeImageUrl(url) {
|
|
|
84
85
|
return url;
|
|
85
86
|
return `${url}=s2048`;
|
|
86
87
|
}
|
|
87
|
-
async function fetchWithCookiePreservingRedirects(url, init, maxRedirects = 10) {
|
|
88
|
+
async function fetchWithCookiePreservingRedirects(url, init, signal, maxRedirects = 10) {
|
|
88
89
|
let current = url;
|
|
89
90
|
for (let i = 0; i <= maxRedirects; i += 1) {
|
|
90
|
-
const res = await fetch(current, { ...init, redirect: 'manual' });
|
|
91
|
+
const res = await fetch(current, { ...init, redirect: 'manual', signal });
|
|
91
92
|
if (res.status >= 300 && res.status < 400) {
|
|
92
93
|
const location = res.headers.get('location');
|
|
93
94
|
if (!location)
|
|
@@ -99,14 +100,14 @@ async function fetchWithCookiePreservingRedirects(url, init, maxRedirects = 10)
|
|
|
99
100
|
}
|
|
100
101
|
throw new Error(`Too many redirects while downloading image (>${maxRedirects}).`);
|
|
101
102
|
}
|
|
102
|
-
async function downloadGeminiImage(url, cookieMap, outputPath) {
|
|
103
|
+
async function downloadGeminiImage(url, cookieMap, outputPath, signal) {
|
|
103
104
|
const cookieHeader = buildCookieHeader(cookieMap);
|
|
104
105
|
const res = await fetchWithCookiePreservingRedirects(ensureFullSizeImageUrl(url), {
|
|
105
106
|
headers: {
|
|
106
107
|
cookie: cookieHeader,
|
|
107
108
|
'user-agent': USER_AGENT,
|
|
108
109
|
},
|
|
109
|
-
});
|
|
110
|
+
}, signal);
|
|
110
111
|
if (!res.ok) {
|
|
111
112
|
throw new Error(`Failed to download image: ${res.status} ${res.statusText} (${res.url})`);
|
|
112
113
|
}
|
|
@@ -114,7 +115,7 @@ async function downloadGeminiImage(url, cookieMap, outputPath) {
|
|
|
114
115
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
115
116
|
await writeFile(outputPath, data);
|
|
116
117
|
}
|
|
117
|
-
async function uploadGeminiFile(filePath) {
|
|
118
|
+
async function uploadGeminiFile(filePath, signal) {
|
|
118
119
|
const absPath = path.resolve(process.cwd(), filePath);
|
|
119
120
|
const data = await readFile(absPath);
|
|
120
121
|
const fileName = path.basename(absPath);
|
|
@@ -123,6 +124,7 @@ async function uploadGeminiFile(filePath) {
|
|
|
123
124
|
const res = await fetch(GEMINI_UPLOAD_URL, {
|
|
124
125
|
method: 'POST',
|
|
125
126
|
redirect: 'follow',
|
|
127
|
+
signal,
|
|
126
128
|
headers: {
|
|
127
129
|
'push-id': GEMINI_UPLOAD_PUSH_ID,
|
|
128
130
|
'user-agent': USER_AGENT,
|
|
@@ -234,10 +236,13 @@ export function isGeminiModelUnavailable(errorCode) {
|
|
|
234
236
|
}
|
|
235
237
|
export async function runGeminiWebOnce(input) {
|
|
236
238
|
const cookieHeader = buildCookieHeader(input.cookieMap);
|
|
237
|
-
const at = await fetchGeminiAccessToken(input.cookieMap);
|
|
239
|
+
const at = await fetchGeminiAccessToken(input.cookieMap, input.signal);
|
|
238
240
|
const uploaded = [];
|
|
239
241
|
for (const file of input.files ?? []) {
|
|
240
|
-
|
|
242
|
+
if (input.signal?.aborted) {
|
|
243
|
+
throw new Error('Gemini web run aborted before upload.');
|
|
244
|
+
}
|
|
245
|
+
uploaded.push(await uploadGeminiFile(file, input.signal));
|
|
241
246
|
}
|
|
242
247
|
const fReq = buildGeminiFReqPayload(input.prompt, uploaded, input.chatMetadata ?? null);
|
|
243
248
|
const params = new URLSearchParams();
|
|
@@ -246,6 +251,7 @@ export async function runGeminiWebOnce(input) {
|
|
|
246
251
|
const res = await fetch(GEMINI_STREAM_GENERATE_URL, {
|
|
247
252
|
method: 'POST',
|
|
248
253
|
redirect: 'follow',
|
|
254
|
+
signal: input.signal,
|
|
249
255
|
headers: {
|
|
250
256
|
'content-type': 'application/x-www-form-urlencoded;charset=utf-8',
|
|
251
257
|
origin: 'https://gemini.google.com',
|
|
@@ -307,15 +313,15 @@ export async function runGeminiWebWithFallback(input) {
|
|
|
307
313
|
}
|
|
308
314
|
return { ...attempt, effectiveModel: input.model };
|
|
309
315
|
}
|
|
310
|
-
export async function saveFirstGeminiImageFromOutput(output, cookieMap, outputPath) {
|
|
316
|
+
export async function saveFirstGeminiImageFromOutput(output, cookieMap, outputPath, signal) {
|
|
311
317
|
const generatedOrWeb = output.images.find((img) => img.kind === 'generated') ?? output.images[0];
|
|
312
318
|
if (generatedOrWeb?.url) {
|
|
313
|
-
await downloadGeminiImage(generatedOrWeb.url, cookieMap, outputPath);
|
|
319
|
+
await downloadGeminiImage(generatedOrWeb.url, cookieMap, outputPath, signal);
|
|
314
320
|
return { saved: true, imageCount: output.images.length };
|
|
315
321
|
}
|
|
316
322
|
const ggdl = extractGgdlUrls(output.rawResponseText);
|
|
317
323
|
if (ggdl[0]) {
|
|
318
|
-
await downloadGeminiImage(ggdl[0], cookieMap, outputPath);
|
|
324
|
+
await downloadGeminiImage(ggdl[0], cookieMap, outputPath, signal);
|
|
319
325
|
return { saved: true, imageCount: ggdl.length };
|
|
320
326
|
}
|
|
321
327
|
return { saved: false, imageCount: 0 };
|
|
@@ -100,6 +100,17 @@ export function createGeminiWebExecutor(geminiOptions) {
|
|
|
100
100
|
if (!cookieMap['__Secure-1PSID'] || !cookieMap['__Secure-1PSIDTS']) {
|
|
101
101
|
throw new Error('Gemini browser mode requires Chrome cookies for google.com (missing __Secure-1PSID/__Secure-1PSIDTS).');
|
|
102
102
|
}
|
|
103
|
+
const configTimeout = typeof runOptions.config?.timeoutMs === 'number' && Number.isFinite(runOptions.config.timeoutMs)
|
|
104
|
+
? Math.max(1_000, runOptions.config.timeoutMs)
|
|
105
|
+
: null;
|
|
106
|
+
const defaultTimeoutMs = geminiOptions.youtube
|
|
107
|
+
? 240_000
|
|
108
|
+
: geminiOptions.generateImage || geminiOptions.editImage
|
|
109
|
+
? 300_000
|
|
110
|
+
: 120_000;
|
|
111
|
+
const timeoutMs = Math.min(configTimeout ?? defaultTimeoutMs, 600_000);
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
103
114
|
const generateImagePath = resolveInvocationPath(geminiOptions.generateImage);
|
|
104
115
|
const editImagePath = resolveInvocationPath(geminiOptions.editImage);
|
|
105
116
|
const outputPath = resolveInvocationPath(geminiOptions.outputPath);
|
|
@@ -116,71 +127,80 @@ export function createGeminiWebExecutor(geminiOptions) {
|
|
|
116
127
|
}
|
|
117
128
|
const model = resolveGeminiWebModel(runOptions.config?.desiredModel, log);
|
|
118
129
|
let response;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
130
|
+
try {
|
|
131
|
+
if (editImagePath) {
|
|
132
|
+
const intro = await runGeminiWebWithFallback({
|
|
133
|
+
prompt: 'Here is an image to edit',
|
|
134
|
+
files: [editImagePath],
|
|
135
|
+
model,
|
|
136
|
+
cookieMap,
|
|
137
|
+
chatMetadata: null,
|
|
138
|
+
signal: controller.signal,
|
|
139
|
+
});
|
|
140
|
+
const editPrompt = `Use image generation tool to ${prompt}`;
|
|
141
|
+
const out = await runGeminiWebWithFallback({
|
|
142
|
+
prompt: editPrompt,
|
|
143
|
+
files: attachmentPaths,
|
|
144
|
+
model,
|
|
145
|
+
cookieMap,
|
|
146
|
+
chatMetadata: intro.metadata,
|
|
147
|
+
signal: controller.signal,
|
|
148
|
+
});
|
|
149
|
+
response = {
|
|
150
|
+
text: out.text ?? null,
|
|
151
|
+
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
152
|
+
has_images: false,
|
|
153
|
+
image_count: 0,
|
|
154
|
+
};
|
|
155
|
+
const resolvedOutputPath = outputPath ?? generateImagePath ?? 'generated.png';
|
|
156
|
+
const imageSave = await saveFirstGeminiImageFromOutput(out, cookieMap, resolvedOutputPath, controller.signal);
|
|
157
|
+
response.has_images = imageSave.saved;
|
|
158
|
+
response.image_count = imageSave.imageCount;
|
|
159
|
+
if (!imageSave.saved) {
|
|
160
|
+
throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
|
|
161
|
+
}
|
|
147
162
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
else if (generateImagePath) {
|
|
164
|
+
const out = await runGeminiWebWithFallback({
|
|
165
|
+
prompt,
|
|
166
|
+
files: attachmentPaths,
|
|
167
|
+
model,
|
|
168
|
+
cookieMap,
|
|
169
|
+
chatMetadata: null,
|
|
170
|
+
signal: controller.signal,
|
|
171
|
+
});
|
|
172
|
+
response = {
|
|
173
|
+
text: out.text ?? null,
|
|
174
|
+
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
175
|
+
has_images: false,
|
|
176
|
+
image_count: 0,
|
|
177
|
+
};
|
|
178
|
+
const imageSave = await saveFirstGeminiImageFromOutput(out, cookieMap, generateImagePath, controller.signal);
|
|
179
|
+
response.has_images = imageSave.saved;
|
|
180
|
+
response.image_count = imageSave.imageCount;
|
|
181
|
+
if (!imageSave.saved) {
|
|
182
|
+
throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const out = await runGeminiWebWithFallback({
|
|
187
|
+
prompt,
|
|
188
|
+
files: attachmentPaths,
|
|
189
|
+
model,
|
|
190
|
+
cookieMap,
|
|
191
|
+
chatMetadata: null,
|
|
192
|
+
signal: controller.signal,
|
|
193
|
+
});
|
|
194
|
+
response = {
|
|
195
|
+
text: out.text ?? null,
|
|
196
|
+
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
197
|
+
has_images: out.images.length > 0,
|
|
198
|
+
image_count: out.images.length,
|
|
199
|
+
};
|
|
168
200
|
}
|
|
169
201
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
prompt,
|
|
173
|
-
files: attachmentPaths,
|
|
174
|
-
model,
|
|
175
|
-
cookieMap,
|
|
176
|
-
chatMetadata: null,
|
|
177
|
-
});
|
|
178
|
-
response = {
|
|
179
|
-
text: out.text ?? null,
|
|
180
|
-
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
181
|
-
has_images: out.images.length > 0,
|
|
182
|
-
image_count: out.images.length,
|
|
183
|
-
};
|
|
202
|
+
finally {
|
|
203
|
+
clearTimeout(timeout);
|
|
184
204
|
}
|
|
185
205
|
const answerText = response.text ?? '';
|
|
186
206
|
let answerMarkdown = answerText;
|
|
@@ -129,7 +129,10 @@ export function registerConsultTool(server) {
|
|
|
129
129
|
let browserConfig;
|
|
130
130
|
if (resolvedEngine === 'browser') {
|
|
131
131
|
const preferredLabel = (browserModelLabel ?? model)?.trim();
|
|
132
|
-
const
|
|
132
|
+
const isChatGptModel = runOptions.model.startsWith('gpt-') && !runOptions.model.includes('codex');
|
|
133
|
+
const desiredModelLabel = isChatGptModel
|
|
134
|
+
? mapModelToBrowserLabel(runOptions.model)
|
|
135
|
+
: resolveBrowserModelLabel(preferredLabel, runOptions.model);
|
|
133
136
|
// Keep the browser path minimal; only forward a desired model label for the ChatGPT picker.
|
|
134
137
|
browserConfig = {
|
|
135
138
|
url: CHATGPT_URL,
|
|
@@ -2,7 +2,7 @@ import { countTokens as countTokensGpt5 } from 'gpt-tokenizer/model/gpt-5';
|
|
|
2
2
|
import { countTokens as countTokensGpt5Pro } from 'gpt-tokenizer/model/gpt-5-pro';
|
|
3
3
|
import { countTokens as countTokensAnthropicRaw } from '@anthropic-ai/tokenizer';
|
|
4
4
|
import { stringifyTokenizerInput } from './tokenStringifier.js';
|
|
5
|
-
export const DEFAULT_MODEL = 'gpt-5.
|
|
5
|
+
export const DEFAULT_MODEL = 'gpt-5.2-pro';
|
|
6
6
|
export const PRO_MODELS = new Set(['gpt-5.1-pro', 'gpt-5-pro', 'gpt-5.2-pro', 'claude-4.5-sonnet', 'claude-4.1-opus']);
|
|
7
7
|
const countTokensAnthropic = (input) => countTokensAnthropicRaw(stringifyTokenizerInput(input));
|
|
8
8
|
export const MODEL_CONFIGS = {
|
package/dist/src/oracle/run.js
CHANGED
|
@@ -191,7 +191,6 @@ export async function runOracle(options, deps = {}) {
|
|
|
191
191
|
(options.model.startsWith('gemini')
|
|
192
192
|
? resolveGeminiModelId(options.model)
|
|
193
193
|
: (modelConfig.apiModel ?? modelConfig.model));
|
|
194
|
-
const headerModelLabel = richTty ? chalk.cyan(modelConfig.model) : modelConfig.model;
|
|
195
194
|
const requestBody = buildRequestBody({
|
|
196
195
|
modelConfig,
|
|
197
196
|
systemPrompt,
|
|
@@ -205,7 +204,13 @@ export async function runOracle(options, deps = {}) {
|
|
|
205
204
|
const tokenLabel = formatTokenEstimate(estimatedInputTokens, (text) => (richTty ? chalk.green(text) : text));
|
|
206
205
|
const fileLabel = richTty ? chalk.magenta(fileCount.toString()) : fileCount.toString();
|
|
207
206
|
const filesPhrase = fileCount === 0 ? 'no files' : `${fileLabel} files`;
|
|
208
|
-
const
|
|
207
|
+
const headerModelLabelBase = richTty ? chalk.cyan(modelConfig.model) : modelConfig.model;
|
|
208
|
+
const headerModelSuffix = effectiveModelId !== modelConfig.model
|
|
209
|
+
? richTty
|
|
210
|
+
? chalk.gray(` (API: ${effectiveModelId})`)
|
|
211
|
+
: ` (API: ${effectiveModelId})`
|
|
212
|
+
: '';
|
|
213
|
+
const headerLine = `Calling ${headerModelLabelBase}${headerModelSuffix} — ${tokenLabel} tokens, ${filesPhrase}.`;
|
|
209
214
|
const shouldReportFiles = (options.filesReport || fileTokenInfo.totalTokens > inputTokenBudget) && fileTokenInfo.stats.length > 0;
|
|
210
215
|
if (!isPreview) {
|
|
211
216
|
if (!options.suppressHeader) {
|
|
@@ -213,9 +218,14 @@ export async function runOracle(options, deps = {}) {
|
|
|
213
218
|
}
|
|
214
219
|
const maskedKey = maskApiKey(apiKey);
|
|
215
220
|
if (maskedKey && options.verbose) {
|
|
216
|
-
const resolvedSuffix = effectiveModelId !== modelConfig.model ? ` (
|
|
221
|
+
const resolvedSuffix = effectiveModelId !== modelConfig.model ? ` (API: ${effectiveModelId})` : '';
|
|
217
222
|
log(dim(`Using ${envVar}=${maskedKey} for model ${modelConfig.model}${resolvedSuffix}`));
|
|
218
223
|
}
|
|
224
|
+
if (!options.suppressHeader &&
|
|
225
|
+
modelConfig.model === 'gpt-5.1-pro' &&
|
|
226
|
+
effectiveModelId === 'gpt-5.2-pro') {
|
|
227
|
+
log(dim('Note: `gpt-5.1-pro` is a stable CLI alias; OpenAI API uses `gpt-5.2-pro`.'));
|
|
228
|
+
}
|
|
219
229
|
if (baseUrl) {
|
|
220
230
|
log(dim(`Base URL: ${formatBaseUrlForLog(baseUrl)}`));
|
|
221
231
|
}
|
|
@@ -442,10 +452,11 @@ export async function runOracle(options, deps = {}) {
|
|
|
442
452
|
if (response.id && response.status === 'in_progress') {
|
|
443
453
|
const polishingStart = now();
|
|
444
454
|
const pollIntervalMs = 2_000;
|
|
445
|
-
const maxWaitMs =
|
|
455
|
+
const maxWaitMs = 180_000;
|
|
446
456
|
log(chalk.dim('Response still in_progress; polling until completion...'));
|
|
447
457
|
// Short polling loop — we don't want to hang forever, just catch late finalization.
|
|
448
458
|
while (now() - polishingStart < maxWaitMs) {
|
|
459
|
+
throwIfTimedOut();
|
|
449
460
|
await wait(pollIntervalMs);
|
|
450
461
|
const refreshed = await clientInstance.responses.retrieve(response.id);
|
|
451
462
|
if (refreshed.status === 'completed') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
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",
|
|
@@ -41,44 +41,44 @@
|
|
|
41
41
|
"homepage": "https://github.com/steipete/oracle#readme",
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
44
|
-
"@google/genai": "^1.
|
|
44
|
+
"@google/genai": "^1.34.0",
|
|
45
45
|
"@google/generative-ai": "^0.24.1",
|
|
46
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
47
47
|
"chalk": "^5.6.2",
|
|
48
48
|
"chrome-cookies-secure": "3.0.0",
|
|
49
49
|
"chrome-launcher": "^1.2.1",
|
|
50
50
|
"chrome-remote-interface": "^0.33.3",
|
|
51
|
-
"clipboardy": "^5.0.
|
|
51
|
+
"clipboardy": "^5.0.2",
|
|
52
52
|
"commander": "^14.0.2",
|
|
53
53
|
"dotenv": "^17.2.3",
|
|
54
54
|
"fast-glob": "^3.3.3",
|
|
55
55
|
"gpt-tokenizer": "^3.4.0",
|
|
56
|
-
"inquirer": "13.0
|
|
56
|
+
"inquirer": "13.1.0",
|
|
57
57
|
"json5": "^2.2.3",
|
|
58
58
|
"keytar": "^7.9.0",
|
|
59
59
|
"kleur": "^4.1.5",
|
|
60
60
|
"markdansi": "^0.1.3",
|
|
61
|
-
"openai": "^6.
|
|
62
|
-
"shiki": "^3.
|
|
61
|
+
"openai": "^6.14.0",
|
|
62
|
+
"shiki": "^3.20.0",
|
|
63
63
|
"sqlite3": "^5.1.7",
|
|
64
64
|
"toasted-notifier": "^10.1.0",
|
|
65
|
-
"zod": "^4.1
|
|
65
|
+
"zod": "^4.2.1"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
69
|
-
"@biomejs/biome": "^2.3.
|
|
69
|
+
"@biomejs/biome": "^2.3.9",
|
|
70
70
|
"@cdktf/node-pty-prebuilt-multiarch": "0.10.2",
|
|
71
71
|
"@types/chrome-remote-interface": "^0.33.0",
|
|
72
72
|
"@types/inquirer": "^9.0.9",
|
|
73
|
-
"@types/node": "^
|
|
74
|
-
"@vitest/coverage-v8": "4.0.
|
|
75
|
-
"devtools-protocol": "
|
|
76
|
-
"es-toolkit": "^1.
|
|
77
|
-
"esbuild": "^0.27.
|
|
78
|
-
"puppeteer-core": "^24.
|
|
73
|
+
"@types/node": "^25.0.3",
|
|
74
|
+
"@vitest/coverage-v8": "4.0.16",
|
|
75
|
+
"devtools-protocol": "0.0.1559729",
|
|
76
|
+
"es-toolkit": "^1.43.0",
|
|
77
|
+
"esbuild": "^0.27.2",
|
|
78
|
+
"puppeteer-core": "^24.33.0",
|
|
79
79
|
"tsx": "^4.21.0",
|
|
80
80
|
"typescript": "^5.9.3",
|
|
81
|
-
"vitest": "^4.0.
|
|
81
|
+
"vitest": "^4.0.16"
|
|
82
82
|
},
|
|
83
83
|
"optionalDependencies": {
|
|
84
84
|
"win-dpapi": "npm:@primno/dpapi@2.0.1"
|