@steipete/oracle 0.10.0 → 0.11.0
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 +55 -10
- package/dist/bin/oracle-cli.js +104 -16
- package/dist/src/browser/actions/archiveConversation.js +224 -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 +78 -13
- 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 +26 -2
- 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 +1257 -485
- 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 +40 -0
- 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 +7 -0
- 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 +2 -1
|
@@ -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
|
}
|
|
@@ -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 Extended. Use model "gpt-5.5" with browser thinking time "heavy" for Thinking Heavy.`);
|
|
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');
|
|
@@ -106,6 +132,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
106
132
|
}
|
|
107
133
|
return false;
|
|
108
134
|
};
|
|
135
|
+
const hasProComposerPill = () => Boolean(
|
|
136
|
+
document.querySelector('button.__composer-pill, button[aria-label="Pro, click to remove"]')
|
|
137
|
+
);
|
|
109
138
|
|
|
110
139
|
const button = document.querySelector(BUTTON_SELECTOR);
|
|
111
140
|
if (!button) {
|
|
@@ -136,14 +165,29 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
136
165
|
const getComposerModelLabel = () =>
|
|
137
166
|
(document.querySelector(COMPOSER_MODEL_SIGNAL_SELECTOR)?.textContent ?? '').trim();
|
|
138
167
|
const readComposerModelSignal = () => normalizeText(getComposerModelLabel());
|
|
139
|
-
const
|
|
168
|
+
const withProPillSignal = (label) => {
|
|
169
|
+
const resolved = label || '';
|
|
170
|
+
if (!wantsPro || !hasProComposerPill()) return resolved;
|
|
171
|
+
const normalized = normalizeText(resolved);
|
|
172
|
+
if (!normalized || normalized.includes('pro')) return resolved;
|
|
173
|
+
return resolved + ' + Pro';
|
|
174
|
+
};
|
|
175
|
+
const getResolvedLabel = (fallback) =>
|
|
176
|
+
withProPillSignal(getComposerModelLabel() || getButtonLabel() || fallback);
|
|
140
177
|
if (MODEL_STRATEGY === 'current') {
|
|
141
|
-
|
|
178
|
+
const currentLabel = getResolvedLabel(PRIMARY_LABEL);
|
|
179
|
+
return {
|
|
180
|
+
status: 'already-selected',
|
|
181
|
+
label: currentLabel,
|
|
182
|
+
};
|
|
142
183
|
}
|
|
143
184
|
const buttonMatchesTarget = () => {
|
|
144
185
|
const normalizedLabel = normalizeText(getButtonLabel());
|
|
145
186
|
if (!normalizedLabel) return false;
|
|
146
187
|
if (isTargetGpt55VisibleAlias(normalizedLabel)) return true;
|
|
188
|
+
if (wantsPro && normalizedLabel === 'chatgpt' && hasProComposerPill()) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
147
191
|
if (desiredVersion) {
|
|
148
192
|
if (desiredVersion === '5-5' && !normalizedLabel.includes('5 5')) return false;
|
|
149
193
|
if (desiredVersion === '5-4' && !normalizedLabel.includes('5 4')) return false;
|
|
@@ -211,6 +255,10 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
211
255
|
};
|
|
212
256
|
|
|
213
257
|
const getOptionLabel = (node) => node?.textContent?.trim() ?? '';
|
|
258
|
+
const isThinkingEffortControl = (node) =>
|
|
259
|
+
node instanceof HTMLElement &&
|
|
260
|
+
(node.getAttribute('data-model-picker-thinking-effort-action') === 'true' ||
|
|
261
|
+
Boolean(node.closest('[data-model-picker-thinking-effort-action="true"]')));
|
|
214
262
|
const optionIsSelected = (node) => {
|
|
215
263
|
if (!(node instanceof HTMLElement)) {
|
|
216
264
|
return false;
|
|
@@ -301,6 +349,19 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
301
349
|
}
|
|
302
350
|
}
|
|
303
351
|
const candidateGpt55VisibleAlias = isTargetGpt55VisibleAlias(normalizedText);
|
|
352
|
+
const candidateHasThinking =
|
|
353
|
+
normalizedText.includes('thinking') || normalizedTestId.includes('thinking');
|
|
354
|
+
const candidateHasPro =
|
|
355
|
+
candidateGpt55VisibleAlias ||
|
|
356
|
+
normalizedText === 'pro' ||
|
|
357
|
+
normalizedText.startsWith('pro ') ||
|
|
358
|
+
normalizedText.includes(' pro ') ||
|
|
359
|
+
normalizedText.endsWith(' pro') ||
|
|
360
|
+
normalizedText.includes('proresearch') ||
|
|
361
|
+
normalizedTestId.includes('pro');
|
|
362
|
+
if (wantsPro && candidateHasThinking) return 0;
|
|
363
|
+
if (wantsPro && !candidateHasPro) return 0;
|
|
364
|
+
if (wantsThinking && candidateHasPro) return 0;
|
|
304
365
|
if (desiredVersion === '5-5' && normalizedText && !candidateGpt55VisibleAlias) {
|
|
305
366
|
const candidateHasVersion =
|
|
306
367
|
normalizedText.includes('5 5') ||
|
|
@@ -373,6 +434,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
373
434
|
for (const menu of menus) {
|
|
374
435
|
const buttons = Array.from(menu.querySelectorAll(${menuItemLiteral}));
|
|
375
436
|
for (const option of buttons) {
|
|
437
|
+
if (isThinkingEffortControl(option)) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
376
440
|
const text = option.textContent ?? '';
|
|
377
441
|
const normalizedText = normalizeText(text);
|
|
378
442
|
const testid = option.getAttribute('data-testid') ?? '';
|
|
@@ -391,15 +455,16 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
391
455
|
const waitForTargetSelection = (previousButtonLabel, previousComposerSignal) => new Promise((resolve) => {
|
|
392
456
|
const waitStart = performance.now();
|
|
393
457
|
const check = () => {
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
458
|
+
if (activeSelectionMatchesTarget()) {
|
|
459
|
+
resolve('target');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (selectionStateChanged(previousButtonLabel, previousComposerSignal)) {
|
|
463
|
+
resolve('changed');
|
|
399
464
|
return;
|
|
400
465
|
}
|
|
401
466
|
if (performance.now() - waitStart > SETTLE_WAIT_MS) {
|
|
402
|
-
resolve(
|
|
467
|
+
resolve('timeout');
|
|
403
468
|
return;
|
|
404
469
|
}
|
|
405
470
|
setTimeout(check, 100);
|
|
@@ -450,7 +515,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
450
515
|
ensureMenuOpen();
|
|
451
516
|
const match = findBestOption();
|
|
452
517
|
if (match) {
|
|
453
|
-
if (
|
|
518
|
+
if (activeSelectionMatchesTarget()) {
|
|
454
519
|
closeMenu();
|
|
455
520
|
resolve({ status: 'already-selected', label: getResolvedLabel(match.label) });
|
|
456
521
|
return;
|
|
@@ -467,7 +532,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
467
532
|
}
|
|
468
533
|
// Wait for the selected model signal to settle before reopening the picker.
|
|
469
534
|
waitForTargetSelection(previousButtonLabel, previousComposerSignal).then((selectionSettled) => {
|
|
470
|
-
if (selectionSettled) {
|
|
535
|
+
if (selectionSettled === 'target') {
|
|
471
536
|
closeMenu();
|
|
472
537
|
resolve({ status: 'switched', label: getResolvedLabel(match.label) });
|
|
473
538
|
return;
|
|
@@ -690,6 +755,6 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
690
755
|
testIdTokens: Array.from(testIdTokens).filter(Boolean),
|
|
691
756
|
};
|
|
692
757
|
}
|
|
693
|
-
export function buildModelSelectionExpressionForTest(targetModel) {
|
|
694
|
-
return buildModelSelectionExpression(targetModel,
|
|
758
|
+
export function buildModelSelectionExpressionForTest(targetModel, strategy = "select") {
|
|
759
|
+
return buildModelSelectionExpression(targetModel, strategy);
|
|
695
760
|
}
|
|
@@ -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.
|