@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.
Files changed (52) hide show
  1. package/README.md +55 -10
  2. package/dist/bin/oracle-cli.js +104 -16
  3. package/dist/src/browser/actions/archiveConversation.js +224 -0
  4. package/dist/src/browser/actions/assistantResponse.js +26 -0
  5. package/dist/src/browser/actions/deepResearch.js +662 -0
  6. package/dist/src/browser/actions/modelSelection.js +78 -13
  7. package/dist/src/browser/actions/navigation.js +22 -0
  8. package/dist/src/browser/actions/projectSources.js +491 -0
  9. package/dist/src/browser/actions/promptComposer.js +52 -27
  10. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  11. package/dist/src/browser/artifacts.js +150 -0
  12. package/dist/src/browser/attachRunning.js +31 -0
  13. package/dist/src/browser/chatgptImages.js +315 -0
  14. package/dist/src/browser/chromeLifecycle.js +214 -3
  15. package/dist/src/browser/config.js +26 -2
  16. package/dist/src/browser/constants.js +8 -0
  17. package/dist/src/browser/controlPlan.js +81 -0
  18. package/dist/src/browser/detect.js +206 -33
  19. package/dist/src/browser/domDebug.js +49 -0
  20. package/dist/src/browser/index.js +1257 -485
  21. package/dist/src/browser/liveTabs.js +434 -0
  22. package/dist/src/browser/profileState.js +83 -3
  23. package/dist/src/browser/projectSourcesRunner.js +366 -0
  24. package/dist/src/browser/reattach.js +117 -45
  25. package/dist/src/browser/reattachHelpers.js +1 -1
  26. package/dist/src/browser/sessionRunner.js +53 -1
  27. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  28. package/dist/src/cli/bridge/claudeConfig.js +12 -8
  29. package/dist/src/cli/bridge/codexConfig.js +2 -2
  30. package/dist/src/cli/browserConfig.js +40 -0
  31. package/dist/src/cli/browserDefaults.js +31 -7
  32. package/dist/src/cli/browserTabs.js +228 -0
  33. package/dist/src/cli/dryRun.js +33 -1
  34. package/dist/src/cli/duplicatePromptGuard.js +10 -2
  35. package/dist/src/cli/help.js +1 -1
  36. package/dist/src/cli/options.js +4 -0
  37. package/dist/src/cli/projectSources.js +116 -0
  38. package/dist/src/cli/sessionCommand.js +51 -0
  39. package/dist/src/cli/sessionDisplay.js +121 -9
  40. package/dist/src/cli/sessionRunner.js +51 -7
  41. package/dist/src/mcp/consultPresets.js +19 -0
  42. package/dist/src/mcp/server.js +2 -0
  43. package/dist/src/mcp/tools/consult.js +201 -26
  44. package/dist/src/mcp/tools/projectSources.js +123 -0
  45. package/dist/src/mcp/types.js +7 -0
  46. package/dist/src/mcp/utils.js +6 -1
  47. package/dist/src/oracle/run.js +4 -1
  48. package/dist/src/projectSources/plan.js +27 -0
  49. package/dist/src/projectSources/types.js +1 -0
  50. package/dist/src/projectSources/url.js +23 -0
  51. package/dist/src/sessionManager.js +1 -0
  52. 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
- : null;
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 getResolvedLabel = (fallback) => getComposerModelLabel() || getButtonLabel() || fallback;
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
- return { status: 'already-selected', label: getResolvedLabel(PRIMARY_LABEL) };
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
- activeSelectionMatchesTarget() ||
396
- selectionStateChanged(previousButtonLabel, previousComposerSignal)
397
- ) {
398
- resolve(true);
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(false);
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 (optionIsSelected(match.node) || activeSelectionMatchesTarget()) {
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, "select");
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.