@steipete/oracle 0.10.0 → 0.11.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 +56 -11
- package/dist/bin/oracle-cli.js +104 -16
- package/dist/src/browser/actions/archiveConversation.js +236 -0
- package/dist/src/browser/actions/assistantResponse.js +26 -0
- package/dist/src/browser/actions/deepResearch.js +662 -0
- package/dist/src/browser/actions/modelSelection.js +86 -16
- package/dist/src/browser/actions/navigation.js +22 -0
- package/dist/src/browser/actions/projectSources.js +491 -0
- package/dist/src/browser/actions/promptComposer.js +52 -27
- package/dist/src/browser/actions/thinkingStatus.js +391 -0
- package/dist/src/browser/artifacts.js +150 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptImages.js +315 -0
- package/dist/src/browser/chromeLifecycle.js +214 -3
- package/dist/src/browser/config.js +27 -9
- package/dist/src/browser/constants.js +8 -0
- package/dist/src/browser/controlPlan.js +81 -0
- package/dist/src/browser/detect.js +206 -33
- package/dist/src/browser/domDebug.js +49 -0
- package/dist/src/browser/index.js +1234 -479
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/profileState.js +83 -3
- package/dist/src/browser/projectSourcesRunner.js +366 -0
- package/dist/src/browser/reattach.js +117 -45
- package/dist/src/browser/reattachHelpers.js +1 -1
- package/dist/src/browser/sessionRunner.js +53 -1
- package/dist/src/browser/tabLeaseRegistry.js +182 -0
- package/dist/src/cli/bridge/claudeConfig.js +12 -8
- package/dist/src/cli/bridge/codexConfig.js +2 -2
- package/dist/src/cli/browserConfig.js +41 -8
- package/dist/src/cli/browserDefaults.js +31 -7
- package/dist/src/cli/browserTabs.js +228 -0
- package/dist/src/cli/dryRun.js +33 -1
- package/dist/src/cli/duplicatePromptGuard.js +10 -2
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +4 -0
- package/dist/src/cli/projectSources.js +116 -0
- package/dist/src/cli/sessionCommand.js +51 -0
- package/dist/src/cli/sessionDisplay.js +121 -9
- package/dist/src/cli/sessionRunner.js +51 -7
- package/dist/src/mcp/consultPresets.js +19 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/consult.js +201 -26
- package/dist/src/mcp/tools/projectSources.js +123 -0
- package/dist/src/mcp/types.js +11 -2
- package/dist/src/mcp/utils.js +6 -1
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/projectSources/plan.js +27 -0
- package/dist/src/projectSources/types.js +1 -0
- package/dist/src/projectSources/url.js +23 -0
- package/dist/src/sessionManager.js +1 -0
- package/package.json +7 -6
|
@@ -13,6 +13,9 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
|
|
|
13
13
|
case "switched":
|
|
14
14
|
case "switched-best-effort": {
|
|
15
15
|
const label = result.label ?? desiredModel;
|
|
16
|
+
if (strategy !== "current") {
|
|
17
|
+
assertResolvedModelSelection(desiredModel, label);
|
|
18
|
+
}
|
|
16
19
|
logger(`Model picker: ${label}`);
|
|
17
20
|
return;
|
|
18
21
|
}
|
|
@@ -22,7 +25,7 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
|
|
|
22
25
|
const available = (result.hint?.availableOptions ?? []).filter(Boolean);
|
|
23
26
|
const availableHint = available.length > 0 ? ` Available: ${available.join(", ")}.` : "";
|
|
24
27
|
const tempHint = isTemporary && /\bpro\b/i.test(desiredModel)
|
|
25
|
-
?
|
|
28
|
+
? " You are in Temporary Chat mode; model labels may differ there. If the current Temporary Chat already shows the desired Pro mode, retry with --browser-model-strategy current; otherwise choose an available model or turn Temporary Chat off."
|
|
26
29
|
: "";
|
|
27
30
|
throw new Error(`Unable to find model option matching "${desiredModel}" in the model switcher.${availableHint}${tempHint}`);
|
|
28
31
|
}
|
|
@@ -32,6 +35,29 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
|
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
}
|
|
38
|
+
function assertResolvedModelSelection(desiredModel, resolvedLabel) {
|
|
39
|
+
const desired = desiredModel.toLowerCase();
|
|
40
|
+
const resolved = resolvedLabel.toLowerCase();
|
|
41
|
+
const wantsGpt55Pro = desired === "gpt-5.5-pro" ||
|
|
42
|
+
desired.includes("5.5 pro") ||
|
|
43
|
+
desired.includes("5-5 pro") ||
|
|
44
|
+
(desired.includes("pro") && desired.includes("extended"));
|
|
45
|
+
if (!wantsGpt55Pro || !resolved) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const hasProSignal = resolved.includes(" pro") ||
|
|
49
|
+
resolved.endsWith("pro") ||
|
|
50
|
+
resolved.includes("pro ") ||
|
|
51
|
+
resolved.includes("extended") ||
|
|
52
|
+
resolved.includes("gpt-5.5-pro") ||
|
|
53
|
+
resolved.includes("gpt 5 5 pro");
|
|
54
|
+
if (!hasProSignal || (resolved.includes("thinking") && !resolved.includes("pro"))) {
|
|
55
|
+
throw new Error(`Model picker selected "${resolvedLabel}" while "${desiredModel}" requires GPT-5.5 Pro. Use model "gpt-5.5" with browser thinking time for the Thinking variant.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function assertResolvedModelSelectionForTest(desiredModel, resolvedLabel) {
|
|
59
|
+
assertResolvedModelSelection(desiredModel, resolvedLabel);
|
|
60
|
+
}
|
|
35
61
|
/**
|
|
36
62
|
* Builds the DOM expression that runs inside the ChatGPT tab to select a model.
|
|
37
63
|
* The string is evaluated inside Chrome, so keep it self-contained and well-commented.
|
|
@@ -91,7 +117,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
91
117
|
? '5-1'
|
|
92
118
|
: normalizedTarget.includes('5 0')
|
|
93
119
|
? '5-0'
|
|
94
|
-
|
|
120
|
+
: null;
|
|
95
121
|
const wantsPro = normalizedTarget.includes(' pro') || normalizedTarget.endsWith(' pro') || normalizedTokens.includes('pro');
|
|
96
122
|
const wantsInstant = normalizedTarget.includes('instant');
|
|
97
123
|
const wantsThinking = normalizedTarget.includes('thinking');
|
|
@@ -99,13 +125,21 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
99
125
|
if (desiredVersion !== '5-5') return false;
|
|
100
126
|
const label = normalizeText(value);
|
|
101
127
|
if (wantsPro) {
|
|
102
|
-
|
|
128
|
+
// ChatGPT UI as of 2026-05: the picker shows just "Pro" (no longer "Pro Extended").
|
|
129
|
+
// "Extended" is now a thinking-effort sub-setting, not part of the model label.
|
|
130
|
+
// Accept bare "pro", legacy "pro extended", and reversed "extended pro" (composer pill).
|
|
131
|
+
return (label === 'pro' || label === 'pro extended' || label === 'extended pro') && !label.includes('thinking');
|
|
103
132
|
}
|
|
104
133
|
if (wantsThinking) {
|
|
105
|
-
|
|
134
|
+
// ChatGPT UI as of 2026-05: the picker shows "Thinking" or "Thinking · Extended"
|
|
135
|
+
// (normalized to "thinking extended"). Accept both old "thinking heavy" and new labels.
|
|
136
|
+
return (label === 'thinking' || label === 'thinking extended' || label === 'thinking heavy') && !label.includes('pro');
|
|
106
137
|
}
|
|
107
138
|
return false;
|
|
108
139
|
};
|
|
140
|
+
const hasProComposerPill = () => Boolean(
|
|
141
|
+
document.querySelector('button.__composer-pill, button[aria-label="Pro, click to remove"]')
|
|
142
|
+
);
|
|
109
143
|
|
|
110
144
|
const button = document.querySelector(BUTTON_SELECTOR);
|
|
111
145
|
if (!button) {
|
|
@@ -136,14 +170,29 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
136
170
|
const getComposerModelLabel = () =>
|
|
137
171
|
(document.querySelector(COMPOSER_MODEL_SIGNAL_SELECTOR)?.textContent ?? '').trim();
|
|
138
172
|
const readComposerModelSignal = () => normalizeText(getComposerModelLabel());
|
|
139
|
-
const
|
|
173
|
+
const withProPillSignal = (label) => {
|
|
174
|
+
const resolved = label || '';
|
|
175
|
+
if (!wantsPro || !hasProComposerPill()) return resolved;
|
|
176
|
+
const normalized = normalizeText(resolved);
|
|
177
|
+
if (!normalized || normalized.includes('pro')) return resolved;
|
|
178
|
+
return resolved + ' + Pro';
|
|
179
|
+
};
|
|
180
|
+
const getResolvedLabel = (fallback) =>
|
|
181
|
+
withProPillSignal(getComposerModelLabel() || getButtonLabel() || fallback);
|
|
140
182
|
if (MODEL_STRATEGY === 'current') {
|
|
141
|
-
|
|
183
|
+
const currentLabel = getResolvedLabel(PRIMARY_LABEL);
|
|
184
|
+
return {
|
|
185
|
+
status: 'already-selected',
|
|
186
|
+
label: currentLabel,
|
|
187
|
+
};
|
|
142
188
|
}
|
|
143
189
|
const buttonMatchesTarget = () => {
|
|
144
190
|
const normalizedLabel = normalizeText(getButtonLabel());
|
|
145
191
|
if (!normalizedLabel) return false;
|
|
146
192
|
if (isTargetGpt55VisibleAlias(normalizedLabel)) return true;
|
|
193
|
+
if (wantsPro && normalizedLabel === 'chatgpt' && hasProComposerPill()) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
147
196
|
if (desiredVersion) {
|
|
148
197
|
if (desiredVersion === '5-5' && !normalizedLabel.includes('5 5')) return false;
|
|
149
198
|
if (desiredVersion === '5-4' && !normalizedLabel.includes('5 4')) return false;
|
|
@@ -211,6 +260,10 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
211
260
|
};
|
|
212
261
|
|
|
213
262
|
const getOptionLabel = (node) => node?.textContent?.trim() ?? '';
|
|
263
|
+
const isThinkingEffortControl = (node) =>
|
|
264
|
+
node instanceof HTMLElement &&
|
|
265
|
+
(node.getAttribute('data-model-picker-thinking-effort-action') === 'true' ||
|
|
266
|
+
Boolean(node.closest('[data-model-picker-thinking-effort-action="true"]')));
|
|
214
267
|
const optionIsSelected = (node) => {
|
|
215
268
|
if (!(node instanceof HTMLElement)) {
|
|
216
269
|
return false;
|
|
@@ -301,6 +354,19 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
301
354
|
}
|
|
302
355
|
}
|
|
303
356
|
const candidateGpt55VisibleAlias = isTargetGpt55VisibleAlias(normalizedText);
|
|
357
|
+
const candidateHasThinking =
|
|
358
|
+
normalizedText.includes('thinking') || normalizedTestId.includes('thinking');
|
|
359
|
+
const candidateHasPro =
|
|
360
|
+
candidateGpt55VisibleAlias ||
|
|
361
|
+
normalizedText === 'pro' ||
|
|
362
|
+
normalizedText.startsWith('pro ') ||
|
|
363
|
+
normalizedText.includes(' pro ') ||
|
|
364
|
+
normalizedText.endsWith(' pro') ||
|
|
365
|
+
normalizedText.includes('proresearch') ||
|
|
366
|
+
normalizedTestId.includes('pro');
|
|
367
|
+
if (wantsPro && candidateHasThinking) return 0;
|
|
368
|
+
if (wantsPro && !candidateHasPro) return 0;
|
|
369
|
+
if (wantsThinking && candidateHasPro) return 0;
|
|
304
370
|
if (desiredVersion === '5-5' && normalizedText && !candidateGpt55VisibleAlias) {
|
|
305
371
|
const candidateHasVersion =
|
|
306
372
|
normalizedText.includes('5 5') ||
|
|
@@ -373,6 +439,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
373
439
|
for (const menu of menus) {
|
|
374
440
|
const buttons = Array.from(menu.querySelectorAll(${menuItemLiteral}));
|
|
375
441
|
for (const option of buttons) {
|
|
442
|
+
if (isThinkingEffortControl(option)) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
376
445
|
const text = option.textContent ?? '';
|
|
377
446
|
const normalizedText = normalizeText(text);
|
|
378
447
|
const testid = option.getAttribute('data-testid') ?? '';
|
|
@@ -391,15 +460,16 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
391
460
|
const waitForTargetSelection = (previousButtonLabel, previousComposerSignal) => new Promise((resolve) => {
|
|
392
461
|
const waitStart = performance.now();
|
|
393
462
|
const check = () => {
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
463
|
+
if (activeSelectionMatchesTarget()) {
|
|
464
|
+
resolve('target');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (selectionStateChanged(previousButtonLabel, previousComposerSignal)) {
|
|
468
|
+
resolve('changed');
|
|
399
469
|
return;
|
|
400
470
|
}
|
|
401
471
|
if (performance.now() - waitStart > SETTLE_WAIT_MS) {
|
|
402
|
-
resolve(
|
|
472
|
+
resolve('timeout');
|
|
403
473
|
return;
|
|
404
474
|
}
|
|
405
475
|
setTimeout(check, 100);
|
|
@@ -450,7 +520,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
450
520
|
ensureMenuOpen();
|
|
451
521
|
const match = findBestOption();
|
|
452
522
|
if (match) {
|
|
453
|
-
if (
|
|
523
|
+
if (activeSelectionMatchesTarget()) {
|
|
454
524
|
closeMenu();
|
|
455
525
|
resolve({ status: 'already-selected', label: getResolvedLabel(match.label) });
|
|
456
526
|
return;
|
|
@@ -467,7 +537,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
467
537
|
}
|
|
468
538
|
// Wait for the selected model signal to settle before reopening the picker.
|
|
469
539
|
waitForTargetSelection(previousButtonLabel, previousComposerSignal).then((selectionSettled) => {
|
|
470
|
-
if (selectionSettled) {
|
|
540
|
+
if (selectionSettled === 'target') {
|
|
471
541
|
closeMenu();
|
|
472
542
|
resolve({ status: 'switched', label: getResolvedLabel(match.label) });
|
|
473
543
|
return;
|
|
@@ -690,6 +760,6 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
690
760
|
testIdTokens: Array.from(testIdTokens).filter(Boolean),
|
|
691
761
|
};
|
|
692
762
|
}
|
|
693
|
-
export function buildModelSelectionExpressionForTest(targetModel) {
|
|
694
|
-
return buildModelSelectionExpression(targetModel,
|
|
763
|
+
export function buildModelSelectionExpressionForTest(targetModel, strategy = "select") {
|
|
764
|
+
return buildModelSelectionExpression(targetModel, strategy);
|
|
695
765
|
}
|
|
@@ -132,6 +132,11 @@ export async function ensureNotBlocked(Runtime, headless, logger) {
|
|
|
132
132
|
logger("Cloudflare anti-bot page detected");
|
|
133
133
|
throw new BrowserAutomationError(message, { stage: "cloudflare-challenge", headless });
|
|
134
134
|
}
|
|
135
|
+
if (await isChatGptAccountSecurityBlock(Runtime)) {
|
|
136
|
+
const message = "ChatGPT account security block detected. Open chatgpt.com in Chrome, secure the account, then rerun Oracle.";
|
|
137
|
+
logger("ChatGPT account security block detected");
|
|
138
|
+
throw new BrowserAutomationError(message, { stage: "chatgpt-account-blocked" });
|
|
139
|
+
}
|
|
135
140
|
}
|
|
136
141
|
const LOGIN_CHECK_TIMEOUT_MS = 5_000;
|
|
137
142
|
export async function ensureLoggedIn(Runtime, logger, options = {}) {
|
|
@@ -336,6 +341,23 @@ async function isCloudflareInterstitial(Runtime) {
|
|
|
336
341
|
});
|
|
337
342
|
return Boolean(result.value);
|
|
338
343
|
}
|
|
344
|
+
async function isChatGptAccountSecurityBlock(Runtime) {
|
|
345
|
+
try {
|
|
346
|
+
const outcome = await Runtime.evaluate({
|
|
347
|
+
expression: `(() => {
|
|
348
|
+
const text = String(document.body?.innerText || '').toLowerCase().replace(/\\s+/g, ' ');
|
|
349
|
+
return text.includes('suspicious activity detected') &&
|
|
350
|
+
text.includes('secure your account') &&
|
|
351
|
+
text.includes('regain access');
|
|
352
|
+
})()`,
|
|
353
|
+
returnByValue: true,
|
|
354
|
+
});
|
|
355
|
+
return Boolean(outcome?.result?.value);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
339
361
|
function buildLoginProbeExpression(timeoutMs) {
|
|
340
362
|
return `(async () => {
|
|
341
363
|
// Learned: /backend-api/me is the most reliable "am I logged in" signal.
|