@steipete/oracle 0.14.0 → 0.15.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 (31) hide show
  1. package/README.md +2 -2
  2. package/dist/bin/oracle-cli.js +10 -8
  3. package/dist/docs-site/browser-mode.html +3 -2
  4. package/dist/docs-site/cli-reference.html +1 -1
  5. package/dist/docs-site/mcp.html +1 -1
  6. package/dist/src/browser/actions/assistantResponse.js +2 -1
  7. package/dist/src/browser/actions/deepResearch.js +132 -61
  8. package/dist/src/browser/actions/modelSelection.js +388 -30
  9. package/dist/src/browser/actions/thinkingTime.js +303 -65
  10. package/dist/src/browser/artifacts.js +2 -8
  11. package/dist/src/browser/chatgptFiles.js +198 -49
  12. package/dist/src/browser/chatgptImages.js +126 -24
  13. package/dist/src/browser/chromeLifecycle.js +35 -4
  14. package/dist/src/browser/deepResearchResult.js +23 -0
  15. package/dist/src/browser/index.js +145 -19
  16. package/dist/src/browser/profileCopy.js +93 -0
  17. package/dist/src/browser/projectSourcesRunner.js +2 -1
  18. package/dist/src/browser/prompt.js +151 -22
  19. package/dist/src/cli/browserConfig.js +19 -2
  20. package/dist/src/cli/browserDefaults.js +2 -1
  21. package/dist/src/cli/options.js +8 -0
  22. package/dist/src/cli/sessionRunner.js +13 -7
  23. package/dist/src/mcp/tools/chatgptImage.js +8 -3
  24. package/dist/src/mcp/tools/consult.js +9 -8
  25. package/dist/src/mcp/types.js +11 -2
  26. package/dist/src/oracle/thinkingTime.js +40 -0
  27. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  28. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  29. package/package.json +6 -6
  30. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  31. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
package/README.md CHANGED
@@ -302,8 +302,8 @@ Browser automation can open or control Chrome, so dry-runs and live runs print a
302
302
  | `--browser-research deep` | Activate ChatGPT Deep Research for broad web research and cited reports (browser only). |
303
303
  | `--browser-follow-up <prompt>` | Browser-only multi-turn consult: submit an additional prompt in the same ChatGPT conversation after the initial answer. Repeat for challenge/revision/final-decision passes. Not supported with Deep Research mode. |
304
304
  | `--browser-archive <auto\|always\|never>` | Archive completed ChatGPT browser conversations after local artifacts are saved. `auto` archives successful one-shot chats only, and skips project, Deep Research, multi-turn, failed, and incomplete sessions. |
305
- | `--browser-attachments <auto\|never\|always>` | Control browser file delivery: `auto` pastes small text files inline and uploads larger bundles, `never` always pastes inline, and `always` uploads files as ChatGPT attachments. |
306
- | `--browser-bundle-files`, `--browser-bundle-format <text\|zip>` | Bundle browser uploads into one attachment. `text` keeps the existing single Markdown-style text bundle; `zip` preserves individual file names inside one ZIP upload. |
305
+ | `--browser-attachments <auto\|never\|always>` | Control browser file delivery: `auto` pastes small text files inline and uploads larger or raw files, `never` requires inline-compatible text files, and `always` uploads files as ChatGPT attachments. |
306
+ | `--browser-bundle-files`, `--browser-bundle-format <auto\|text\|zip>` | Bundle browser uploads into one attachment. `auto` uses a text bundle for text-only inputs and a byte-preserving ZIP when bundled inputs include raw files; `text` writes a Markdown-style text bundle; `zip` archives the original file bytes. |
307
307
  | `--browser-port <port>` | Pin the Chrome DevTools port (WSL/Windows firewall helper). |
308
308
  | `--browser-inline-cookies[(-file)] <payload \| path>` | Supply cookies without Chrome/Keychain (browser). |
309
309
  | `--browser-timeout`, `--browser-input-timeout`, `--browser-attachment-timeout` | Control overall/browser input/attachment readiness timeouts (supports h/m/s/ms). |
@@ -18,7 +18,7 @@ import { DEFAULT_MODEL, MODEL_CONFIGS } from "../src/oracle/config.js";
18
18
  import { isKnownModel } from "../src/oracle/modelResolver.js";
19
19
  import { CHATGPT_URL } from "../src/browser/constants.js";
20
20
  import { applyHelpStyling } from "../src/cli/help.js";
21
- import { collectPaths, collectModelList, collectTextValues, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, parseDurationOption, mergePathLikeOptions, dedupePathInputs, } from "../src/cli/options.js";
21
+ import { collectPaths, collectModelList, collectTextValues, parseFloatOption, parseIntOption, parseSearchOption, parseThinkingTimeOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, parseDurationOption, mergePathLikeOptions, dedupePathInputs, } from "../src/cli/options.js";
22
22
  import { copyToClipboard } from "../src/cli/clipboard.js";
23
23
  import { buildMarkdownBundle } from "../src/cli/markdownBundle.js";
24
24
  import { shouldDetachSession } from "../src/cli/detach.js";
@@ -325,12 +325,13 @@ program
325
325
  .addOption(new Option("--browser-no-cookie-sync", "Skip copying cookies from Chrome.").hideHelp())
326
326
  .addOption(new Option("--browser-manual-login", "Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login.").hideHelp())
327
327
  .addOption(new Option("--browser-manual-login-profile-dir <path>", "Persistent Chrome profile directory for manual-login browser runs.").hideHelp())
328
+ .addOption(new Option("--copy-profile <dir>", 'Copy a signed-in Chrome user-data dir to a throwaway profile and run browser mode against it (login-free; auto-cleanup). e.g. "$HOME/Library/Application Support/Google/Chrome".'))
328
329
  .addOption(new Option("--browser-headless", "Launch Chrome in headless mode.").hideHelp())
329
330
  .addOption(new Option("--browser-hide-window", "Hide the Chrome window after launch (macOS headful only).").hideHelp())
330
331
  .addOption(new Option("--browser-keep-browser", "Keep Chrome running after completion.").hideHelp())
331
332
  .addOption(new Option("--browser-model-strategy <mode>", "ChatGPT model picker strategy: select (default) switches to the requested model, current keeps the active model, ignore skips the picker entirely.").choices(["select", "current", "ignore"]))
332
- .addOption(new Option("--browser-thinking-time <level>", "Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.")
333
- .choices(["light", "standard", "extended", "heavy"])
333
+ .addOption(new Option("--browser-thinking-time <level>", "Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy, or ChatGPT UI aliases.")
334
+ .argParser(parseThinkingTimeOption)
334
335
  .hideHelp())
335
336
  .addOption(new Option("--browser-research <mode>", "Browser research mode: deep activates ChatGPT Deep Research.").choices(["off", "deep"]))
336
337
  .addOption(new Option("--browser-archive <mode>", "Archive completed ChatGPT browser conversations after local artifacts are saved (auto archives successful non-project one-shots only).").choices(["auto", "always", "never"]))
@@ -338,7 +339,7 @@ program
338
339
  .argParser(collectTextValues)
339
340
  .default([]))
340
341
  .addOption(new Option("--browser-allow-cookie-errors", "Continue even if Chrome cookies cannot be copied.").hideHelp())
341
- .addOption(new Option("--browser-attachments <mode>", "How to deliver --file inputs in browser mode: auto (default) pastes inline up to ~60k chars then uploads; never always paste inline; always always upload.")
342
+ .addOption(new Option("--browser-attachments <mode>", "How to deliver --file inputs in browser mode: auto (default) pastes text inline up to ~60k chars then uploads; never requires inline-compatible text files; always uploads.")
342
343
  .choices(["auto", "never", "always"])
343
344
  .default("auto"))
344
345
  .addOption(new Option("--remote-chrome <host:port>", "Connect to remote Chrome DevTools Protocol, or when combined with --browser-attach-running use this host:port as the local attach hint."))
@@ -347,9 +348,9 @@ program
347
348
  .addOption(new Option("--remote-token <token>", "Access token for the remote `oracle serve` instance."))
348
349
  .addOption(new Option("--browser-inline-files", "Alias for --browser-attachments never (force pasting file contents inline).").default(false))
349
350
  .addOption(new Option("--browser-bundle-files", "Bundle all attachments into a single archive before uploading.").default(false))
350
- .addOption(new Option("--browser-bundle-format <format>", "Bundle format for browser uploads when files are bundled: text (default) or zip.")
351
- .choices(["text", "zip"])
352
- .default("text"))
351
+ .addOption(new Option("--browser-bundle-format <format>", "Bundle format for browser uploads when files are bundled: auto (default), text, or zip.")
352
+ .choices(["auto", "text", "zip"])
353
+ .default("auto"))
353
354
  .addOption(new Option("--youtube <url>", "YouTube video URL to analyze (Gemini web/cookie mode only; uses your signed-in Chrome cookies for gemini.google.com)."))
354
355
  .addOption(new Option("--generate-image <file>", "Generate image and save to file (Gemini browser mode; ChatGPT browser mode saves downloadable image artifacts when present)."))
355
356
  .addOption(new Option("--edit-image <file>", "Edit existing image (Gemini browser mode; for ChatGPT attach source images with --file and use --generate-image for output)."))
@@ -689,7 +690,7 @@ function buildRunOptions(options, overrides = {}) {
689
690
  "auto",
690
691
  browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
691
692
  browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
692
- browserBundleFormat: overrides.browserBundleFormat ?? options.browserBundleFormat ?? "text",
693
+ browserBundleFormat: overrides.browserBundleFormat ?? options.browserBundleFormat ?? "auto",
693
694
  generateImage: overrides.generateImage ?? options.generateImage,
694
695
  outputPath: overrides.outputPath ?? options.output,
695
696
  browserFollowUps: overrides.browserFollowUps ?? options.browserFollowUp ?? [],
@@ -1301,6 +1302,7 @@ async function runRootCommand(options) {
1301
1302
  const { buildBrowserConfig, resolveBrowserModelLabel } = await import("../src/cli/browserConfig.js");
1302
1303
  const config = await buildBrowserConfig({
1303
1304
  ...options,
1305
+ remoteHost: remoteHost ?? undefined,
1304
1306
  model: activeModel,
1305
1307
  browserModelLabel: resolveBrowserModelLabel(cliModelArg, activeModel),
1306
1308
  });
@@ -336,11 +336,12 @@ body:not(.home) .doc>h1:first-child{display:none}
336
336
  <li><code>--browser-port &lt;port&gt;</code> (alias: <code>--browser-debug-port</code>; env: <code>ORACLE_BROWSER_PORT</code>/<code>ORACLE_BROWSER_DEBUG_PORT</code>): pin the DevTools port (handy on WSL/Windows firewalls). When omitted, a random open port is chosen.</li>
337
337
  <li><code>ORACLE_CHATGPT_ACCOUNT_EMAIL</code>: exact saved-account email to select if ChatGPT shows its “Welcome back” account picker. Set it on the machine running browser automation. Oracle never logs the address; without it, Oracle selects only a single unambiguous saved account and fails closed when several are present.</li>
338
338
  <li><code>--browser-no-cookie-sync</code>, <code>--browser-manual-login</code> (persistent automation profile + user-driven login), <code>--browser-headless</code>, <code>--browser-hide-window</code>, <code>--browser-keep-browser</code>, and the global <code>-v/--verbose</code> flag for detailed automation logs.</li>
339
+ <li><code>--copy-profile &lt;dir&gt;</code>: copy a signed-in Chrome user-data directory (e.g. <code>&quot;$HOME/Library/Application Support/Google/Chrome&quot;</code>) to a throwaway profile and run against it, reusing your live ChatGPT session with no manual sign-in. Oracle copies the profile recorded as active in <code>Local State</code>; pass <code>--browser-chrome-profile &lt;name&gt;</code> to select another direct child profile. The copy is launched with the real Keychain (not mocked) so its encrypted cookies decrypt, and is always deleted afterward—including setup/launch failures, incomplete captures, Cloudflare challenges, and interrupts. Copied-profile runs cannot be kept or reattached. Not compatible with <code>--browser-keep-browser</code>, <code>--browser-manual-login</code>, <code>--browser-attach-running</code>, <code>--remote-chrome</code>, or <code>--remote-host</code>, and fails fast if the required <code>Local State</code> cannot be copied. macOS/Linux; requires <code>rsync</code>.</li>
339
340
  <li><code>--browser-url</code>: override ChatGPT base URL if needed.</li>
340
- <li><code>--browser-attachments &lt;auto|never|always&gt;</code>: control how <code>--file</code> inputs are delivered in browser mode. Default <code>auto</code> pastes file contents inline up to ~60k characters and switches to uploads above that.</li>
341
+ <li><code>--browser-attachments &lt;auto|never|always&gt;</code>: control how <code>--file</code> inputs are delivered in browser mode. Default <code>auto</code> pastes text contents inline up to ~60k characters and uploads larger or raw files. <code>never</code> requires inline-compatible text inputs and rejects raw/binary files.</li>
341
342
  <li><code>--browser-inline-files</code>: alias for <code>--browser-attachments never</code> (forces inline paste; never uploads attachments).</li>
342
343
  <li><code>--browser-bundle-files</code>: bundle all resolved attachments into a single temp file before uploading (only used when uploads are enabled/selected).</li>
343
- <li><code>--browser-bundle-format &lt;text|zip&gt;</code>: choose the bundle format. <code>text</code> keeps the existing single Markdown-style text bundle; <code>zip</code> preserves individual file names in one uploaded archive.</li>
344
+ <li><code>--browser-bundle-format &lt;auto|text|zip&gt;</code>: choose the bundle format. <code>auto</code> uses a text bundle for text-only inputs and a byte-preserving ZIP when bundled inputs include raw files; <code>text</code> keeps the single Markdown-style text bundle; <code>zip</code> archives the original file bytes. ZIP bundle inputs are capped at 128 MiB because bundle creation is in-memory.</li>
344
345
  <li>sqlite bindings: automatic rebuilds now require <code>ORACLE_ALLOW_SQLITE_REBUILD=1</code>. Without it, the CLI logs instructions instead of running <code>pnpm rebuild</code> on your behalf.</li>
345
346
  <li><code>--model</code>: the same flag used for API runs is accepted, but the ChatGPT automation path supports GPT-5.5, GPT-5.4, and GPT-5.2 variants. Use <code>gpt-5.5-pro</code>, <code>gpt-5.5</code>, <code>gpt-5.5-instant</code>, <code>gpt-5.4-pro</code>, <code>gpt-5.4</code>, <code>gpt-5.2</code>, <code>gpt-5.2-thinking</code>, <code>gpt-5.2-instant</code>, or <code>gpt-5.2-pro</code>. Legacy Pro aliases still resolve to the latest Pro picker target.</li>
346
347
  <li>Cookie sync is mandatory—if we can’t copy cookies from Chrome, the run exits early. By default Oracle copies a small ChatGPT auth/Cloudflare allowlist to avoid oversized request headers; use <code>--browser-cookie-names</code> only when you need to override that set. Use the hidden <code>--browser-allow-cookie-errors</code> flag only when you’re intentionally running logged out (it skips the early exit but still warns).</li>
@@ -264,7 +264,7 @@ body:not(.home) .doc>h1:first-child{display:none}
264
264
  <table><thead><tr><th>Flag</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--base-url &lt;url&gt;</code></td><td>LiteLLM / Azure / OpenRouter / proxy.</td></tr><tr><td><code>--provider &lt;mode&gt;</code></td><td>API route: <code>auto</code>, <code>openai</code>, or <code>azure</code>.</td></tr><tr><td><code>--no-azure</code></td><td>Ignore Azure env/config for this run.</td></tr><tr><td><code>--route</code></td><td>Print redacted API route plan, then exit.</td></tr><tr><td><code>--azure-endpoint</code></td><td>Azure OpenAI endpoint.</td></tr><tr><td><code>--azure-deployment</code></td><td>Azure deployment name.</td></tr><tr><td><code>--azure-api-version</code></td><td>Azure API version.</td></tr></tbody></table>
265
265
  <p>See <a href="openai-endpoints.html">OpenAI / Azure / OpenRouter</a> and <a href="openrouter.html">OpenRouter</a>.</p>
266
266
  <h2 id="browser-mode"><a class="anchor" href="#browser-mode" aria-label="Anchor link">#</a>Browser mode</h2>
267
- <table><thead><tr><th>Flag</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--chatgpt-url &lt;url&gt;</code></td><td>Target a ChatGPT workspace / project folder.</td></tr><tr><td><code>--browser-model-strategy &lt;select|current|ignore&gt;</code></td><td>Control ChatGPT model picker.</td></tr><tr><td><code>--browser-manual-login</code></td><td>Use persistent profile + manual login (no Keychain).</td></tr><tr><td><code>--browser-attach-running</code></td><td>Attach to your already-running Chrome via DevTools.</td></tr><tr><td><code>--browser-tab &lt;ref&gt;</code></td><td>Reuse an existing tab (<code>current</code>, id, URL, title substring).</td></tr><tr><td><code>--browser-thinking-time &lt;light|standard|extended|heavy&gt;</code></td><td>Pro / Thinking model intensity.</td></tr><tr><td><code>--browser-research deep</code></td><td>Activate Deep Research mode.</td></tr><tr><td><code>--browser-follow-up &lt;prompt&gt;</code></td><td>Multi-turn in the same ChatGPT conversation.</td></tr><tr><td><code>--browser-port &lt;port&gt;</code></td><td>Pin Chrome DevTools port.</td></tr><tr><td><code>--browser-inline-cookies[(-file)] &lt;…&gt;</code></td><td>Supply cookies inline (no Keychain / Chrome).</td></tr><tr><td><code>--browser-timeout</code>, <code>--browser-input-timeout</code>, <code>--browser-attachment-timeout</code></td><td>Overall / input / attachment readiness timeouts (h/m/s/ms).</td></tr><tr><td><code>--browser-recheck-delay</code>, <code>--browser-recheck-timeout</code></td><td>Delayed retry after a timeout.</td></tr><tr><td><code>--browser-auto-reattach-delay/-interval/-timeout</code></td><td>Poll the existing tab when ChatGPT redirects mid-load.</td></tr><tr><td><code>--browser-reuse-wait</code></td><td>Wait for shared Chrome profile before launching.</td></tr><tr><td><code>--browser-profile-lock-timeout</code></td><td>Wait for the manual-login profile lock.</td></tr><tr><td><code>--browser-max-concurrent-tabs</code></td><td>Soft limit for shared-profile parallel runs (default 3).</td></tr><tr><td><code>--browser-keep-browser</code></td><td>Keep the browser open after the run.</td></tr><tr><td><code>--browser-headless</code>, <code>--browser-hide-window</code></td><td>Visibility controls.</td></tr><tr><td><code>--browser-attachments &lt;auto|never|always&gt;</code></td><td>Attach files inline vs upload.</td></tr><tr><td><code>--browser-bundle-files</code>, <code>--browser-bundle-format &lt;text|zip&gt;</code></td><td>Bundle browser uploads as text or ZIP.</td></tr><tr><td><code>--browser-chrome-path</code>, <code>--browser-cookie-path</code></td><td>Override Chrome / cookie store discovery (Linux / Windows).</td></tr></tbody></table>
267
+ <table><thead><tr><th>Flag</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--chatgpt-url &lt;url&gt;</code></td><td>Target a ChatGPT workspace / project folder.</td></tr><tr><td><code>--browser-model-strategy &lt;select|current|ignore&gt;</code></td><td>Control ChatGPT model picker.</td></tr><tr><td><code>--browser-manual-login</code></td><td>Use persistent profile + manual login (no Keychain).</td></tr><tr><td><code>--browser-attach-running</code></td><td>Attach to your already-running Chrome via DevTools.</td></tr><tr><td><code>--browser-tab &lt;ref&gt;</code></td><td>Reuse an existing tab (<code>current</code>, id, URL, title substring).</td></tr><tr><td><code>--browser-thinking-time &lt;light|standard|extended|heavy&gt;</code></td><td>Pro / Thinking model intensity.</td></tr><tr><td><code>--browser-research deep</code></td><td>Activate Deep Research mode.</td></tr><tr><td><code>--browser-follow-up &lt;prompt&gt;</code></td><td>Multi-turn in the same ChatGPT conversation.</td></tr><tr><td><code>--browser-port &lt;port&gt;</code></td><td>Pin Chrome DevTools port.</td></tr><tr><td><code>--browser-inline-cookies[(-file)] &lt;…&gt;</code></td><td>Supply cookies inline (no Keychain / Chrome).</td></tr><tr><td><code>--browser-timeout</code>, <code>--browser-input-timeout</code>, <code>--browser-attachment-timeout</code></td><td>Overall / input / attachment readiness timeouts (h/m/s/ms).</td></tr><tr><td><code>--browser-recheck-delay</code>, <code>--browser-recheck-timeout</code></td><td>Delayed retry after a timeout.</td></tr><tr><td><code>--browser-auto-reattach-delay/-interval/-timeout</code></td><td>Poll the existing tab when ChatGPT redirects mid-load.</td></tr><tr><td><code>--browser-reuse-wait</code></td><td>Wait for shared Chrome profile before launching.</td></tr><tr><td><code>--browser-profile-lock-timeout</code></td><td>Wait for the manual-login profile lock.</td></tr><tr><td><code>--browser-max-concurrent-tabs</code></td><td>Soft limit for shared-profile parallel runs (default 3).</td></tr><tr><td><code>--browser-keep-browser</code></td><td>Keep the browser open after the run.</td></tr><tr><td><code>--browser-headless</code>, <code>--browser-hide-window</code></td><td>Visibility controls.</td></tr><tr><td><code>--browser-attachments &lt;auto|never|always&gt;</code></td><td>Attach files inline vs upload.</td></tr><tr><td><code>--browser-bundle-files</code>, <code>--browser-bundle-format &lt;auto|text|zip&gt;</code></td><td>Bundle browser uploads as text or byte-preserving ZIP.</td></tr><tr><td><code>--browser-chrome-path</code>, <code>--browser-cookie-path</code></td><td>Override Chrome / cookie store discovery (Linux / Windows).</td></tr></tbody></table>
268
268
  <p>See <a href="browser-mode.html">Browser Mode</a> for usage.</p>
269
269
  <h2 id="remote-browser"><a class="anchor" href="#remote-browser" aria-label="Anchor link">#</a>Remote browser</h2>
270
270
  <table><thead><tr><th>Flag</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--remote-host &lt;host:port&gt;</code></td><td>Use a remote <code>oracle serve</code> host.</td></tr><tr><td><code>--remote-token &lt;secret&gt;</code></td><td>Auth for the remote host.</td></tr><tr><td><code>--remote-chrome &lt;host:port&gt;</code></td><td>Attach to an existing remote Chrome session.</td></tr></tbody></table>
@@ -266,7 +266,7 @@ body:not(.home) .doc>h1:first-child{display:none}
266
266
  <ul>
267
267
  <li>Inputs: <code>prompt</code> (required), <code>files?: string[]</code> (globs), <code>model?: string</code> (defaults to CLI), <code>engine?: &quot;api&quot; | &quot;browser&quot;</code> (optional; Oracle follows CLI defaults: <code>ORACLE_ENGINE</code> and the effective config first, then API when <code>OPENAI_API_KEY</code> is set, otherwise browser), <code>slug?: string</code>.</li>
268
268
  <li>Presets: <code>preset?: &quot;chatgpt-pro-heavy&quot;</code> applies browser mode + current Pro model alias + extended thinking, unless the request overrides those fields.</li>
269
- <li>Browser-only extras: <code>browserAttachments?: &quot;auto&quot;|&quot;never&quot;|&quot;always&quot;</code>, <code>browserBundleFiles?: boolean</code>, <code>browserBundleFormat?: &quot;text&quot;|&quot;zip&quot;</code>, <code>browserThinkingTime?: &quot;light&quot;|&quot;standard&quot;|&quot;extended&quot;|&quot;heavy&quot;</code>, <code>browserResearchMode?: &quot;deep&quot;</code>, <code>browserFollowUps?: string[]</code>, <code>browserArchive?: &quot;auto&quot;|&quot;always&quot;|&quot;never&quot;</code>, <code>browserKeepBrowser?: boolean</code>, <code>browserModelLabel?: string</code>, <code>browserModelStrategy?: &quot;select&quot;|&quot;current&quot;|&quot;ignore&quot;</code>, <code>generateImage?: string</code>, <code>outputPath?: string</code>.</li>
269
+ <li>Browser-only extras: <code>browserAttachments?: &quot;auto&quot;|&quot;never&quot;|&quot;always&quot;</code>, <code>browserBundleFiles?: boolean</code>, <code>browserBundleFormat?: &quot;auto&quot;|&quot;text&quot;|&quot;zip&quot;</code>, <code>browserThinkingTime?: &quot;light&quot;|&quot;standard&quot;|&quot;extended&quot;|&quot;heavy&quot;</code>, <code>browserResearchMode?: &quot;deep&quot;</code>, <code>browserFollowUps?: string[]</code>, <code>browserArchive?: &quot;auto&quot;|&quot;always&quot;|&quot;never&quot;</code>, <code>browserKeepBrowser?: boolean</code>, <code>browserModelLabel?: string</code>, <code>browserModelStrategy?: &quot;select&quot;|&quot;current&quot;|&quot;ignore&quot;</code>, <code>generateImage?: string</code>, <code>outputPath?: string</code>.</li>
270
270
  <li>Dry runs: set <code>dryRun: true</code> to preview the resolved request without creating a session or touching the browser.</li>
271
271
  <li>Behavior: starts a session, runs it with the chosen engine, returns final output + metadata. Background/foreground follows the CLI (e.g., GPT‑5 Pro detaches by default). If API mode fails because <code>OPENAI_API_KEY</code> is missing and you have ChatGPT Pro, retry with <code>engine: &quot;browser&quot;</code> or <code>preset: &quot;chatgpt-pro-heavy&quot;</code> to use your signed-in ChatGPT session instead of an API key.</li>
272
272
  <li>Logging: emits MCP logs (<code>info</code> per line, <code>debug</code> for streamed chunks with byte sizes). If browser prerequisites are missing, returns an error payload instead of running.</li>
@@ -857,7 +857,8 @@ function buildAssistantExtractor(functionName) {
857
857
  !normalizedText ||
858
858
  normalizedText === 'edit' ||
859
859
  normalizedText === 'stopped thinking' ||
860
- normalizedText === 'stopped thinking edit';
860
+ normalizedText === 'stopped thinking edit' ||
861
+ /^thought for \\d+(?:\\.\\d+)?\\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)\\s+edit$/.test(normalizedText);
861
862
  if (generatedImages.length > 0 && imageOnlyChrome) {
862
863
  const label = generatedImages.length === 1 ? 'Generated image.' : \`Generated \${generatedImages.length} images.\`;
863
864
  return { text: label, html: messageRoot?.innerHTML ?? html, messageId, turnId, turnIndex: index };
@@ -1,5 +1,6 @@
1
1
  import { DEEP_RESEARCH_PLUS_BUTTON, DEEP_RESEARCH_DROPDOWN_ITEM_TEXT, DEEP_RESEARCH_PILL_LABEL, DEEP_RESEARCH_POLL_INTERVAL_MS, DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS, DEEP_RESEARCH_DEFAULT_TIMEOUT_MS, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR, CONVERSATION_TURN_SELECTOR, } from "../constants.js";
2
2
  import { delay } from "../utils.js";
3
+ import { isDeepResearchIncompleteText } from "../deepResearchResult.js";
3
4
  import { buildClickDispatcher } from "./domEvents.js";
4
5
  import { captureAssistantMarkdown, readAssistantSnapshot } from "./assistantResponse.js";
5
6
  import { BrowserAutomationError } from "../../oracle/errors.js";
@@ -116,7 +117,10 @@ export async function waitForDeepResearchCompletion(Runtime, logger, timeoutMs =
116
117
  : -1;
117
118
  const scopedToNewTurns = minTurnLiteral >= 0;
118
119
  const ignoredTargetKeys = new Set(options?.ignoredTargetKeys ?? []);
119
- const requireScopedTargetOwner = options?.requireScopedTargetOwner === true;
120
+ const requireScopedTargetOwner = options?.requireScopedTargetOwner === true ||
121
+ (scopedToNewTurns && options?.targetBaselineCaptured !== true);
122
+ let observedResearchEvidence = false;
123
+ let loggedIncompleteResult = false;
120
124
  logger(`Monitoring Deep Research (timeout: ${Math.round(timeoutMs / 60_000)}min)...`);
121
125
  while (Date.now() - start < timeoutMs) {
122
126
  const { result } = await Runtime.evaluate({
@@ -127,7 +131,6 @@ export async function waitForDeepResearchCompletion(Runtime, logger, timeoutMs =
127
131
  if (val?.accountBlocked) {
128
132
  throw new BrowserAutomationError("ChatGPT account security block detected during Deep Research. Open chatgpt.com in Chrome, secure the account, then rerun Oracle.", { stage: "chatgpt-account-blocked", code: "chatgpt-account-blocked" });
129
133
  }
130
- const activeScopedResearch = Boolean(val?.hasActiveScopedResearch);
131
134
  // ChatGPT renders the Deep Research report inside an out-of-process,
132
135
  // sandboxed iframe (connector_openai_deep_research.*.oaiusercontent.com),
133
136
  // doubly nested and same-origin. That OOPIF does NOT appear in the main
@@ -136,24 +139,27 @@ export async function waitForDeepResearchCompletion(Runtime, logger, timeoutMs =
136
139
  // (readDeepResearchTargetResult) attaches to the iframe's own CDP target and
137
140
  // walks its nested frames, so it CAN read the report. Prefer the target path
138
141
  // and fall back to the in-page frame path for legacy/inline rendering.
139
- const targetResult = client
142
+ const rawTargetResult = client
140
143
  ? ((await readDeepResearchTargetResult(client, ignoredTargetKeys, requireScopedTargetOwner ? minTurnLiteral : -1).catch(() => null))?.read ?? null)
141
144
  : null;
145
+ const targetResult = filterIncompleteDeepResearchRead(rawTargetResult);
142
146
  // A completed target read is authoritative. If the target read is missing or
143
147
  // only in-progress, still try the in-page frame path so an incomplete target
144
148
  // read does not suppress a completed report there (legacy/inline rendering).
145
- const inPageResult = !targetResult?.completed && Page
146
- ? await readDeepResearchFrameResult(Runtime, Page).catch(() => null)
149
+ const inPageScan = !targetResult?.completed && Page
150
+ ? await readDeepResearchFrameResult(Runtime, Page, client, scopedToNewTurns ? minTurnLiteral : -1).catch(() => null)
147
151
  : null;
152
+ const rawInPageResult = inPageScan?.read ?? null;
153
+ const inPageResult = filterIncompleteDeepResearchRead(rawInPageResult);
148
154
  const read = pickPreferredDeepResearchRead(targetResult, inPageResult);
149
- // A target-confirmed completion read the live connector iframe directly, so
150
- // it is authoritative even when the main DOM exposes no assistant turn (the
151
- // report lives entirely in the OOPIF). The main-DOM hasActiveScopedResearch
152
- // heuristic no longer holds in that case, so don't gate on it.
153
- const completedFromTarget = Boolean(targetResult?.completed);
154
- if (read?.completed &&
155
- read.text &&
156
- (completedFromTarget || !scopedToNewTurns || activeScopedResearch)) {
155
+ // Target keys captured before submission are ignored, so a target result is
156
+ // tied to this run. Main-page iframes are not: old reports can remain in the
157
+ // conversation and must never authorize a new normal-response fallback.
158
+ observedResearchEvidence ||= Boolean(rawTargetResult ||
159
+ (scopedToNewTurns && rawInPageResult) ||
160
+ val?.researchActivity ||
161
+ val?.hasActiveScopedResearch);
162
+ if (read?.completed && read.text) {
157
163
  logger(`Deep Research completed (${Math.round((Date.now() - start) / 1000)}s elapsed)`);
158
164
  return {
159
165
  text: read.text,
@@ -163,9 +169,18 @@ export async function waitForDeepResearchCompletion(Runtime, logger, timeoutMs =
163
169
  }
164
170
  // Completion detected
165
171
  if (val?.finished) {
172
+ if (!observedResearchEvidence) {
173
+ throw new BrowserAutomationError("ChatGPT returned a completed response without starting Deep Research. The Deep Research selection may have silently fallen back to a normal response.", { stage: "deep-research-not-started", code: "deep-research-not-started" });
174
+ }
166
175
  logger(`Deep Research completed (${Math.round((Date.now() - start) / 1000)}s elapsed)`);
167
176
  return await extractDeepResearchResult(Runtime, logger, minTurnIndex ?? undefined);
168
177
  }
178
+ const incompleteFrameResult = Boolean((rawTargetResult?.completed && !targetResult?.completed) ||
179
+ (rawInPageResult?.completed && !inPageResult?.completed));
180
+ if ((val?.incompleteResult || incompleteFrameResult) && !loggedIncompleteResult) {
181
+ logger("Deep Research interim status detected; waiting for the final report");
182
+ loggedIncompleteResult = true;
183
+ }
169
184
  // Progress logging every 60 seconds
170
185
  const now = Date.now();
171
186
  if (now - lastLogTime >= 60_000) {
@@ -204,24 +219,26 @@ export async function extractDeepResearchResult(Runtime, logger, minTurnIndex) {
204
219
  };
205
220
  // Try the copy-button approach first for clean markdown
206
221
  const markdown = await captureAssistantMarkdown(Runtime, meta, logger);
207
- if (markdown && !isDeepResearchPlaceholderText(markdown)) {
222
+ if (markdown && !isDeepResearchIncompleteText(markdown)) {
208
223
  return { text: markdown, html: snapshot?.html ?? undefined, meta };
209
224
  }
210
225
  // Fall back to snapshot text
211
- if (snapshot?.text && !isDeepResearchPlaceholderText(snapshot.text)) {
226
+ if (snapshot?.text && !isDeepResearchIncompleteText(snapshot.text)) {
212
227
  return { text: snapshot.text, html: snapshot.html ?? undefined, meta };
213
228
  }
214
229
  throw new BrowserAutomationError("Deep Research completed but failed to extract the response text.", { stage: "deep-research-extract", code: "extraction-failed" });
215
230
  }
216
- function isDeepResearchPlaceholderText(text) {
217
- const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
218
- return (normalized === "called tool" ||
219
- normalized === "used tool" ||
220
- normalized === "użyto narzędzia" ||
221
- normalized === "narzędzie wywołane");
222
- }
223
231
  export function isDeepResearchPlaceholderTextForTest(text) {
224
- return isDeepResearchPlaceholderText(text);
232
+ return isDeepResearchIncompleteText(text);
233
+ }
234
+ function filterIncompleteDeepResearchRead(result) {
235
+ if (!result?.completed || !result.text || !isDeepResearchIncompleteText(result.text)) {
236
+ return result;
237
+ }
238
+ return { ...result, completed: false, inProgress: true };
239
+ }
240
+ export function filterIncompleteDeepResearchReadForTest(result) {
241
+ return filterIncompleteDeepResearchRead(result);
225
242
  }
226
243
  /**
227
244
  * Choose the authoritative Deep Research read between the target-attach result
@@ -243,37 +260,64 @@ function pickPreferredDeepResearchRead(targetResult, inPageResult) {
243
260
  export function pickPreferredDeepResearchReadForTest(targetResult, inPageResult) {
244
261
  return pickPreferredDeepResearchRead(targetResult, inPageResult);
245
262
  }
246
- async function readDeepResearchFrameResult(Runtime, Page) {
263
+ async function readDeepResearchFrameResult(Runtime, Page, client, minTurnIndex = -1) {
247
264
  const pageWithFrames = Page;
248
265
  if (typeof pageWithFrames.getFrameTree !== "function" ||
249
266
  typeof pageWithFrames.createIsolatedWorld !== "function") {
250
267
  return null;
251
268
  }
252
269
  const frameTree = (await pageWithFrames.getFrameTree())?.frameTree;
253
- const frameId = findDeepResearchFrameId(frameTree);
254
- if (!frameId) {
270
+ const frameIds = collectPageDeepResearchFrameIds(frameTree);
271
+ if (frameIds.length === 0) {
255
272
  return null;
256
273
  }
257
- const world = await pageWithFrames.createIsolatedWorld({
258
- frameId,
259
- worldName: "oracle-deep-research",
260
- grantUniveralAccess: true,
261
- });
262
- if (typeof world.executionContextId !== "number") {
263
- return null;
274
+ const rawClient = client;
275
+ if (minTurnIndex >= 0) {
276
+ if (typeof rawClient?.send !== "function") {
277
+ return null;
278
+ }
264
279
  }
265
- const { result } = await Runtime.evaluate({
266
- expression: buildDeepResearchFrameStatusExpression(),
267
- contextId: world.executionContextId,
268
- returnByValue: true,
269
- });
270
- return result?.value ?? null;
280
+ let best = null;
281
+ for (const frameId of frameIds) {
282
+ let ownerTurnIndex = null;
283
+ if (minTurnIndex >= 0 && rawClient?.send) {
284
+ ownerTurnIndex = await readDeepResearchTargetOwnerTurnIndex(rawClient, frameId, rawClient.oraclePageSessionId);
285
+ if (ownerTurnIndex === null || ownerTurnIndex < minTurnIndex) {
286
+ continue;
287
+ }
288
+ }
289
+ const world = await pageWithFrames.createIsolatedWorld({
290
+ frameId,
291
+ worldName: "oracle-deep-research",
292
+ grantUniveralAccess: true,
293
+ });
294
+ if (typeof world.executionContextId !== "number") {
295
+ continue;
296
+ }
297
+ const { result } = await Runtime.evaluate({
298
+ expression: buildDeepResearchFrameStatusExpression(),
299
+ contextId: world.executionContextId,
300
+ returnByValue: true,
301
+ });
302
+ const read = result?.value ?? null;
303
+ if (!read) {
304
+ continue;
305
+ }
306
+ best = { read, ownerTurnIndex };
307
+ if (read.completed) {
308
+ return best;
309
+ }
310
+ }
311
+ return best;
271
312
  }
272
313
  async function readDeepResearchTargetResult(client, ignoredTargetKeys = new Set(), minTurnIndex = -1) {
273
314
  const rawClient = client;
274
315
  if (typeof rawClient.send !== "function") {
275
316
  return null;
276
317
  }
318
+ if (typeof client.on !== "function") {
319
+ return null;
320
+ }
277
321
  // On the browser-WSEndpoint path, `client` is a session-bound wrapper whose
278
322
  // domain methods target the page session but whose raw `send` is the
279
323
  // browser-level send. We must therefore pass the page session id explicitly so
@@ -299,7 +343,7 @@ async function readDeepResearchTargetResult(client, ignoredTargetKeys = new Set(
299
343
  ownedSessionIds.add(eventSessionId);
300
344
  }
301
345
  };
302
- client.on?.("Target.attachedToTarget", onAttached);
346
+ client.on("Target.attachedToTarget", onAttached);
303
347
  try {
304
348
  // Scope discovery to the current Oracle-controlled page. `client` is
305
349
  // connected to the conversation page target, so enabling auto-attach on this
@@ -311,13 +355,16 @@ async function readDeepResearchTargetResult(client, ignoredTargetKeys = new Set(
311
355
  // would surface another tab's completed Deep Research report and let it be
312
356
  // saved into the current session (cross-tab leak). Only auto-attached,
313
357
  // page-scoped sessions are treated as belonging to this run.
314
- await rawClient
358
+ const autoAttachEnabled = await rawClient
315
359
  .send("Target.setAutoAttach", {
316
360
  autoAttach: true,
317
361
  waitForDebuggerOnStart: false,
318
362
  flatten: true,
319
363
  }, pageSessionId)
320
- .catch(() => undefined);
364
+ .then(() => true, () => false);
365
+ if (!autoAttachEnabled) {
366
+ return null;
367
+ }
321
368
  await delay(100);
322
369
  if (minTurnIndex >= 0) {
323
370
  await rawClient.send("DOM.enable", {}, pageSessionId).catch(() => undefined);
@@ -374,7 +421,11 @@ async function readDeepResearchTargetResult(client, ignoredTargetKeys = new Set(
374
421
  }
375
422
  }
376
423
  export async function captureDeepResearchTargetKeys(client) {
377
- return (await readDeepResearchTargetResult(client))?.targetKeys ?? [];
424
+ const scan = await readDeepResearchTargetResult(client);
425
+ if (!scan) {
426
+ throw new Error("Deep Research target baseline capture unavailable");
427
+ }
428
+ return scan.targetKeys;
378
429
  }
379
430
  async function readDeepResearchTargetOwnerTurnIndex(rawClient, frameId, pageSessionId) {
380
431
  const owner = (await rawClient
@@ -419,7 +470,7 @@ async function readDeepResearchTargetSession(rawClient, sessionId, targetUrl) {
419
470
  const frameTree = (await rawClient
420
471
  .send("Page.getFrameTree", {}, sessionId)
421
472
  .catch(() => null));
422
- const frameId = frameTree?.frameTree?.frame?.id;
473
+ const ownerFrameId = frameTree?.frameTree?.frame?.id;
423
474
  if (!isConfirmedDeepResearchTarget(targetUrl, frameTree?.frameTree)) {
424
475
  return { confirmed: false, read: null };
425
476
  }
@@ -438,7 +489,7 @@ async function readDeepResearchTargetSession(rawClient, sessionId, targetUrl) {
438
489
  }
439
490
  const value = await evaluateDeepResearchFrameStatus(rawClient, sessionId, world.executionContextId);
440
491
  if (value?.completed) {
441
- return { confirmed: true, read: value, frameId };
492
+ return { confirmed: true, read: value, frameId: ownerFrameId };
442
493
  }
443
494
  if ((value?.textLength ?? 0) > (best?.textLength ?? 0) || value?.inProgress) {
444
495
  best = value;
@@ -446,12 +497,12 @@ async function readDeepResearchTargetSession(rawClient, sessionId, targetUrl) {
446
497
  }
447
498
  const topFrameValue = await evaluateDeepResearchFrameStatus(rawClient, sessionId);
448
499
  if (topFrameValue?.completed) {
449
- return { confirmed: true, read: topFrameValue, frameId };
500
+ return { confirmed: true, read: topFrameValue, frameId: ownerFrameId };
450
501
  }
451
502
  if ((topFrameValue?.textLength ?? 0) > (best?.textLength ?? 0) || topFrameValue?.inProgress) {
452
503
  best = topFrameValue;
453
504
  }
454
- return { confirmed: true, read: best, frameId };
505
+ return { confirmed: true, read: best, frameId: ownerFrameId };
455
506
  }
456
507
  async function evaluateDeepResearchFrameStatus(rawClient, sessionId, contextId) {
457
508
  const response = (await rawClient
@@ -474,21 +525,20 @@ function isDeepResearchFrameDescriptor(url, name = "") {
474
525
  return (descriptor.includes("connector_openai_deep_research") || descriptor.includes("deep-research"));
475
526
  }
476
527
  function findDeepResearchFrameId(tree) {
528
+ return collectPageDeepResearchFrameIds(tree)[0] ?? null;
529
+ }
530
+ function collectPageDeepResearchFrameIds(tree) {
477
531
  if (!tree?.frame) {
478
- return null;
532
+ return [];
479
533
  }
480
- const url = tree.frame.url ?? "";
481
- const name = tree.frame.name ?? "";
482
- if (isDeepResearchFrameDescriptor(url, name)) {
483
- return tree.frame.id ?? null;
534
+ const ids = [];
535
+ if (tree.frame.id && isDeepResearchFrameDescriptor(tree.frame.url ?? "", tree.frame.name ?? "")) {
536
+ ids.push(tree.frame.id);
484
537
  }
485
538
  for (const child of tree.childFrames ?? []) {
486
- const match = findDeepResearchFrameId(child);
487
- if (match) {
488
- return match;
489
- }
539
+ ids.push(...collectPageDeepResearchFrameIds(child));
490
540
  }
491
- return null;
541
+ return ids;
492
542
  }
493
543
  function collectDeepResearchFrameIds(tree) {
494
544
  if (!tree?.frame) {
@@ -644,20 +694,41 @@ function buildDeepResearchCompletionPollExpression(minTurnIndex) {
644
694
  const text = (lastTurn?.textContent || '').trim();
645
695
  const normalized = text.toLowerCase().replace(/\\s+/g, ' ').trim();
646
696
  const textLength = text.length;
697
+ const lines = text.split(/\\n+/).map(line => line.trim()).filter(Boolean);
698
+ const tailIsPlanningPanel = text.length <= 1500 &&
699
+ lines.length >= 4 &&
700
+ lines.length <= 20 &&
701
+ /^update$/i.test(lines[1] || '') &&
702
+ /^stop research$/i.test(lines[lines.length - 1] || '') &&
703
+ /^determining steps for creating a report(?:\\.\\.\\.)?$/i.test(lines[lines.length - 2] || '');
647
704
  const isToolStub = normalized === 'called tool' ||
648
705
  normalized === 'used tool' ||
649
706
  normalized === 'użyto narzędzia' ||
650
707
  normalized === 'narzędzie wywołane';
708
+ const incompleteResult = isToolStub ||
709
+ normalized === 'planning' ||
710
+ normalized === 'researching' ||
711
+ normalized === 'searching the web' ||
712
+ (text.trimStart().startsWith('<system-reminder>') &&
713
+ /<system-reminder>[\\s\\S]*#\\s*plan mode\\b/i.test(text)) ||
714
+ tailIsPlanningPanel;
651
715
  const finished = Boolean(lastTurn?.querySelector(${finishedSelector})) &&
652
716
  textLength >= 40 &&
653
- !isToolStub;
717
+ !incompleteResult;
654
718
  const hasIframe = Array.from(document.querySelectorAll('iframe')).some(f => {
655
719
  const rect = f.getBoundingClientRect();
656
720
  return rect.width > 200 && rect.height > 200;
657
721
  });
658
- const hasActiveScopedResearch = scopedToNewTurns && Boolean(lastTurn) && hasIframe &&
659
- (textLength < 40 || isToolStub || /chatgpt\\s+said:?$/i.test(text));
660
- return { finished, stopVisible, textLength, hasIframe, isToolStub, hasActiveScopedResearch, accountBlocked };
722
+ const hasScopedDeepResearchIframe = Array.from(lastTurn?.querySelectorAll?.('iframe') || []).some(f => {
723
+ const rect = f.getBoundingClientRect();
724
+ const descriptor = String(f.getAttribute('src') || '') + ' ' + String(f.getAttribute('name') || '');
725
+ return rect.width > 200 && rect.height > 200 &&
726
+ /connector_openai_deep_research|deep-research/i.test(descriptor);
727
+ });
728
+ const hasActiveScopedResearch = scopedToNewTurns && Boolean(lastTurn) &&
729
+ hasScopedDeepResearchIframe &&
730
+ (textLength < 40 || isToolStub || tailIsPlanningPanel || /chatgpt\\s+said:?$/i.test(text));
731
+ return { finished, stopVisible, textLength, hasIframe, isToolStub, incompleteResult, researchActivity: tailIsPlanningPanel || (isToolStub && hasScopedDeepResearchIframe), hasActiveScopedResearch, accountBlocked };
661
732
  })()`;
662
733
  }
663
734
  export function buildDeepResearchStatusExpressionForTest() {