@steipete/oracle 0.6.1 → 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.
Files changed (31) hide show
  1. package/README.md +16 -8
  2. package/dist/bin/oracle-cli.js +37 -17
  3. package/dist/src/browser/actions/assistantResponse.js +81 -49
  4. package/dist/src/browser/actions/attachments.js +37 -3
  5. package/dist/src/browser/actions/modelSelection.js +94 -5
  6. package/dist/src/browser/actions/promptComposer.js +22 -14
  7. package/dist/src/browser/constants.js +6 -2
  8. package/dist/src/browser/index.js +78 -5
  9. package/dist/src/browser/prompt.js +30 -6
  10. package/dist/src/browser/sessionRunner.js +0 -5
  11. package/dist/src/cli/browserConfig.js +34 -8
  12. package/dist/src/cli/help.js +3 -3
  13. package/dist/src/cli/options.js +20 -8
  14. package/dist/src/cli/runOptions.js +10 -8
  15. package/dist/src/cli/sessionRunner.js +0 -3
  16. package/dist/src/gemini-web/client.js +328 -0
  17. package/dist/src/gemini-web/executor.js +224 -0
  18. package/dist/src/gemini-web/index.js +1 -0
  19. package/dist/src/gemini-web/types.js +1 -0
  20. package/dist/src/mcp/tools/consult.js +4 -1
  21. package/dist/src/oracle/config.js +1 -1
  22. package/dist/src/oracle/run.js +15 -4
  23. package/package.json +17 -17
  24. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  25. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
  26. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  27. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  28. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
  29. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +0 -45
  30. package/dist/vendor/oracle-notifier/oracle-notifier/README.md +0 -24
  31. package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +0 -93
@@ -1,5 +1,5 @@
1
1
  export const CHATGPT_URL = 'https://chatgpt.com/';
2
- export const DEFAULT_MODEL_TARGET = 'ChatGPT 5.2';
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
- const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
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 (finalText &&
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
- if (answerMarkdown.trim() === promptText.trim()) {
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
- if (text && text !== promptText.trim()) {
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
- const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
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,10 +6,32 @@ import { isKnownModel } from '../oracle/modelResolver.js';
6
6
  import { buildPromptMarkdown } from '../oracle/promptAssembly.js';
7
7
  import { buildAttachmentPlan } from './policies.js';
8
8
  const DEFAULT_BROWSER_INLINE_CHAR_BUDGET = 60_000;
9
+ const MEDIA_EXTENSIONS = new Set([
10
+ '.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v',
11
+ '.mp3', '.wav', '.aac', '.flac', '.ogg', '.m4a',
12
+ '.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.heic', '.heif',
13
+ '.pdf',
14
+ ]);
15
+ export function isMediaFile(filePath) {
16
+ const ext = path.extname(filePath).toLowerCase();
17
+ return MEDIA_EXTENSIONS.has(ext);
18
+ }
9
19
  export async function assembleBrowserPrompt(runOptions, deps = {}) {
10
20
  const cwd = deps.cwd ?? process.cwd();
11
21
  const readFilesFn = deps.readFilesImpl ?? readFiles;
12
- const files = await readFilesFn(runOptions.file ?? [], { cwd });
22
+ const allFilePaths = runOptions.file ?? [];
23
+ const textFilePaths = allFilePaths.filter((f) => !isMediaFile(f));
24
+ const mediaFilePaths = allFilePaths.filter((f) => isMediaFile(f));
25
+ const mediaAttachments = await Promise.all(mediaFilePaths.map(async (filePath) => {
26
+ const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
27
+ const stats = await fs.stat(resolvedPath);
28
+ return {
29
+ path: resolvedPath,
30
+ displayPath: path.relative(cwd, resolvedPath) || path.basename(resolvedPath),
31
+ sizeBytes: stats.size,
32
+ };
33
+ }));
34
+ const files = await readFilesFn(textFilePaths, { cwd });
13
35
  const basePrompt = (runOptions.prompt ?? '').trim();
14
36
  const userPrompt = basePrompt;
15
37
  const systemPrompt = runOptions.system?.trim() || '';
@@ -40,9 +62,10 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
40
62
  .filter(Boolean)
41
63
  .join('\n\n')
42
64
  .trim();
43
- const attachments = selectedPlan.attachments.slice();
65
+ const attachments = [...selectedPlan.attachments, ...mediaAttachments];
44
66
  const shouldBundle = selectedPlan.shouldBundle;
45
67
  let bundleText = null;
68
+ let bundled = null;
46
69
  if (shouldBundle) {
47
70
  const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
48
71
  const bundlePath = path.join(bundleDir, 'attachments-bundle.txt');
@@ -59,6 +82,8 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
59
82
  displayPath: bundlePath,
60
83
  sizeBytes: Buffer.byteLength(bundleText, 'utf8'),
61
84
  });
85
+ attachments.push(...mediaAttachments);
86
+ bundled = { originalCount: sections.length, bundlePath };
62
87
  }
63
88
  const inlineFileCount = selectedPlan.inlineFileCount;
64
89
  const modelConfig = isKnownModel(runOptions.model) ? MODEL_CONFIGS[runOptions.model] : MODEL_CONFIGS['gpt-5.1'];
@@ -85,7 +110,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
85
110
  let fallback = null;
86
111
  if (attachmentsPolicy === 'auto' && selectedPlan.mode === 'inline' && sections.length > 0) {
87
112
  const fallbackComposerText = baseComposerSections.join('\n\n').trim();
88
- const fallbackAttachments = uploadPlan.attachments.slice();
113
+ const fallbackAttachments = [...uploadPlan.attachments, ...mediaAttachments];
89
114
  let fallbackBundled = null;
90
115
  if (uploadPlan.shouldBundle) {
91
116
  const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
@@ -103,6 +128,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
103
128
  displayPath: bundlePath,
104
129
  sizeBytes: Buffer.byteLength(fallbackBundleText, 'utf8'),
105
130
  });
131
+ fallbackAttachments.push(...mediaAttachments);
106
132
  fallbackBundled = { originalCount: sections.length, bundlePath };
107
133
  }
108
134
  fallback = {
@@ -121,8 +147,6 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
121
147
  attachmentsPolicy,
122
148
  attachmentMode: selectedPlan.mode,
123
149
  fallback,
124
- bundled: shouldBundle && attachments.length === 1 && attachments[0]?.displayPath
125
- ? { originalCount: sections.length, bundlePath: attachments[0].displayPath }
126
- : null,
150
+ bundled,
127
151
  };
128
152
  }
@@ -5,11 +5,6 @@ import { runBrowserMode } from '../browserMode.js';
5
5
  import { assembleBrowserPrompt } from './prompt.js';
6
6
  import { BrowserAutomationError } from '../oracle/errors.js';
7
7
  export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, deps = {}) {
8
- if (runOptions.model.startsWith('gemini')) {
9
- throw new BrowserAutomationError('Gemini models are not available in browser mode. Re-run with --engine api.', {
10
- stage: 'preflight',
11
- });
12
- }
13
8
  const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
14
9
  const executeBrowser = deps.executeBrowser ?? runBrowserMode;
15
10
  const promptArtifacts = await assemblePrompt(runOptions, { cwd });
@@ -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
- 'gpt-5-pro': 'GPT-5 Pro',
10
- 'gpt-5.1-pro': 'GPT-5.1 Pro',
11
- 'gpt-5.1': 'GPT-5.1',
12
- 'gpt-5.2': 'GPT-5.2 Thinking',
13
- 'gpt-5.2-instant': 'GPT-5.2 Instant',
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 shouldUseOverride = normalizedOverride.length > 0 && normalizedOverride !== baseModel;
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: shouldUseOverride ? desiredModelOverride : mapModelToBrowserLabel(options.model),
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
- return BROWSER_MODEL_LABELS[model] ?? DEFAULT_MODEL_TARGET;
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?.() ?? '';
@@ -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.1 Pro/GPT-5.1 for tough questions with code/file context.';
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.1-pro API). If the CLI times out, do not re-run — reattach with ${colors.accent('oracle session <slug>')} to resume/inspect the existing run.`,
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.1-pro,gemini-3-pro --file "src/**/*.ts"`, 'Run multiple API models in one go and aggregate cost/usage.'),
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.'),
@@ -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.1-pro';
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.1-pro';
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,10 +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 resolvedModel = resolvedEngine === 'browser' && normalizedRequestedModels.length === 0
14
+ const inferredModel = resolvedEngine === 'browser' && normalizedRequestedModels.length === 0
14
15
  ? inferModelFromLabel(cliModelArg)
15
16
  : resolveApiModel(cliModelArg);
16
- const isGemini = resolvedModel.startsWith('gemini');
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;
17
19
  const isCodex = resolvedModel.startsWith('gpt-5.1-codex');
18
20
  const isClaude = resolvedModel.startsWith('claude');
19
21
  const isGrok = resolvedModel.startsWith('grok');
@@ -21,13 +23,13 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
21
23
  const allModels = normalizedRequestedModels.length > 0
22
24
  ? Array.from(new Set(normalizedRequestedModels.map((entry) => resolveApiModel(entry))))
23
25
  : [resolvedModel];
24
- const hasNonGptBrowserTarget = (browserRequested || browserConfigured) && allModels.some((m) => !m.startsWith('gpt-'));
25
- if (hasNonGptBrowserTarget) {
26
- throw new PromptValidationError('Browser engine only supports GPT-series ChatGPT models. Re-run with --engine api for Grok, Claude, Gemini, or other non-GPT models.', { engine: 'browser', models: allModels });
26
+ const isBrowserCompatible = (m) => m.startsWith('gpt-') || m.startsWith('gemini');
27
+ const hasNonBrowserCompatibleTarget = (browserRequested || browserConfigured) && allModels.some((m) => !isBrowserCompatible(m));
28
+ if (hasNonBrowserCompatibleTarget) {
29
+ throw new PromptValidationError('Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.', { engine: 'browser', models: allModels });
27
30
  }
28
- const engineCoercedToApi = engineWasBrowser && (isGemini || isCodex || isClaude || isGrok);
29
- // When Gemini, Claude, or Grok is selected, force API engine for auto-browser detection; codex also forces API.
30
- const fixedEngine = isGemini || isCodex || isClaude || isGrok || normalizedRequestedModels.length > 0 ? 'api' : resolvedEngine;
31
+ const engineCoercedToApi = engineWasBrowser && (isCodex || isClaude || isGrok);
32
+ const fixedEngine = isCodex || isClaude || isGrok || normalizedRequestedModels.length > 0 ? 'api' : resolvedEngine;
31
33
  const promptWithSuffix = userConfig?.promptSuffix && userConfig.promptSuffix.trim().length > 0
32
34
  ? `${prompt.trim()}\n${userConfig.promptSuffix}`
33
35
  : prompt;
@@ -38,9 +38,6 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
38
38
  const modelForStatus = runOptions.model ?? sessionMeta.model;
39
39
  try {
40
40
  if (mode === 'browser') {
41
- if (runOptions.model.startsWith('gemini')) {
42
- throw new Error('Gemini models are not available in browser mode. Re-run with --engine api.');
43
- }
44
41
  if (!browserConfig) {
45
42
  throw new Error('Missing browser configuration for session.');
46
43
  }