@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.
Files changed (52) hide show
  1. package/README.md +56 -11
  2. package/dist/bin/oracle-cli.js +104 -16
  3. package/dist/src/browser/actions/archiveConversation.js +236 -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 +86 -16
  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 +27 -9
  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 +1234 -479
  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 +41 -8
  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 +11 -2
  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 +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
- ? ' You are in Temporary Chat mode; Pro models are not available there. Remove "temporary-chat=true" from --chatgpt-url or use a non-Pro model (e.g. gpt-5.2).'
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
- : 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');
@@ -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
- return label.includes('pro') && label.includes('extended') && !label.includes('thinking');
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
- return label.includes('thinking') && label.includes('heavy') && !label.includes('pro');
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 getResolvedLabel = (fallback) => getComposerModelLabel() || getButtonLabel() || fallback;
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
- return { status: 'already-selected', label: getResolvedLabel(PRIMARY_LABEL) };
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
- activeSelectionMatchesTarget() ||
396
- selectionStateChanged(previousButtonLabel, previousComposerSignal)
397
- ) {
398
- resolve(true);
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(false);
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 (optionIsSelected(match.node) || activeSelectionMatchesTarget()) {
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, "select");
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.