@steipete/oracle 0.13.0 → 0.14.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 (71) hide show
  1. package/README.md +22 -11
  2. package/dist/bin/oracle-cli.js +71 -71
  3. package/dist/src/browser/actions/assistantResponse.js +93 -10
  4. package/dist/src/browser/actions/deepResearch.js +180 -52
  5. package/dist/src/browser/actions/modelSelection.js +357 -40
  6. package/dist/src/browser/actions/navigation.js +294 -65
  7. package/dist/src/browser/actions/promptComposer.js +50 -4
  8. package/dist/src/browser/actions/thinkingTime.js +517 -105
  9. package/dist/src/browser/artifacts.js +19 -0
  10. package/dist/src/browser/chatgptFiles.js +797 -0
  11. package/dist/src/browser/chatgptImages.js +256 -27
  12. package/dist/src/browser/chromeLifecycle.js +4 -0
  13. package/dist/src/browser/config.js +2 -0
  14. package/dist/src/browser/index.js +404 -41
  15. package/dist/src/browser/pageActions.js +1 -1
  16. package/dist/src/browser/policies.js +2 -6
  17. package/dist/src/browser/projectSourcesRunner.js +16 -3
  18. package/dist/src/browser/prompt.js +161 -36
  19. package/dist/src/browser/reattach.js +6 -2
  20. package/dist/src/browser/reattachability.js +22 -10
  21. package/dist/src/browser/recoverConversation.js +73 -0
  22. package/dist/src/browser/sessionRunner.js +4 -1
  23. package/dist/src/cli/browserConfig.js +6 -2
  24. package/dist/src/cli/browserDefaults.js +2 -1
  25. package/dist/src/cli/browserTabs.js +129 -54
  26. package/dist/src/cli/followup.js +72 -0
  27. package/dist/src/cli/help.js +1 -1
  28. package/dist/src/cli/options.js +24 -0
  29. package/dist/src/cli/reattachGuidance.js +8 -0
  30. package/dist/src/cli/runOptions.js +2 -12
  31. package/dist/src/cli/sessionCommand.js +4 -0
  32. package/dist/src/cli/sessionDisplay.js +17 -3
  33. package/dist/src/cli/sessionLineage.js +7 -4
  34. package/dist/src/cli/sessionRunner.js +16 -1
  35. package/dist/src/gemini-web/client.js +5 -10
  36. package/dist/src/gemini-web/executor.js +1 -24
  37. package/dist/src/gemini-web/models.js +83 -0
  38. package/dist/src/mcp/server.js +2 -0
  39. package/dist/src/mcp/tools/chatgptImage.js +132 -0
  40. package/dist/src/mcp/tools/consult.js +270 -175
  41. package/dist/src/mcp/types.js +13 -2
  42. package/dist/src/mcp/utils.js +87 -1
  43. package/dist/src/oracle/config.js +27 -1
  44. package/dist/src/oracle/files.js +4 -6
  45. package/dist/src/oracle/geminiModels.js +2 -0
  46. package/dist/src/oracle/markdown.js +36 -3
  47. package/dist/src/oracle/modelResolver.js +21 -13
  48. package/dist/src/oracle/promptAssembly.js +2 -4
  49. package/dist/src/oracle/request.js +3 -5
  50. package/dist/src/oracle/thinkingTime.js +40 -0
  51. package/dist/src/oracle/tokenStats.js +5 -1
  52. package/dist/src/oracle.js +1 -1
  53. package/dist/src/sessionManager.js +1 -0
  54. package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
  55. package/package.json +47 -58
  56. package/vendor/oracle-notifier/build-notifier.sh +0 -0
  57. package/dist/bin/oracle.js +0 -569
  58. package/dist/src/browser/chromeCookies.js +0 -312
  59. package/dist/src/browser/keytarShim.js +0 -56
  60. package/dist/src/browser/windowsCookies.js +0 -219
  61. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  62. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
  63. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  64. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  65. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
  66. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  67. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
  68. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  69. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  70. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
  71. package/vendor/oracle-notifier/README.md +0 -26
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="MIT License"></a>
12
12
  </p>
13
13
 
14
- Oracle bundles your prompt and files so another AI can answer with real context. It speaks GPT-5.5 Pro (default), GPT-5.5, GPT-5.4 Pro, GPT-5.4, GPT-5.1 Pro, GPT-5.1 Codex (API-only), GPT-5.1, GPT-5.2, Gemini 3.1 Pro (API-only), Gemini 3 Pro, Claude Sonnet 4.6, Claude Opus 4.1, and more—and it can ask one or multiple models in a single run. Browser automation is available; use `--browser-model-strategy current` to keep the active ChatGPT model (or `ignore` to skip the picker). API remains the most reliable path, and `--copy` is an easy manual fallback.
14
+ Oracle bundles your prompt and files so another AI can answer with real context. It speaks GPT-5.5 Pro (default), GPT-5.5, GPT-5.4 Pro, GPT-5.4, GPT-5.1 Pro, GPT-5.1 Codex (API-only), GPT-5.1, GPT-5.2, Gemini 3.1 Pro, Gemini 3.5 Flash, Gemini 3.1 Flash-Lite, Claude Sonnet 4.6, Claude Opus 4.1, and more—and it can ask one or multiple models in a single run. Browser automation is available; use `--browser-model-strategy current` to keep the active ChatGPT model (or `ignore` to skip the picker). API remains the most reliable path, and `--copy` is an easy manual fallback.
15
15
 
16
16
  ## Setting up (macOS Browser Mode)
17
17
 
@@ -101,7 +101,7 @@ npx -y @steipete/oracle --engine browser --model gpt-5.5-pro \
101
101
  --browser-follow-up "Give the final decision"
102
102
 
103
103
  # Gemini browser mode (no API key; uses Chrome cookies from gemini.google.com)
104
- npx -y @steipete/oracle --engine browser --model gemini-3-pro --prompt "a cute robot holding a banana" --generate-image out.jpg --aspect 1:1
104
+ npx -y @steipete/oracle --engine browser --model gemini-3.1-pro --prompt "a cute robot holding a banana" --generate-image out.jpg --aspect 1:1
105
105
 
106
106
  # Sessions (list and replay)
107
107
  npx -y @steipete/oracle status --hours 72
@@ -118,13 +118,14 @@ Engine auto-picks API when `OPENAI_API_KEY` is set, otherwise browser; browser i
118
118
 
119
119
  **CLI**
120
120
 
121
- - API mode expects API keys in your environment: `OPENAI_API_KEY` (GPT-5.x), `GEMINI_API_KEY` (Gemini 3.1 Pro / Gemini 3 Pro), `ANTHROPIC_API_KEY` (Claude Sonnet 4.6 / Opus 4.1).
121
+ - API mode expects API keys in your environment: `OPENAI_API_KEY` (GPT-5.x), `GEMINI_API_KEY` (Gemini 3.1 Pro / 3.5 Flash / 3.1 Flash-Lite), `ANTHROPIC_API_KEY` (Claude Sonnet 4.6 / Opus 4.1).
122
122
  - Gemini browser mode uses Chrome cookies instead of an API key—just be logged into `gemini.google.com` in Chrome (no Python/venv required).
123
- - If your Gemini account can’t access “Pro”, Oracle auto-falls back to a supported model for web runs (and logs the fallback in verbose mode).
123
+ - Gemini browser mode accepts explicit `gemini-3.1-flash-lite`, `gemini-3.5-flash`, and `gemini-3.1-pro` IDs. Legacy `gemini-3-pro` browser runs target current Gemini 3.1 Pro. If your account can’t access the requested model, Oracle falls back to 3.1 Flash-Lite and logs the fallback in verbose mode.
124
124
  - Prefer API mode or `--copy` + manual paste; browser automation is experimental.
125
125
  - Browser support: stable on macOS; works on Linux (add `--browser-chrome-path/--browser-cookie-path` when needed) and Windows (manual-login or inline cookies recommended when app-bound cookies block decryption).
126
126
  - Remote browser service: `oracle serve` on a signed-in host; clients use `--remote-host/--remote-token`.
127
- - Browser artifacts: browser sessions save `transcript.md` and generated artifacts under `~/.oracle/sessions/<id>/artifacts/`. Deep Research saves `deep-research-report.md` when the report surface is captured; ChatGPT-generated images are downloaded with the active browser cookies when image URLs are present.
127
+ - Browser artifacts: browser sessions save `transcript.md` and generated artifacts under `~/.oracle/sessions/<id>/artifacts/`. Deep Research saves `deep-research-report.md` when the report surface is captured; ChatGPT-generated images and downloadable files are saved with the active browser session when supported file URLs are present.
128
+ - MCP image agents: use the `chatgpt_image` tool for the easiest path, or pass `generateImage` to `consult` with `engine: "browser"`; saved paths come back in `structuredContent.images`.
128
129
  - Browser archiving: by default, successful non-project, non-Deep-Research, non-multi-turn ChatGPT one-shots are archived after local artifacts are saved. Use `--browser-archive never` to disable or `--browser-archive always` to force archiving after a successful browser run. Archived chats remain manageable in ChatGPT.
129
130
  - Conversation mode guidance: use one-shot browser runs for narrow bug reports or quick file-set reviews; use explicit browser follow-ups for ambiguous architecture/product tradeoffs where a challenge pass and final decision are valuable; use Deep Research for broad public-web questions that need citations. Oracle never invents follow-ups automatically.
130
131
  - Project Sources: `oracle project-sources list|add --chatgpt-url <project-url>` manages the Project Sources tab in ChatGPT browser mode. v1 is append-only (`list`, `add`, `--dry-run`) so agents can share explicit project context without deleting or replacing user sources.
@@ -179,7 +180,7 @@ npx -y @steipete/oracle oracle-mcp
179
180
  - Claude Code / MCP browser consults can use the `chatgpt-pro-heavy` preset for a compact ChatGPT Pro second-opinion workflow.
180
181
  - Render/copy bundles for manual paste into ChatGPT when automation is blocked.
181
182
  - GPT‑5 Pro API runs detach by default; reattach via `oracle session <id>` / `oracle status` or block with `--wait`.
182
- - OpenAI/Azure follow-up API runs can continue from `--followup <sessionId|responseId>`; for multi-model parents, add `--followup-model <model>`.
183
+ - Saved ChatGPT browser conversations and OpenAI/Azure API runs can continue from `--followup <sessionId|responseId>`; for multi-model API parents, add `--followup-model <model>`.
183
184
  - Azure endpoints supported via `--azure-endpoint/--azure-deployment/--azure-api-version` or `AZURE_OPENAI_*` envs; use `--provider openai` / `--no-azure` to force first-party OpenAI when Azure env vars are present.
184
185
  - Redacted provider checks via `oracle doctor --providers`, `--route`, and `--preflight` before spending API time.
185
186
  - File safety: globs/excludes, size guards, `--files-report`.
@@ -212,7 +213,17 @@ Successful models write per-model files such as `/tmp/oracle-panel.gpt-5.4.md`;
212
213
 
213
214
  ## Follow-up and lineage
214
215
 
215
- Use `--followup` to continue an existing OpenAI/Azure Responses API run with additional context/files:
216
+ Use `--followup` to continue a saved ChatGPT browser conversation or an existing OpenAI/Azure Responses API run with additional context/files:
217
+
218
+ ```bash
219
+ oracle \
220
+ --followup <browser-session-id-or-slug> \
221
+ --slug "my-browser-followup" \
222
+ -p "Follow-up: review this additional file in the same conversation." \
223
+ --file "server/src/strategy/plan.ts"
224
+ ```
225
+
226
+ Browser followup reopens the exact saved conversation and inherits its browser profile, configuration, and model. Resume fails closed before submission if Oracle cannot verify the saved thread and prior turns.
216
227
 
217
228
  ```bash
218
229
  oracle \
@@ -277,9 +288,9 @@ Browser automation can open or control Chrome, so dry-runs and live runs print a
277
288
  | `-p, --prompt <text>` | Required prompt. |
278
289
  | `-f, --file <paths...>` | Attach files/dirs (globs + `!` excludes). |
279
290
  | `-e, --engine <api\|browser>` | Choose API or browser (browser is experimental). |
280
- | `-m, --model <name>` | Built-ins (`gpt-5.5-pro` default, `gpt-5.5`, `gpt-5.4-pro`, `gpt-5.4`, `gpt-5.1-pro`, `gpt-5-pro`, `gpt-5.1`, `gpt-5.1-codex`, `gpt-5.2`, `gpt-5.2-instant`, `gpt-5.2-pro`, `gemini-3.1-pro` API-only, `gemini-3-pro`, `claude-4.6-sonnet`, `claude-4.1-opus`) plus any OpenRouter id (e.g., `minimax/minimax-m2`, `openai/gpt-4o-mini`). |
291
+ | `-m, --model <name>` | Built-ins (`gpt-5.5-pro` default, `gpt-5.5`, `gpt-5.4-pro`, `gpt-5.4`, `gpt-5.1-pro`, `gpt-5-pro`, `gpt-5.1`, `gpt-5.1-codex`, `gpt-5.2`, `gpt-5.2-instant`, `gpt-5.2-pro`, `gemini-3.1-pro` API + UI, `gemini-3-pro`, `claude-4.6-sonnet`, `claude-4.1-opus`) plus any OpenRouter id (e.g., `minimax/minimax-m2`, `openai/gpt-4o-mini`). |
281
292
  | `--models <list>` | Comma-separated API models (mix built-ins and OpenRouter ids) for multi-model runs. |
282
- | `--followup <sessionId\|responseId>` | Continue an OpenAI/Azure Responses API run from a stored oracle session or `resp_...` response id. |
293
+ | `--followup <sessionId\|responseId>` | Continue a saved ChatGPT browser conversation or an OpenAI/Azure Responses API run from a stored Oracle session or `resp_...` response id. |
283
294
  | `--followup-model <model>` | For multi-model OpenAI/Azure parent sessions, choose which model response to continue from. |
284
295
  | `--base-url <url>` | Point API runs at LiteLLM/Azure/OpenRouter/etc. |
285
296
  | `--chatgpt-url <url>` | Target a ChatGPT workspace/folder or Temporary Chat URL (browser). |
@@ -291,8 +302,8 @@ Browser automation can open or control Chrome, so dry-runs and live runs print a
291
302
  | `--browser-research deep` | Activate ChatGPT Deep Research for broad web research and cited reports (browser only). |
292
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. |
293
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. |
294
- | `--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. |
295
- | `--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. |
296
307
  | `--browser-port <port>` | Pin the Chrome DevTools port (WSL/Windows firewall helper). |
297
308
  | `--browser-inline-cookies[(-file)] <payload \| path>` | Supply cookies without Chrome/Keychain (browser). |
298
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";
@@ -41,6 +41,7 @@ import { resolveConfiguredMaxFileSizeBytes } from "../src/cli/fileSize.js";
41
41
  import { isAzureOpenAICandidateModel, validateProviderRouting, } from "../src/oracle/providerRouting.js";
42
42
  import { buildSessionLifecycle, formatSessionLifecycleBlock } from "../src/cli/sessionLifecycle.js";
43
43
  import { buildDetachedPerfTraceEnv, createPerfTrace, isTraceValueFlag, } from "../src/cli/perfTrace.js";
44
+ import { resolveBrowserFollowupReference } from "../src/cli/followup.js";
44
45
  const VERSION = getCliVersion();
45
46
  const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
46
47
  const LEGACY_FLAG_ALIASES = new Map([
@@ -203,8 +204,8 @@ program
203
204
  .argument("[prompt]", "Prompt text (shorthand for --prompt).")
204
205
  .option("-p, --prompt <text>", "User prompt to send to the model.")
205
206
  .addOption(new Option("--message <text>", "Alias for --prompt.").hideHelp())
206
- .option("--followup <sessionId|responseId>", "Continue an OpenAI/Azure Responses API run from a stored response id (resp_...) or from a stored oracle session id.")
207
- .option("--followup-model <model>", "When following up a multi-model session, choose which model response to continue from.")
207
+ .option("--followup <sessionId|responseId>", "Continue a stored ChatGPT browser conversation or an OpenAI/Azure Responses API run.")
208
+ .option("--followup-model <model>", "For multi-model API sessions, choose which model response to continue from.")
208
209
  .option("-f, --file <paths...>", "Files/directories or glob patterns to attach (prefix with !pattern to exclude). Oversized files are rejected automatically (default cap: 1 MB; configurable via ORACLE_MAX_FILE_SIZE_BYTES or config.maxFileSizeBytes).", collectPaths, [])
209
210
  .option("--max-file-size-bytes <bytes>", "Reject files larger than this many bytes.", parseIntOption)
210
211
  .addOption(new Option("--include <paths...>", "Alias for --file.")
@@ -226,7 +227,7 @@ program
226
227
  .addOption(new Option("--copy-markdown", "Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.").default(false))
227
228
  .addOption(new Option("--copy").hideHelp().default(false))
228
229
  .option("-s, --slug <words>", "Custom session slug (3-5 words).")
229
- .option("-m, --model <model>", 'Model to target (gpt-5.5-pro default). Also gpt-5.5, gpt-5.4-pro, gpt-5.4, gpt-5.1-pro, gpt-5-pro, gpt-5.1, gpt-5.1-codex API-only, gpt-5.2, gpt-5.2-instant, gpt-5.2-pro, gemini-3.1-pro API-only, gemini-3-pro, claude-4.6-sonnet, claude-4.1-opus, or ChatGPT labels like "5.5 Pro" / "5.2 Thinking" for browser runs).', normalizeModelOption)
230
+ .option("-m, --model <model>", 'Model to target (gpt-5.5-pro default). Also gpt-5.5, gpt-5.4-pro, gpt-5.4, gpt-5.1-pro, gpt-5-pro, gpt-5.1, gpt-5.1-codex API-only, gpt-5.2, gpt-5.2-instant, gpt-5.2-pro, gemini-3.1-flash-lite, gemini-3.5-flash, gemini-3.1-pro, legacy gemini-3-pro, claude-4.6-sonnet, claude-4.1-opus, or ChatGPT labels like "5.5 Pro" / "5.2 Thinking" for browser runs).', normalizeModelOption)
230
231
  .addOption(new Option("--models <models>", 'Comma-separated API model list to query in parallel (e.g., "gpt-5.5-pro,gemini-3-pro").')
231
232
  .argParser(collectModelList)
232
233
  .default([]))
@@ -328,8 +329,8 @@ program
328
329
  .addOption(new Option("--browser-hide-window", "Hide the Chrome window after launch (macOS headful only).").hideHelp())
329
330
  .addOption(new Option("--browser-keep-browser", "Keep Chrome running after completion.").hideHelp())
330
331
  .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"]))
331
- .addOption(new Option("--browser-thinking-time <level>", "Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.")
332
- .choices(["light", "standard", "extended", "heavy"])
332
+ .addOption(new Option("--browser-thinking-time <level>", "Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy, or ChatGPT UI aliases.")
333
+ .argParser(parseThinkingTimeOption)
333
334
  .hideHelp())
334
335
  .addOption(new Option("--browser-research <mode>", "Browser research mode: deep activates ChatGPT Deep Research.").choices(["off", "deep"]))
335
336
  .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"]))
@@ -337,7 +338,7 @@ program
337
338
  .argParser(collectTextValues)
338
339
  .default([]))
339
340
  .addOption(new Option("--browser-allow-cookie-errors", "Continue even if Chrome cookies cannot be copied.").hideHelp())
340
- .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.")
341
+ .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.")
341
342
  .choices(["auto", "never", "always"])
342
343
  .default("auto"))
343
344
  .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."))
@@ -346,9 +347,9 @@ program
346
347
  .addOption(new Option("--remote-token <token>", "Access token for the remote `oracle serve` instance."))
347
348
  .addOption(new Option("--browser-inline-files", "Alias for --browser-attachments never (force pasting file contents inline).").default(false))
348
349
  .addOption(new Option("--browser-bundle-files", "Bundle all attachments into a single archive before uploading.").default(false))
349
- .addOption(new Option("--browser-bundle-format <format>", "Bundle format for browser uploads when files are bundled: text (default) or zip.")
350
- .choices(["text", "zip"])
351
- .default("text"))
350
+ .addOption(new Option("--browser-bundle-format <format>", "Bundle format for browser uploads when files are bundled: auto (default), text, or zip.")
351
+ .choices(["auto", "text", "zip"])
352
+ .default("auto"))
352
353
  .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)."))
353
354
  .addOption(new Option("--generate-image <file>", "Generate image and save to file (Gemini browser mode; ChatGPT browser mode saves downloadable image artifacts when present)."))
354
355
  .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)."))
@@ -553,6 +554,7 @@ program
553
554
  .option("--live", "Tail the live browser tab for this session until it completes, stalls, or detaches.", false)
554
555
  .option("--write-output <path>", "Write harvested browser output to this file (requires --harvest or --live).")
555
556
  .option("--browser-tab <ref>", "Override the browser tab ref used for harvesting/live tail (current, target id, URL, or title substring).")
557
+ .option("--no-recover", "Do not relaunch Chrome to reopen the saved conversation URL when --harvest/--live finds no live tab.")
556
558
  .addOption(new Option("--clean", "Deprecated alias for --clear.").default(false).hideHelp())
557
559
  .action(async (sessionId, _options, cmd) => {
558
560
  const { handleSessionCommand } = await import("../src/cli/sessionCommand.js");
@@ -657,6 +659,7 @@ function buildRunOptions(options, overrides = {}) {
657
659
  model: options.model,
658
660
  models: overrides.models ?? options.models,
659
661
  previousResponseId: overrides.previousResponseId ?? options.previousResponseId,
662
+ browserResumeConversationUrl: overrides.browserResumeConversationUrl ?? options.browserResumeConversationUrl,
660
663
  effectiveModelId: overrides.effectiveModelId ?? options.effectiveModelId ?? options.model,
661
664
  file: overrides.file ?? options.file ?? [],
662
665
  maxFileSizeBytes: overrides.maxFileSizeBytes ?? options.maxFileSizeBytes,
@@ -686,7 +689,7 @@ function buildRunOptions(options, overrides = {}) {
686
689
  "auto",
687
690
  browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
688
691
  browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
689
- browserBundleFormat: overrides.browserBundleFormat ?? options.browserBundleFormat ?? "text",
692
+ browserBundleFormat: overrides.browserBundleFormat ?? options.browserBundleFormat ?? "auto",
690
693
  generateImage: overrides.generateImage ?? options.generateImage,
691
694
  outputPath: overrides.outputPath ?? options.output,
692
695
  browserFollowUps: overrides.browserFollowUps ?? options.browserFollowUp ?? [],
@@ -894,6 +897,7 @@ function buildRunOptionsFromMetadata(metadata) {
894
897
  model: stored.model ?? DEFAULT_MODEL,
895
898
  models: stored.models,
896
899
  previousResponseId: stored.previousResponseId,
900
+ browserResumeConversationUrl: stored.browserResumeConversationUrl,
897
901
  effectiveModelId: stored.effectiveModelId ?? stored.model,
898
902
  file: stored.file ?? [],
899
903
  maxFileSizeBytes: stored.maxFileSizeBytes,
@@ -1167,18 +1171,6 @@ async function runRootCommand(options) {
1167
1171
  throw new Error("--remote-host does not support --models yet. Use API engine locally instead.");
1168
1172
  }
1169
1173
  const resolvedModel = normalizedMultiModels[0] ?? (isGemini ? resolveApiModel(cliModelArg) : resolvedModelCandidate);
1170
- const includesGeminiApiOnly = (normalizedMultiModels.length > 0 ? normalizedMultiModels : [resolvedModel]).some((model) => model === "gemini-3.1-pro");
1171
- if (browserExplicitlyRequested && includesGeminiApiOnly) {
1172
- throw new Error("gemini-3.1-pro is API-only today. Use --engine api or switch to gemini-3-pro for Gemini web.");
1173
- }
1174
- if (engine === "browser" && includesGeminiApiOnly) {
1175
- console.log(chalk.dim("gemini-3.1-pro is API-only today; switching to API."));
1176
- engine = "api";
1177
- }
1178
- const browserFollowUpCount = options.browserFollowUp?.filter((entry) => entry.trim().length > 0).length ?? 0;
1179
- if (engine !== "browser" && browserFollowUpCount > 0) {
1180
- throw new Error("--browser-follow-up requires --engine browser.");
1181
- }
1182
1174
  const effectiveModelId = resolvedModel.startsWith("gemini")
1183
1175
  ? resolveGeminiModelId(resolvedModel)
1184
1176
  : isKnownModel(resolvedModel)
@@ -1196,18 +1188,6 @@ async function runRootCommand(options) {
1196
1188
  resolvedOptions.effectiveModelId = effectiveModelId;
1197
1189
  resolvedOptions.provider = providerMode;
1198
1190
  resolvedOptions.writeOutputPath = resolveOutputPath(options.writeOutput, process.cwd());
1199
- // Decide whether to block until completion:
1200
- // - explicit --wait / --no-wait wins
1201
- // - otherwise block for fast models (gpt-5.1, browser) and detach by default for pro API runs
1202
- let waitPreference = resolveWaitFlag({
1203
- waitFlag: options.wait,
1204
- model: resolvedModel,
1205
- engine,
1206
- });
1207
- if (remoteHost && waitPreference === false) {
1208
- console.log(chalk.dim("Remote browser runs require --wait; ignoring --no-wait."));
1209
- waitPreference = true;
1210
- }
1211
1191
  if (options.status) {
1212
1192
  const { attachSession, showStatus } = await import("../src/cli/sessionDisplay.js");
1213
1193
  if (options.session) {
@@ -1280,16 +1260,53 @@ async function runRootCommand(options) {
1280
1260
  getSource("browserAttachmentTimeout") === "default")) {
1281
1261
  options.browserAttachmentTimeout = attachmentTimeoutEnv;
1282
1262
  }
1263
+ let browserFollowup = null;
1264
+ if (options.followup) {
1265
+ if (normalizedMultiModels.length > 0) {
1266
+ throw new Error("--followup cannot be combined with --models.");
1267
+ }
1268
+ browserFollowup = await resolveBrowserFollowupReference(options.followup, sessionStore);
1269
+ if (browserFollowup) {
1270
+ engine = "browser";
1271
+ resolvedOptions.model = browserFollowup.model;
1272
+ resolvedOptions.effectiveModelId = browserFollowup.model;
1273
+ resolvedOptions.followupSessionId = browserFollowup.sessionId;
1274
+ resolvedOptions.browserResumeConversationUrl = browserFollowup.resumeConversationUrl;
1275
+ }
1276
+ else {
1277
+ assertFollowupSupported({
1278
+ engine,
1279
+ model: resolvedModel,
1280
+ baseUrl: resolvedBaseUrl,
1281
+ azureEndpoint: resolvedOptions.azure?.endpoint,
1282
+ });
1283
+ const followup = await resolveFollowupReference(options.followup, options.followupModel);
1284
+ resolvedOptions.previousResponseId = followup.responseId;
1285
+ resolvedOptions.followupSessionId = followup.sessionId;
1286
+ resolvedOptions.followupModel = options.followupModel;
1287
+ }
1288
+ }
1289
+ const activeModel = resolvedOptions.model;
1290
+ const browserFollowUpCount = options.browserFollowUp?.filter((entry) => entry.trim().length > 0).length ?? 0;
1291
+ if (engine !== "browser" && browserFollowUpCount > 0) {
1292
+ throw new Error("--browser-follow-up requires --engine browser.");
1293
+ }
1283
1294
  const sessionMode = engine === "browser" ? "browser" : "api";
1284
1295
  const browserConfig = await (async () => {
1285
1296
  if (sessionMode !== "browser")
1286
1297
  return undefined;
1298
+ if (browserFollowup) {
1299
+ return browserFollowup.browserConfig;
1300
+ }
1287
1301
  const { buildBrowserConfig, resolveBrowserModelLabel } = await import("../src/cli/browserConfig.js");
1288
- return buildBrowserConfig({
1302
+ const config = await buildBrowserConfig({
1289
1303
  ...options,
1290
- model: resolvedModel,
1291
- browserModelLabel: resolveBrowserModelLabel(cliModelArg, resolvedModel),
1304
+ model: activeModel,
1305
+ browserModelLabel: resolveBrowserModelLabel(cliModelArg, activeModel),
1292
1306
  });
1307
+ return resolvedOptions.browserResumeConversationUrl
1308
+ ? { ...config, resumeConversationUrl: resolvedOptions.browserResumeConversationUrl }
1309
+ : config;
1293
1310
  })();
1294
1311
  if (previewMode) {
1295
1312
  if (!options.prompt) {
@@ -1299,21 +1316,6 @@ async function runRootCommand(options) {
1299
1316
  options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
1300
1317
  }
1301
1318
  resolvedOptions.prompt = options.prompt;
1302
- if (options.followup) {
1303
- assertFollowupSupported({
1304
- engine,
1305
- model: resolvedModel,
1306
- baseUrl: resolvedBaseUrl,
1307
- azureEndpoint: resolvedOptions.azure?.endpoint,
1308
- });
1309
- if (normalizedMultiModels.length > 0) {
1310
- throw new Error("--followup cannot be combined with --models.");
1311
- }
1312
- const followup = await resolveFollowupReference(options.followup, options.followupModel);
1313
- resolvedOptions.previousResponseId = followup.responseId;
1314
- resolvedOptions.followupSessionId = followup.sessionId;
1315
- resolvedOptions.followupModel = options.followupModel;
1316
- }
1317
1319
  const runOptions = buildRunOptions(resolvedOptions, {
1318
1320
  preview: true,
1319
1321
  previewMode,
@@ -1360,21 +1362,6 @@ async function runRootCommand(options) {
1360
1362
  options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
1361
1363
  }
1362
1364
  resolvedOptions.prompt = options.prompt;
1363
- if (options.followup) {
1364
- assertFollowupSupported({
1365
- engine,
1366
- model: resolvedModel,
1367
- baseUrl: resolvedBaseUrl,
1368
- azureEndpoint: resolvedOptions.azure?.endpoint,
1369
- });
1370
- if (normalizedMultiModels.length > 0) {
1371
- throw new Error("--followup cannot be combined with --models.");
1372
- }
1373
- const followup = await resolveFollowupReference(options.followup, options.followupModel);
1374
- resolvedOptions.previousResponseId = followup.responseId;
1375
- resolvedOptions.followupSessionId = followup.sessionId;
1376
- resolvedOptions.followupModel = options.followupModel;
1377
- }
1378
1365
  const duplicateBlocked = await shouldBlockDuplicatePrompt({
1379
1366
  prompt: resolvedOptions.prompt,
1380
1367
  browserFollowUps: resolvedOptions.browserFollowUp,
@@ -1413,7 +1400,7 @@ async function runRootCommand(options) {
1413
1400
  };
1414
1401
  console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
1415
1402
  }
1416
- else if (browserConfig && resolvedModel.startsWith("gemini")) {
1403
+ else if (browserConfig && activeModel.startsWith("gemini")) {
1417
1404
  const { createGeminiWebExecutor } = await import("../src/gemini-web/index.js");
1418
1405
  browserDeps = {
1419
1406
  executeBrowser: createGeminiWebExecutor({
@@ -1448,6 +1435,18 @@ async function runRootCommand(options) {
1448
1435
  }, {});
1449
1436
  return;
1450
1437
  }
1438
+ // Decide whether to block until completion:
1439
+ // - explicit --wait / --no-wait wins
1440
+ // - otherwise block for fast models (gpt-5.1, browser) and detach by default for pro API runs
1441
+ let waitPreference = resolveWaitFlag({
1442
+ waitFlag: options.wait,
1443
+ model: activeModel,
1444
+ engine,
1445
+ });
1446
+ if (remoteHost && waitPreference === false) {
1447
+ console.log(chalk.dim("Remote browser runs require --wait; ignoring --no-wait."));
1448
+ waitPreference = true;
1449
+ }
1451
1450
  await sessionStore.ensureStorage();
1452
1451
  const baseRunOptions = buildRunOptions(resolvedOptions, {
1453
1452
  preview: false,
@@ -1469,6 +1468,7 @@ async function runRootCommand(options) {
1469
1468
  browserConfig,
1470
1469
  followupSessionId: resolvedOptions.followupSessionId,
1471
1470
  followupModel: resolvedOptions.followupModel,
1471
+ browserResumeConversationUrl: resolvedOptions.browserResumeConversationUrl,
1472
1472
  waitPreference,
1473
1473
  youtube: options.youtube,
1474
1474
  generateImage: options.generateImage,
@@ -1480,14 +1480,14 @@ async function runRootCommand(options) {
1480
1480
  const liveRunOptions = {
1481
1481
  ...baseRunOptions,
1482
1482
  sessionId: sessionMeta.id,
1483
- effectiveModelId,
1483
+ effectiveModelId: resolvedOptions.effectiveModelId ?? effectiveModelId,
1484
1484
  };
1485
1485
  const disableDetachEnv = process.env.ORACLE_NO_DETACH === "1";
1486
1486
  const detachAllowed = remoteExecutionActive
1487
1487
  ? false
1488
1488
  : shouldDetachSession({
1489
1489
  engine,
1490
- model: resolvedModel,
1490
+ model: activeModel,
1491
1491
  waitPreference,
1492
1492
  disableDetachEnv,
1493
1493
  });
@@ -3,6 +3,31 @@ import { delay } from "../utils.js";
3
3
  import { logDomFailure, logConversationSnapshot, buildConversationDebugExpression, } from "../domDebug.js";
4
4
  import { buildClickDispatcher } from "./domEvents.js";
5
5
  const ASSISTANT_POLL_TIMEOUT_ERROR = "assistant-response-watchdog-timeout";
6
+ const THINKING_STATUS_LABELS = [
7
+ "thinking",
8
+ "pro thinking",
9
+ "thinking longer for a better answer",
10
+ "reasoning",
11
+ "finalizing answer",
12
+ "finalizing",
13
+ "analyzing",
14
+ "researching",
15
+ "working on it",
16
+ "working",
17
+ "planning",
18
+ "searching the web",
19
+ "searching",
20
+ "reading",
21
+ ];
22
+ function matchesThinkingStatusLabel(trimmed) {
23
+ if (!trimmed)
24
+ return false;
25
+ if (THINKING_STATUS_LABELS.includes(trimmed))
26
+ return true;
27
+ if (trimmed.startsWith("thought for ") && trimmed.length <= 40)
28
+ return true;
29
+ return trimmed.startsWith("pro thinking") && trimmed.length <= 40;
30
+ }
6
31
  function isAnswerNowPlaceholderText(normalized) {
7
32
  const text = normalized.trim();
8
33
  if (!text)
@@ -17,6 +42,26 @@ function isAnswerNowPlaceholderText(normalized) {
17
42
  }
18
43
  return (text.includes("answer now") && (text.includes("pro thinking") || text.includes("chatgpt said")));
19
44
  }
45
+ function buildActiveThinkingStatusPredicateJs(fnName) {
46
+ const labelsLiteral = JSON.stringify(THINKING_STATUS_LABELS);
47
+ const stopSelectorLiteral = JSON.stringify(STOP_BUTTON_SELECTOR);
48
+ return `const ${fnName} = (snapshot) => {
49
+ const normalized = String(snapshot?.text ?? '').toLowerCase().replace(/\\s+/g, ' ').trim();
50
+ if (!normalized) return false;
51
+ const labels = ${labelsLiteral};
52
+ const matches =
53
+ labels.includes(normalized) ||
54
+ (normalized.startsWith('thought for ') && normalized.length <= 40) ||
55
+ (normalized.startsWith('pro thinking') && normalized.length <= 40);
56
+ return matches && Boolean(document.querySelector(${stopSelectorLiteral}));
57
+ };`;
58
+ }
59
+ export function matchesThinkingStatusLabelForTest(text) {
60
+ return matchesThinkingStatusLabel(text.toLowerCase().replace(/\s+/g, " ").trim());
61
+ }
62
+ export function buildActiveThinkingStatusPredicateJsForTest(fnName) {
63
+ return buildActiveThinkingStatusPredicateJs(fnName);
64
+ }
20
65
  export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId) {
21
66
  const start = Date.now();
22
67
  logger("Waiting for ChatGPT response");
@@ -464,12 +509,22 @@ function buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId)
464
509
  }
465
510
  return normalized.includes('answer now') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'));
466
511
  };
467
- if (extracted && extracted.text && !isPlaceholder(extracted)) {
512
+ ${buildActiveThinkingStatusPredicateJs("isActiveThinkingStatus")}
513
+ if (
514
+ extracted &&
515
+ extracted.text &&
516
+ !isPlaceholder(extracted) &&
517
+ !isActiveThinkingStatus(extracted)
518
+ ) {
468
519
  return extracted;
469
520
  }
470
521
  // Fallback for ChatGPT project view: answers can live outside conversation turns.
471
- const fallback = ${buildMarkdownFallbackExtractor("MIN_TURN_INDEX")};
472
- return fallback ?? extracted;
522
+ const extractFallback = ${buildMarkdownFallbackExtractor("MIN_TURN_INDEX")};
523
+ const fallback = extractFallback();
524
+ if (fallback && !isPlaceholder(fallback) && !isActiveThinkingStatus(fallback)) {
525
+ return fallback;
526
+ }
527
+ return null;
473
528
  })()`;
474
529
  }
475
530
  function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConversationId) {
@@ -509,6 +564,7 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
509
564
  }
510
565
  return normalized.includes('answer now') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'));
511
566
  };
567
+ ${buildActiveThinkingStatusPredicateJs("isActiveThinkingStatus")}
512
568
 
513
569
  // Helper to detect assistant turns - must match buildAssistantExtractor logic for consistency.
514
570
  const isAssistantTurn = (node) => {
@@ -574,12 +630,20 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
574
630
  try {
575
631
  const extractedRaw = extractFromTurns();
576
632
  const extractedCandidate =
577
- extractedRaw && !isAnswerNowPlaceholder(extractedRaw) ? extractedRaw : null;
633
+ extractedRaw &&
634
+ !isAnswerNowPlaceholder(extractedRaw) &&
635
+ !isActiveThinkingStatus(extractedRaw)
636
+ ? extractedRaw
637
+ : null;
578
638
  let extracted = acceptSnapshot(extractedCandidate);
579
639
  if (!extracted) {
580
640
  const fallbackRaw = extractFromMarkdownFallback();
581
641
  const fallbackCandidate =
582
- fallbackRaw && !isAnswerNowPlaceholder(fallbackRaw) ? fallbackRaw : null;
642
+ fallbackRaw &&
643
+ !isAnswerNowPlaceholder(fallbackRaw) &&
644
+ !isActiveThinkingStatus(fallbackRaw)
645
+ ? fallbackRaw
646
+ : null;
583
647
  extracted = acceptSnapshot(fallbackCandidate);
584
648
  }
585
649
  if (extracted) {
@@ -658,12 +722,20 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
658
722
  await new Promise((resolve) => setTimeout(resolve, settleIntervalMs));
659
723
  const refreshedRaw = extractFromTurns();
660
724
  const refreshedCandidate =
661
- refreshedRaw && !isAnswerNowPlaceholder(refreshedRaw) ? refreshedRaw : null;
725
+ refreshedRaw &&
726
+ !isAnswerNowPlaceholder(refreshedRaw) &&
727
+ !isActiveThinkingStatus(refreshedRaw)
728
+ ? refreshedRaw
729
+ : null;
662
730
  let refreshed = acceptSnapshot(refreshedCandidate);
663
731
  if (!refreshed) {
664
732
  const fallbackRaw = extractFromMarkdownFallback();
665
733
  const fallbackCandidate =
666
- fallbackRaw && !isAnswerNowPlaceholder(fallbackRaw) ? fallbackRaw : null;
734
+ fallbackRaw &&
735
+ !isAnswerNowPlaceholder(fallbackRaw) &&
736
+ !isActiveThinkingStatus(fallbackRaw)
737
+ ? fallbackRaw
738
+ : null;
667
739
  refreshed = acceptSnapshot(fallbackCandidate);
668
740
  }
669
741
  const nextLength = refreshed?.text?.length ?? lastLength;
@@ -687,11 +759,21 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
687
759
  };
688
760
 
689
761
  const extractedRaw = extractFromTurns();
690
- const extractedCandidate = extractedRaw && !isAnswerNowPlaceholder(extractedRaw) ? extractedRaw : null;
762
+ const extractedCandidate =
763
+ extractedRaw &&
764
+ !isAnswerNowPlaceholder(extractedRaw) &&
765
+ !isActiveThinkingStatus(extractedRaw)
766
+ ? extractedRaw
767
+ : null;
691
768
  let extracted = acceptSnapshot(extractedCandidate);
692
769
  if (!extracted) {
693
770
  const fallbackRaw = extractFromMarkdownFallback();
694
- const fallbackCandidate = fallbackRaw && !isAnswerNowPlaceholder(fallbackRaw) ? fallbackRaw : null;
771
+ const fallbackCandidate =
772
+ fallbackRaw &&
773
+ !isAnswerNowPlaceholder(fallbackRaw) &&
774
+ !isActiveThinkingStatus(fallbackRaw)
775
+ ? fallbackRaw
776
+ : null;
695
777
  extracted = acceptSnapshot(fallbackCandidate);
696
778
  }
697
779
  if (extracted) {
@@ -775,7 +857,8 @@ function buildAssistantExtractor(functionName) {
775
857
  !normalizedText ||
776
858
  normalizedText === 'edit' ||
777
859
  normalizedText === 'stopped thinking' ||
778
- 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);
779
862
  if (generatedImages.length > 0 && imageOnlyChrome) {
780
863
  const label = generatedImages.length === 1 ? 'Generated image.' : \`Generated \${generatedImages.length} images.\`;
781
864
  return { text: label, html: messageRoot?.innerHTML ?? html, messageId, turnId, turnIndex: index };