@indykish/oracle 0.9.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 (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/assets-oracle-icon.png +0 -0
  4. package/dist/bin/oracle-cli.js +1252 -0
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/scripts/agent-send.js +147 -0
  7. package/dist/scripts/browser-tools.js +536 -0
  8. package/dist/scripts/check.js +21 -0
  9. package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
  10. package/dist/scripts/docs-list.js +110 -0
  11. package/dist/scripts/git-policy.js +125 -0
  12. package/dist/scripts/run-cli.js +14 -0
  13. package/dist/scripts/runner.js +1378 -0
  14. package/dist/scripts/test-browser.js +103 -0
  15. package/dist/scripts/test-remote-chrome.js +68 -0
  16. package/dist/src/bridge/connection.js +103 -0
  17. package/dist/src/bridge/userConfigFile.js +28 -0
  18. package/dist/src/browser/actions/assistantResponse.js +1067 -0
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
  20. package/dist/src/browser/actions/attachments.js +1910 -0
  21. package/dist/src/browser/actions/domEvents.js +19 -0
  22. package/dist/src/browser/actions/modelSelection.js +485 -0
  23. package/dist/src/browser/actions/navigation.js +445 -0
  24. package/dist/src/browser/actions/promptComposer.js +485 -0
  25. package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
  26. package/dist/src/browser/actions/thinkingTime.js +206 -0
  27. package/dist/src/browser/chromeLifecycle.js +344 -0
  28. package/dist/src/browser/config.js +103 -0
  29. package/dist/src/browser/constants.js +71 -0
  30. package/dist/src/browser/cookies.js +191 -0
  31. package/dist/src/browser/detect.js +164 -0
  32. package/dist/src/browser/domDebug.js +36 -0
  33. package/dist/src/browser/index.js +1741 -0
  34. package/dist/src/browser/modelStrategy.js +13 -0
  35. package/dist/src/browser/pageActions.js +5 -0
  36. package/dist/src/browser/policies.js +43 -0
  37. package/dist/src/browser/profileState.js +280 -0
  38. package/dist/src/browser/prompt.js +152 -0
  39. package/dist/src/browser/promptSummary.js +20 -0
  40. package/dist/src/browser/reattach.js +186 -0
  41. package/dist/src/browser/reattachHelpers.js +382 -0
  42. package/dist/src/browser/sessionRunner.js +119 -0
  43. package/dist/src/browser/types.js +1 -0
  44. package/dist/src/browser/utils.js +122 -0
  45. package/dist/src/browserMode.js +1 -0
  46. package/dist/src/cli/bridge/claudeConfig.js +54 -0
  47. package/dist/src/cli/bridge/client.js +73 -0
  48. package/dist/src/cli/bridge/codexConfig.js +43 -0
  49. package/dist/src/cli/bridge/doctor.js +107 -0
  50. package/dist/src/cli/bridge/host.js +259 -0
  51. package/dist/src/cli/browserConfig.js +278 -0
  52. package/dist/src/cli/browserDefaults.js +81 -0
  53. package/dist/src/cli/bundleWarnings.js +9 -0
  54. package/dist/src/cli/clipboard.js +10 -0
  55. package/dist/src/cli/detach.js +11 -0
  56. package/dist/src/cli/dryRun.js +105 -0
  57. package/dist/src/cli/duplicatePromptGuard.js +14 -0
  58. package/dist/src/cli/engine.js +41 -0
  59. package/dist/src/cli/errorUtils.js +9 -0
  60. package/dist/src/cli/format.js +13 -0
  61. package/dist/src/cli/help.js +77 -0
  62. package/dist/src/cli/hiddenAliases.js +22 -0
  63. package/dist/src/cli/markdownBundle.js +17 -0
  64. package/dist/src/cli/markdownRenderer.js +97 -0
  65. package/dist/src/cli/notifier.js +306 -0
  66. package/dist/src/cli/options.js +281 -0
  67. package/dist/src/cli/oscUtils.js +2 -0
  68. package/dist/src/cli/promptRequirement.js +17 -0
  69. package/dist/src/cli/renderFlags.js +9 -0
  70. package/dist/src/cli/renderOutput.js +26 -0
  71. package/dist/src/cli/rootAlias.js +30 -0
  72. package/dist/src/cli/runOptions.js +78 -0
  73. package/dist/src/cli/sessionCommand.js +111 -0
  74. package/dist/src/cli/sessionDisplay.js +567 -0
  75. package/dist/src/cli/sessionRunner.js +602 -0
  76. package/dist/src/cli/sessionTable.js +92 -0
  77. package/dist/src/cli/tagline.js +258 -0
  78. package/dist/src/cli/tui/index.js +486 -0
  79. package/dist/src/cli/writeOutputPath.js +21 -0
  80. package/dist/src/config.js +26 -0
  81. package/dist/src/gemini-web/client.js +328 -0
  82. package/dist/src/gemini-web/executor.js +285 -0
  83. package/dist/src/gemini-web/index.js +1 -0
  84. package/dist/src/gemini-web/types.js +1 -0
  85. package/dist/src/heartbeat.js +43 -0
  86. package/dist/src/mcp/server.js +40 -0
  87. package/dist/src/mcp/tools/consult.js +290 -0
  88. package/dist/src/mcp/tools/sessionResources.js +75 -0
  89. package/dist/src/mcp/tools/sessions.js +105 -0
  90. package/dist/src/mcp/types.js +22 -0
  91. package/dist/src/mcp/utils.js +37 -0
  92. package/dist/src/oracle/background.js +141 -0
  93. package/dist/src/oracle/claude.js +101 -0
  94. package/dist/src/oracle/client.js +197 -0
  95. package/dist/src/oracle/config.js +227 -0
  96. package/dist/src/oracle/errors.js +132 -0
  97. package/dist/src/oracle/files.js +378 -0
  98. package/dist/src/oracle/finishLine.js +32 -0
  99. package/dist/src/oracle/format.js +30 -0
  100. package/dist/src/oracle/fsAdapter.js +10 -0
  101. package/dist/src/oracle/gemini.js +195 -0
  102. package/dist/src/oracle/logging.js +36 -0
  103. package/dist/src/oracle/markdown.js +46 -0
  104. package/dist/src/oracle/modelResolver.js +183 -0
  105. package/dist/src/oracle/multiModelRunner.js +153 -0
  106. package/dist/src/oracle/oscProgress.js +24 -0
  107. package/dist/src/oracle/promptAssembly.js +13 -0
  108. package/dist/src/oracle/request.js +50 -0
  109. package/dist/src/oracle/run.js +596 -0
  110. package/dist/src/oracle/runUtils.js +31 -0
  111. package/dist/src/oracle/tokenEstimate.js +37 -0
  112. package/dist/src/oracle/tokenStats.js +39 -0
  113. package/dist/src/oracle/tokenStringifier.js +24 -0
  114. package/dist/src/oracle/types.js +1 -0
  115. package/dist/src/oracle.js +12 -0
  116. package/dist/src/oracleHome.js +13 -0
  117. package/dist/src/remote/client.js +129 -0
  118. package/dist/src/remote/health.js +113 -0
  119. package/dist/src/remote/remoteServiceConfig.js +31 -0
  120. package/dist/src/remote/server.js +533 -0
  121. package/dist/src/remote/types.js +1 -0
  122. package/dist/src/sessionManager.js +637 -0
  123. package/dist/src/sessionStore.js +56 -0
  124. package/dist/src/version.js +39 -0
  125. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  126. package/dist/vendor/oracle-notifier/README.md +24 -0
  127. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  128. package/package.json +115 -0
  129. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  130. package/vendor/oracle-notifier/README.md +24 -0
  131. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,1252 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import { spawn } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { once } from 'node:events';
6
+ import { Command, Option } from 'commander';
7
+ // Allow `npx @steipete/oracle oracle-mcp` to resolve the MCP server even though npx runs the default binary.
8
+ if (process.argv[2] === 'oracle-mcp') {
9
+ const { startMcpServer } = await import('../src/mcp/server.js');
10
+ await startMcpServer();
11
+ process.exit(0);
12
+ }
13
+ import { resolveEngine, defaultWaitPreference } from '../src/cli/engine.js';
14
+ import { shouldRequirePrompt } from '../src/cli/promptRequirement.js';
15
+ import chalk from 'chalk';
16
+ import { sessionStore, pruneOldSessions } from '../src/sessionStore.js';
17
+ import { DEFAULT_MODEL, MODEL_CONFIGS, readFiles, estimateRequestTokens, buildRequestBody } from '../src/oracle.js';
18
+ import { isKnownModel } from '../src/oracle/modelResolver.js';
19
+ import { CHATGPT_URL } from '../src/browserMode.js';
20
+ import { createRemoteBrowserExecutor } from '../src/remote/client.js';
21
+ import { createGeminiWebExecutor } from '../src/gemini-web/index.js';
22
+ import { applyHelpStyling } from '../src/cli/help.js';
23
+ import { collectPaths, collectModelList, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, parseDurationOption, mergePathLikeOptions, dedupePathInputs, } from '../src/cli/options.js';
24
+ import { copyToClipboard } from '../src/cli/clipboard.js';
25
+ import { buildMarkdownBundle } from '../src/cli/markdownBundle.js';
26
+ import { shouldDetachSession } from '../src/cli/detach.js';
27
+ import { applyHiddenAliases } from '../src/cli/hiddenAliases.js';
28
+ import { buildBrowserConfig, resolveBrowserModelLabel } from '../src/cli/browserConfig.js';
29
+ import { performSessionRun } from '../src/cli/sessionRunner.js';
30
+ import { isMediaFile } from '../src/browser/prompt.js';
31
+ import { attachSession, showStatus, formatCompletionSummary } from '../src/cli/sessionDisplay.js';
32
+ import { formatCompactNumber } from '../src/cli/format.js';
33
+ import { formatIntroLine } from '../src/cli/tagline.js';
34
+ import { warnIfOversizeBundle } from '../src/cli/bundleWarnings.js';
35
+ import { formatRenderedMarkdown } from '../src/cli/renderOutput.js';
36
+ import { resolveRenderFlag, resolveRenderPlain } from '../src/cli/renderFlags.js';
37
+ import { resolveGeminiModelId } from '../src/oracle/gemini.js';
38
+ import { handleSessionCommand, formatSessionCleanupMessage } from '../src/cli/sessionCommand.js';
39
+ import { isErrorLogged } from '../src/cli/errorUtils.js';
40
+ import { handleSessionAlias, handleStatusFlag } from '../src/cli/rootAlias.js';
41
+ import { resolveOutputPath } from '../src/cli/writeOutputPath.js';
42
+ import { getCliVersion } from '../src/version.js';
43
+ import { runDryRunSummary, runBrowserPreview } from '../src/cli/dryRun.js';
44
+ import { launchTui } from '../src/cli/tui/index.js';
45
+ import { resolveNotificationSettings, deriveNotificationSettingsFromMetadata, } from '../src/cli/notifier.js';
46
+ import { loadUserConfig } from '../src/config.js';
47
+ import { applyBrowserDefaultsFromConfig } from '../src/cli/browserDefaults.js';
48
+ import { shouldBlockDuplicatePrompt } from '../src/cli/duplicatePromptGuard.js';
49
+ import { resolveRemoteServiceConfig } from '../src/remote/remoteServiceConfig.js';
50
+ const VERSION = getCliVersion();
51
+ const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
52
+ const LEGACY_FLAG_ALIASES = new Map([
53
+ ['--[no-]notify', '--notify'],
54
+ ['--[no-]notify-sound', '--notify-sound'],
55
+ ['--[no-]background', '--background'],
56
+ ]);
57
+ const normalizedArgv = process.argv.map((arg, index) => {
58
+ if (index < 2)
59
+ return arg;
60
+ return LEGACY_FLAG_ALIASES.get(arg) ?? arg;
61
+ });
62
+ const rawCliArgs = normalizedArgv.slice(2);
63
+ const userCliArgs = rawCliArgs[0] === CLI_ENTRYPOINT ? rawCliArgs.slice(1) : rawCliArgs;
64
+ const isTty = process.stdout.isTTY;
65
+ const program = new Command();
66
+ let introPrinted = false;
67
+ program.hook('preAction', () => {
68
+ if (introPrinted)
69
+ return;
70
+ console.log(formatIntroLine(VERSION, { env: process.env, richTty: isTty }));
71
+ introPrinted = true;
72
+ });
73
+ applyHelpStyling(program, VERSION, isTty);
74
+ program.hook('preAction', (thisCommand) => {
75
+ if (thisCommand !== program) {
76
+ return;
77
+ }
78
+ if (userCliArgs.some((arg) => arg === '--help' || arg === '-h')) {
79
+ return;
80
+ }
81
+ if (userCliArgs.length === 0) {
82
+ // Let the root action handle zero-arg entry (help + hint to `oracle tui`).
83
+ return;
84
+ }
85
+ const opts = thisCommand.optsWithGlobals();
86
+ applyHiddenAliases(opts, (key, value) => thisCommand.setOptionValue(key, value));
87
+ const positional = thisCommand.args?.[0];
88
+ if (!opts.prompt && positional) {
89
+ opts.prompt = positional;
90
+ thisCommand.setOptionValue('prompt', positional);
91
+ }
92
+ if (shouldRequirePrompt(userCliArgs, opts)) {
93
+ console.log(chalk.yellow('Prompt is required. Provide it via --prompt "<text>" or positional [prompt].'));
94
+ thisCommand.help({ error: false });
95
+ process.exitCode = 1;
96
+ return;
97
+ }
98
+ });
99
+ program
100
+ .name('oracle')
101
+ .description('One-shot GPT-5.2 Pro / GPT-5.2 / GPT-5.1 Codex tool for hard questions that benefit from large file context and server-side search.')
102
+ .version(VERSION)
103
+ .argument('[prompt]', 'Prompt text (shorthand for --prompt).')
104
+ .option('-p, --prompt <text>', 'User prompt to send to the model.')
105
+ .addOption(new Option('--message <text>', 'Alias for --prompt.').hideHelp())
106
+ .option('-f, --file <paths...>', 'Files/directories or glob patterns to attach (prefix with !pattern to exclude). Files larger than 1 MB are rejected automatically.', collectPaths, [])
107
+ .addOption(new Option('--include <paths...>', 'Alias for --file.')
108
+ .argParser(collectPaths)
109
+ .default([])
110
+ .hideHelp())
111
+ .addOption(new Option('--files <paths...>', 'Alias for --file.')
112
+ .argParser(collectPaths)
113
+ .default([])
114
+ .hideHelp())
115
+ .addOption(new Option('--path <paths...>', 'Alias for --file.')
116
+ .argParser(collectPaths)
117
+ .default([])
118
+ .hideHelp())
119
+ .addOption(new Option('--paths <paths...>', 'Alias for --file.')
120
+ .argParser(collectPaths)
121
+ .default([])
122
+ .hideHelp())
123
+ .addOption(new Option('--copy-markdown', 'Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.').default(false))
124
+ .addOption(new Option('--copy').hideHelp().default(false))
125
+ .option('-s, --slug <words>', 'Custom session slug (3-5 words).')
126
+ .option('-m, --model <model>', 'Model to target (gpt-5.2-pro default; also supports gpt-5.1-pro alias). Also 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-pro, claude-4.5-sonnet, claude-4.1-opus, or ChatGPT labels like "5.2 Thinking" for browser runs).', normalizeModelOption)
127
+ .addOption(new Option('--models <models>', 'Comma-separated API model list to query in parallel (e.g., "gpt-5.2-pro,gemini-3-pro").')
128
+ .argParser(collectModelList)
129
+ .default([]))
130
+ .addOption(new Option('-e, --engine <mode>', 'Execution engine (api | browser). Browser engine: GPT models automate ChatGPT; Gemini models use a cookie-based client for gemini.google.com. If omitted, oracle picks api when OPENAI_API_KEY is set, otherwise browser.').choices(['api', 'browser']))
131
+ .addOption(new Option('--mode <mode>', 'Alias for --engine (api | browser).').choices(['api', 'browser']).hideHelp())
132
+ .option('--files-report', 'Show token usage per attached file (also prints automatically when files exceed the token budget).', false)
133
+ .option('-v, --verbose', 'Enable verbose logging for all operations.', false)
134
+ .addOption(new Option('--notify', 'Desktop notification when a session finishes (default on unless CI/SSH).').default(undefined))
135
+ .addOption(new Option('--no-notify', 'Disable desktop notifications.').default(undefined))
136
+ .addOption(new Option('--notify-sound', 'Play a notification sound on completion (default off).').default(undefined))
137
+ .addOption(new Option('--no-notify-sound', 'Disable notification sounds.').default(undefined))
138
+ .addOption(new Option('--timeout <seconds|auto>', 'Overall timeout before aborting the API call (auto = 60m for gpt-5.2-pro, 120s otherwise).')
139
+ .argParser(parseTimeoutOption)
140
+ .default('auto'))
141
+ .addOption(new Option('--background', 'Use Responses API background mode (create + retrieve) for API runs.').default(undefined))
142
+ .addOption(new Option('--no-background', 'Disable Responses API background mode.').default(undefined))
143
+ .addOption(new Option('--http-timeout <ms|s|m|h>', 'HTTP client timeout for API requests (default 20m).')
144
+ .argParser((value) => parseDurationOption(value, 'HTTP timeout'))
145
+ .default(undefined))
146
+ .addOption(new Option('--zombie-timeout <ms|s|m|h>', 'Override stale-session cutoff used by `oracle status` (default 60m).')
147
+ .argParser((value) => parseDurationOption(value, 'Zombie timeout'))
148
+ .default(undefined))
149
+ .option('--zombie-last-activity', 'Base stale-session detection on last log activity instead of start time.', false)
150
+ .addOption(new Option('--preview [mode]', '(alias) Preview the request without calling the model (summary | json | full). Deprecated: use --dry-run instead.')
151
+ .hideHelp()
152
+ .choices(['summary', 'json', 'full'])
153
+ .preset('summary'))
154
+ .addOption(new Option('--dry-run [mode]', 'Preview without calling the model (summary | json | full).')
155
+ .choices(['summary', 'json', 'full'])
156
+ .preset('summary')
157
+ .default(false))
158
+ .addOption(new Option('--exec-session <id>').hideHelp())
159
+ .addOption(new Option('--session <id>').hideHelp())
160
+ .addOption(new Option('--status', 'Show stored sessions (alias for `oracle status`).').default(false).hideHelp())
161
+ .option('--render-markdown', 'Print the assembled markdown bundle for prompt + files and exit; pair with --copy to put it on the clipboard.', false)
162
+ .option('--render', 'Alias for --render-markdown.', false)
163
+ .option('--render-plain', 'Render markdown without ANSI/highlighting (use plain text even in a TTY).', false)
164
+ .option('--write-output <path>', 'Write only the final assistant message to this file (overwrites; multi-model appends .<model> before the extension).')
165
+ .option('--verbose-render', 'Show render/TTY diagnostics when replaying sessions.', false)
166
+ .addOption(new Option('--search <mode>', 'Set server-side search behavior (on/off).')
167
+ .argParser(parseSearchOption)
168
+ .hideHelp())
169
+ .addOption(new Option('--max-input <tokens>', 'Override the input token budget for the selected model.')
170
+ .argParser(parseIntOption)
171
+ .hideHelp())
172
+ .addOption(new Option('--max-output <tokens>', 'Override the max output tokens for the selected model.')
173
+ .argParser(parseIntOption)
174
+ .hideHelp())
175
+ .option('--base-url <url>', 'Override the OpenAI-compatible base URL for API runs (e.g. LiteLLM proxy endpoint).')
176
+ .option('--azure-endpoint <url>', 'Azure OpenAI Endpoint (e.g. https://resource.openai.azure.com/).')
177
+ .option('--azure-deployment <name>', 'Azure OpenAI Deployment Name.')
178
+ .option('--azure-api-version <version>', 'Azure OpenAI API Version.')
179
+ .addOption(new Option('--browser', '(deprecated) Use --engine browser instead.').default(false).hideHelp())
180
+ .addOption(new Option('--browser-chrome-profile <name>', 'Chrome profile name/path for cookie reuse.').hideHelp())
181
+ .addOption(new Option('--browser-chrome-path <path>', 'Explicit Chrome or Chromium executable path.').hideHelp())
182
+ .addOption(new Option('--browser-cookie-path <path>', 'Explicit Chrome/Chromium cookie DB path for session reuse.'))
183
+ .addOption(new Option('--chatgpt-url <url>', `Override the ChatGPT web URL (e.g., workspace/folder like https://chatgpt.com/g/.../project; default ${CHATGPT_URL}).`))
184
+ .addOption(new Option('--browser-url <url>', `Alias for --chatgpt-url (default ${CHATGPT_URL}).`).hideHelp())
185
+ .addOption(new Option('--browser-timeout <ms|s|m>', 'Maximum time to wait for an answer (default 1200s / 20m).').hideHelp())
186
+ .addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 60s).').hideHelp())
187
+ .addOption(new Option('--browser-recheck-delay <ms|s|m|h>', 'After an assistant timeout, wait this long then revisit the conversation to retry capture.').hideHelp())
188
+ .addOption(new Option('--browser-recheck-timeout <ms|s|m|h>', 'Time budget for the delayed recheck attempt (default 120s).').hideHelp())
189
+ .addOption(new Option('--browser-reuse-wait <ms|s|m|h>', 'Wait for a shared Chrome profile to appear before launching a new one (helps parallel runs).').hideHelp())
190
+ .addOption(new Option('--browser-profile-lock-timeout <ms|s|m|h>', 'Wait for the shared manual-login profile lock before sending (serializes parallel runs).').hideHelp())
191
+ .addOption(new Option('--browser-auto-reattach-delay <ms|s|m|h>', 'Delay before starting periodic auto-reattach attempts after a timeout.').hideHelp())
192
+ .addOption(new Option('--browser-auto-reattach-interval <ms|s|m|h>', 'Interval between auto-reattach attempts (0 disables).').hideHelp())
193
+ .addOption(new Option('--browser-auto-reattach-timeout <ms|s|m|h>', 'Time budget for each auto-reattach attempt (default 120s).').hideHelp())
194
+ .addOption(new Option('--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.').hideHelp())
195
+ .addOption(new Option('--browser-port <port>', 'Use a fixed Chrome DevTools port (helpful on WSL firewalls).')
196
+ .argParser(parseIntOption))
197
+ .addOption(new Option('--browser-debug-port <port>', '(alias) Use a fixed Chrome DevTools port.').argParser(parseIntOption).hideHelp())
198
+ .addOption(new Option('--browser-cookie-names <names>', 'Comma-separated cookie allowlist for sync.').hideHelp())
199
+ .addOption(new Option('--browser-inline-cookies <jsonOrBase64>', 'Inline cookies payload (JSON array or base64-encoded JSON).').hideHelp())
200
+ .addOption(new Option('--browser-inline-cookies-file <path>', 'Load inline cookies from file (JSON or base64 JSON).').hideHelp())
201
+ .addOption(new Option('--browser-no-cookie-sync', 'Skip copying cookies from Chrome.').hideHelp())
202
+ .addOption(new Option('--browser-manual-login', 'Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login.').hideHelp())
203
+ .addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
204
+ .addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
205
+ .addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
206
+ .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']))
207
+ .addOption(new Option('--browser-thinking-time <level>', 'Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.')
208
+ .choices(['light', 'standard', 'extended', 'heavy'])
209
+ .hideHelp())
210
+ .addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
211
+ .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.')
212
+ .choices(['auto', 'never', 'always'])
213
+ .default('auto'))
214
+ .addOption(new Option('--remote-chrome <host:port>', 'Connect to remote Chrome DevTools Protocol (e.g., 192.168.1.10:9222 or [2001:db8::1]:9222 for IPv6).'))
215
+ .addOption(new Option('--remote-host <host:port>', 'Delegate browser runs to a remote `oracle serve` instance.'))
216
+ .addOption(new Option('--remote-token <token>', 'Access token for the remote `oracle serve` instance.'))
217
+ .addOption(new Option('--browser-inline-files', 'Alias for --browser-attachments never (force pasting file contents inline).').default(false))
218
+ .addOption(new Option('--browser-bundle-files', 'Bundle all attachments into a single archive before uploading.').default(false))
219
+ .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).'))
220
+ .addOption(new Option('--generate-image <file>', 'Generate image and save to file (Gemini web/cookie mode only; requires gemini.google.com Chrome cookies).'))
221
+ .addOption(new Option('--edit-image <file>', 'Edit existing image (use with --output, Gemini web/cookie mode only).'))
222
+ .addOption(new Option('--output <file>', 'Output file path for image operations (Gemini web/cookie mode only).'))
223
+ .addOption(new Option('--aspect <ratio>', 'Aspect ratio for image generation: 16:9, 1:1, 4:3, 3:4 (Gemini web/cookie mode only).'))
224
+ .addOption(new Option('--gemini-show-thoughts', 'Display Gemini thinking process (Gemini web/cookie mode only).').default(false))
225
+ .option('--retain-hours <hours>', 'Prune stored sessions older than this many hours before running (set 0 to disable).', parseFloatOption)
226
+ .option('--force', 'Force start a new session even if an identical prompt is already running.', false)
227
+ .option('--debug-help', 'Show the advanced/debug option set and exit.', false)
228
+ .option('--heartbeat <seconds>', 'Emit periodic in-progress updates (0 to disable).', parseHeartbeatOption, 30)
229
+ .addOption(new Option('--wait').default(undefined))
230
+ .addOption(new Option('--no-wait').default(undefined).hideHelp())
231
+ .showHelpAfterError('(use --help for usage)');
232
+ program.addHelpText('after', `
233
+ Examples:
234
+ # Quick API run with two files
235
+ oracle --prompt "Summarize the risk register" --file docs/risk-register.md docs/risk-matrix.md
236
+
237
+ # Browser run (no API key) + globbed TypeScript sources, excluding tests
238
+ oracle --engine browser --prompt "Review the TS data layer" \\
239
+ --file "src/**/*.ts" --file "!src/**/*.test.ts"
240
+
241
+ # Build, print, and copy a markdown bundle (semi-manual)
242
+ oracle --render --copy -p "Review the TS data layer" --file "src/**/*.ts" --file "!src/**/*.test.ts"
243
+ `);
244
+ program
245
+ .command('serve')
246
+ .description('Run Oracle browser automation as a remote service for other machines.')
247
+ .option('--host <address>', 'Interface to bind (default 0.0.0.0).')
248
+ .option('--port <number>', 'Port to listen on (default random).', parseIntOption)
249
+ .option('--token <value>', 'Access token clients must provide (random if omitted).')
250
+ .option('--manual-login', 'Use a dedicated Chrome profile for manual login (recommended when cookie sync is unavailable).', false)
251
+ .option('--manual-login-profile-dir <path>', 'Chrome profile directory for manual login (default ~/.oracle/browser-profile).')
252
+ .action(async (commandOptions) => {
253
+ const { serveRemote } = await import('../src/remote/server.js');
254
+ await serveRemote({
255
+ host: commandOptions.host,
256
+ port: commandOptions.port,
257
+ token: commandOptions.token,
258
+ manualLoginDefault: commandOptions.manualLogin,
259
+ manualLoginProfileDir: commandOptions.manualLoginProfileDir,
260
+ });
261
+ });
262
+ const bridgeCommand = program.command('bridge').description('Bridge a Windows-hosted ChatGPT session to Linux clients.');
263
+ bridgeCommand
264
+ .command('host')
265
+ .description('Start a secure oracle serve host (optionally with an SSH reverse tunnel).')
266
+ .option('--bind <host:port>', 'Local bind address for the host service (default 127.0.0.1:9473).')
267
+ .option('--token <token|auto>', 'Service access token (default auto).', 'auto')
268
+ .option('--write-connection <path>', 'Write a connection artifact JSON (default ~/.oracle/bridge-connection.json).')
269
+ .option('--ssh <user@host>', 'Maintain an SSH reverse tunnel to the Linux host (ssh -N -R ...).')
270
+ .option('--ssh-remote-port <port>', 'Remote port to bind on the Linux host (default matches --bind port).', parseIntOption)
271
+ .option('--ssh-identity <path>', 'SSH identity file (ssh -i).')
272
+ .option('--ssh-extra-args <args>', 'Extra args passed to ssh (quoted string).')
273
+ .option('--background', 'Run the host in the background and write pid/log files.', false)
274
+ .option('--foreground', 'Run the host in the foreground (default).', false)
275
+ .option('--print', 'Print the client connection string (includes token).', false)
276
+ .option('--print-token', 'Print only the token.', false)
277
+ .action(async (commandOptions) => {
278
+ const { runBridgeHost } = await import('../src/cli/bridge/host.js');
279
+ await runBridgeHost(commandOptions);
280
+ });
281
+ bridgeCommand
282
+ .command('client')
283
+ .description('Configure this machine to use a remote oracle serve host.')
284
+ .requiredOption('--connect <connection>', 'Connection string or path to bridge-connection.json.')
285
+ .option('--config <path>', 'Override the oracle config file location (default ~/.oracle/config.json).')
286
+ .option('--no-write-config', 'Do not write ~/.oracle/config.json (just validate).')
287
+ .option('--no-test', 'Skip remote /health check.')
288
+ .option('--print-env', 'Print env var exports (includes token).', false)
289
+ .action(async (commandOptions) => {
290
+ const { runBridgeClient } = await import('../src/cli/bridge/client.js');
291
+ await runBridgeClient(commandOptions);
292
+ });
293
+ bridgeCommand
294
+ .command('doctor')
295
+ .description('Diagnose bridge connectivity and browser engine prerequisites.')
296
+ .option('--verbose', 'Show extra diagnostics.', false)
297
+ .action(async (commandOptions) => {
298
+ const { runBridgeDoctor } = await import('../src/cli/bridge/doctor.js');
299
+ await runBridgeDoctor(commandOptions);
300
+ });
301
+ bridgeCommand
302
+ .command('codex-config')
303
+ .description('Print a Codex CLI MCP server config snippet for oracle-mcp.')
304
+ .option('--print-token', 'Include ORACLE_REMOTE_TOKEN in the snippet.', false)
305
+ .action(async (commandOptions) => {
306
+ const { runBridgeCodexConfig } = await import('../src/cli/bridge/codexConfig.js');
307
+ await runBridgeCodexConfig(commandOptions);
308
+ });
309
+ bridgeCommand
310
+ .command('claude-config')
311
+ .description('Print a Claude Code MCP config snippet (.mcp.json) for oracle-mcp.')
312
+ .option('--print-token', 'Include ORACLE_REMOTE_TOKEN in the snippet.', false)
313
+ .action(async (commandOptions) => {
314
+ const { runBridgeClaudeConfig } = await import('../src/cli/bridge/claudeConfig.js');
315
+ await runBridgeClaudeConfig(commandOptions);
316
+ });
317
+ program
318
+ .command('tui')
319
+ .description('Launch the interactive terminal UI for humans (no automation).')
320
+ .action(async () => {
321
+ await sessionStore.ensureStorage();
322
+ await launchTui({ version: VERSION, printIntro: false });
323
+ });
324
+ const sessionCommand = program
325
+ .command('session [id]')
326
+ .description('Attach to a stored session or list recent sessions when no ID is provided.')
327
+ .option('--hours <hours>', 'Look back this many hours when listing sessions (default 24).', parseFloatOption, 24)
328
+ .option('--limit <count>', 'Maximum sessions to show when listing (max 1000).', parseIntOption, 100)
329
+ .option('--all', 'Include all stored sessions regardless of age.', false)
330
+ .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
331
+ .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
332
+ .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
333
+ .option('--render-markdown', 'Alias for --render.', false)
334
+ .option('--model <name>', 'Filter sessions/output for a specific model.', '')
335
+ .option('--path', 'Print the stored session paths instead of attaching.', false)
336
+ .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
337
+ .action(async (sessionId, _options, cmd) => {
338
+ await handleSessionCommand(sessionId, cmd);
339
+ });
340
+ const statusCommand = program
341
+ .command('status [id]')
342
+ .description('List recent sessions (24h window by default) or attach to a session when an ID is provided.')
343
+ .option('--hours <hours>', 'Look back this many hours (default 24).', parseFloatOption, 24)
344
+ .option('--limit <count>', 'Maximum sessions to show (max 1000).', parseIntOption, 100)
345
+ .option('--all', 'Include all stored sessions regardless of age.', false)
346
+ .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
347
+ .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
348
+ .option('--render-markdown', 'Alias for --render.', false)
349
+ .option('--model <name>', 'Filter sessions/output for a specific model.', '')
350
+ .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
351
+ .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
352
+ .action(async (sessionId, _options, command) => {
353
+ const statusOptions = command.opts();
354
+ const clearRequested = Boolean(statusOptions.clear || statusOptions.clean);
355
+ if (clearRequested) {
356
+ if (sessionId) {
357
+ console.error('Cannot combine a session ID with --clear. Remove the ID to delete cached sessions.');
358
+ process.exitCode = 1;
359
+ return;
360
+ }
361
+ const hours = statusOptions.hours;
362
+ const includeAll = statusOptions.all;
363
+ const result = await sessionStore.deleteOlderThan({ hours, includeAll });
364
+ const scope = includeAll ? 'all stored sessions' : `sessions older than ${hours}h`;
365
+ console.log(formatSessionCleanupMessage(result, scope));
366
+ return;
367
+ }
368
+ if (sessionId === 'clear' || sessionId === 'clean') {
369
+ console.error('Session cleanup now uses --clear. Run "oracle status --clear --hours <n>" instead.');
370
+ process.exitCode = 1;
371
+ return;
372
+ }
373
+ if (sessionId) {
374
+ const autoRender = !command.getOptionValueSource?.('render') && !command.getOptionValueSource?.('renderMarkdown')
375
+ ? process.stdout.isTTY
376
+ : false;
377
+ const renderMarkdown = Boolean(statusOptions.render || statusOptions.renderMarkdown || autoRender);
378
+ await attachSession(sessionId, { renderMarkdown, renderPrompt: !statusOptions.hidePrompt });
379
+ return;
380
+ }
381
+ const showExamples = usesDefaultStatusFilters(command);
382
+ await showStatus({
383
+ hours: statusOptions.all ? Infinity : statusOptions.hours,
384
+ includeAll: statusOptions.all,
385
+ limit: statusOptions.limit,
386
+ showExamples,
387
+ });
388
+ });
389
+ program
390
+ .command('restart <id>')
391
+ .description('Re-run a stored session as a new session (clones options).')
392
+ .addOption(new Option('--wait').default(undefined))
393
+ .addOption(new Option('--no-wait').default(undefined).hideHelp())
394
+ .option('--remote-host <host:port>', 'Delegate browser runs to a remote `oracle serve` instance.')
395
+ .option('--remote-token <token>', 'Access token for the remote `oracle serve` instance.')
396
+ .action(async (sessionId, _options, cmd) => {
397
+ const restartOptions = cmd.opts();
398
+ await restartSession(sessionId, restartOptions);
399
+ });
400
+ function buildRunOptions(options, overrides = {}) {
401
+ if (!options.prompt) {
402
+ throw new Error('Prompt is required.');
403
+ }
404
+ const normalizedBaseUrl = normalizeBaseUrl(overrides.baseUrl ?? options.baseUrl);
405
+ const azure = options.azureEndpoint || overrides.azure?.endpoint
406
+ ? {
407
+ endpoint: overrides.azure?.endpoint ?? options.azureEndpoint,
408
+ deployment: overrides.azure?.deployment ?? options.azureDeployment,
409
+ apiVersion: overrides.azure?.apiVersion ?? options.azureApiVersion,
410
+ }
411
+ : undefined;
412
+ return {
413
+ prompt: options.prompt,
414
+ model: options.model,
415
+ models: overrides.models ?? options.models,
416
+ effectiveModelId: overrides.effectiveModelId ?? options.effectiveModelId ?? options.model,
417
+ file: overrides.file ?? options.file ?? [],
418
+ slug: overrides.slug ?? options.slug,
419
+ filesReport: overrides.filesReport ?? options.filesReport,
420
+ maxInput: overrides.maxInput ?? options.maxInput,
421
+ maxOutput: overrides.maxOutput ?? options.maxOutput,
422
+ system: overrides.system ?? options.system,
423
+ timeoutSeconds: overrides.timeoutSeconds ?? options.timeout,
424
+ httpTimeoutMs: overrides.httpTimeoutMs ?? options.httpTimeout,
425
+ zombieTimeoutMs: overrides.zombieTimeoutMs ?? options.zombieTimeout,
426
+ zombieUseLastActivity: overrides.zombieUseLastActivity ?? options.zombieLastActivity,
427
+ silent: overrides.silent ?? options.silent,
428
+ search: overrides.search ?? options.search,
429
+ preview: overrides.preview ?? undefined,
430
+ previewMode: overrides.previewMode ?? options.previewMode,
431
+ apiKey: overrides.apiKey ?? options.apiKey,
432
+ baseUrl: normalizedBaseUrl,
433
+ azure,
434
+ sessionId: overrides.sessionId ?? options.sessionId,
435
+ verbose: overrides.verbose ?? options.verbose,
436
+ heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
437
+ browserAttachments: overrides.browserAttachments ?? options.browserAttachments ?? 'auto',
438
+ browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
439
+ browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
440
+ background: overrides.background ?? undefined,
441
+ renderPlain: overrides.renderPlain ?? options.renderPlain ?? false,
442
+ writeOutputPath: overrides.writeOutputPath ?? options.writeOutputPath,
443
+ };
444
+ }
445
+ export function enforceBrowserSearchFlag(runOptions, sessionMode, logFn = console.log) {
446
+ if (sessionMode === 'browser' && runOptions.search === false) {
447
+ logFn(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
448
+ runOptions.search = undefined;
449
+ }
450
+ }
451
+ function resolveHeartbeatIntervalMs(seconds) {
452
+ if (typeof seconds !== 'number' || seconds <= 0) {
453
+ return undefined;
454
+ }
455
+ return Math.round(seconds * 1000);
456
+ }
457
+ function buildRunOptionsFromMetadata(metadata) {
458
+ const stored = metadata.options ?? {};
459
+ return {
460
+ prompt: stored.prompt ?? '',
461
+ model: stored.model ?? DEFAULT_MODEL,
462
+ models: stored.models,
463
+ effectiveModelId: stored.effectiveModelId ?? stored.model,
464
+ file: stored.file ?? [],
465
+ slug: stored.slug,
466
+ filesReport: stored.filesReport,
467
+ maxInput: stored.maxInput,
468
+ maxOutput: stored.maxOutput,
469
+ system: stored.system,
470
+ silent: stored.silent,
471
+ search: stored.search,
472
+ preview: false,
473
+ previewMode: undefined,
474
+ apiKey: undefined,
475
+ baseUrl: normalizeBaseUrl(stored.baseUrl),
476
+ azure: stored.azure,
477
+ timeoutSeconds: stored.timeoutSeconds,
478
+ httpTimeoutMs: stored.httpTimeoutMs,
479
+ zombieTimeoutMs: stored.zombieTimeoutMs,
480
+ zombieUseLastActivity: stored.zombieUseLastActivity,
481
+ sessionId: metadata.id,
482
+ verbose: stored.verbose,
483
+ heartbeatIntervalMs: stored.heartbeatIntervalMs,
484
+ browserAttachments: stored.browserAttachments,
485
+ browserInlineFiles: stored.browserInlineFiles,
486
+ browserBundleFiles: stored.browserBundleFiles,
487
+ background: stored.background,
488
+ renderPlain: stored.renderPlain,
489
+ writeOutputPath: stored.writeOutputPath,
490
+ };
491
+ }
492
+ function getSessionMode(metadata) {
493
+ return metadata.mode ?? metadata.options?.mode ?? 'api';
494
+ }
495
+ function getBrowserConfigFromMetadata(metadata) {
496
+ return metadata.options?.browserConfig ?? metadata.browser?.config;
497
+ }
498
+ async function runRootCommand(options) {
499
+ if (process.env.ORACLE_FORCE_TUI === '1') {
500
+ await sessionStore.ensureStorage();
501
+ await launchTui({ version: VERSION, printIntro: false });
502
+ return;
503
+ }
504
+ const userConfig = (await loadUserConfig()).config;
505
+ const helpRequested = rawCliArgs.some((arg) => arg === '--help' || arg === '-h');
506
+ const multiModelProvided = Array.isArray(options.models) && options.models.length > 0;
507
+ if (multiModelProvided) {
508
+ const modelFromConfigOrCli = normalizeModelOption(options.model ?? userConfig.model ?? '');
509
+ if (modelFromConfigOrCli) {
510
+ throw new Error('--models cannot be combined with --model.');
511
+ }
512
+ }
513
+ const optionUsesDefault = (name) => {
514
+ // Commander reports undefined for untouched options, so treat undefined/default the same
515
+ const source = program.getOptionValueSource?.(name);
516
+ return source == null || source === 'default';
517
+ };
518
+ if (helpRequested) {
519
+ if (options.verbose) {
520
+ console.log('');
521
+ printDebugHelp(program.name());
522
+ console.log('');
523
+ }
524
+ program.help({ error: false });
525
+ return;
526
+ }
527
+ const previewMode = resolvePreviewMode(options.dryRun || options.preview);
528
+ const mergedFileInputs = mergePathLikeOptions(options.file, options.include, options.files, options.path, options.paths);
529
+ if (mergedFileInputs.length > 0) {
530
+ const { deduped, duplicates } = dedupePathInputs(mergedFileInputs, { cwd: process.cwd() });
531
+ if (duplicates.length > 0) {
532
+ const preview = duplicates.slice(0, 8).join(', ');
533
+ const suffix = duplicates.length > 8 ? ` (+${duplicates.length - 8} more)` : '';
534
+ console.log(chalk.dim(`Ignoring duplicate --file inputs: ${preview}${suffix}`));
535
+ }
536
+ options.file = deduped;
537
+ }
538
+ const copyMarkdown = options.copyMarkdown || options.copy;
539
+ const renderMarkdown = resolveRenderFlag(options.render, options.renderMarkdown);
540
+ const renderPlain = resolveRenderPlain(options.renderPlain, options.render, options.renderMarkdown);
541
+ const applyRetentionOption = () => {
542
+ if (optionUsesDefault('retainHours') && typeof userConfig.sessionRetentionHours === 'number') {
543
+ options.retainHours = userConfig.sessionRetentionHours;
544
+ }
545
+ const envRetention = process.env.ORACLE_RETAIN_HOURS;
546
+ if (optionUsesDefault('retainHours') && envRetention) {
547
+ const parsed = Number.parseFloat(envRetention);
548
+ if (!Number.isNaN(parsed)) {
549
+ options.retainHours = parsed;
550
+ }
551
+ }
552
+ };
553
+ applyRetentionOption();
554
+ const remoteConfig = resolveRemoteServiceConfig({
555
+ cliHost: options.remoteHost,
556
+ cliToken: options.remoteToken,
557
+ userConfig,
558
+ env: process.env,
559
+ });
560
+ const remoteHost = remoteConfig.host;
561
+ const remoteToken = remoteConfig.token;
562
+ if (remoteHost) {
563
+ console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
564
+ }
565
+ if (userCliArgs.length === 0) {
566
+ console.log(chalk.yellow('No prompt or subcommand supplied. Run `oracle --help` or `oracle tui` for the TUI.'));
567
+ program.outputHelp();
568
+ return;
569
+ }
570
+ const retentionHours = typeof options.retainHours === 'number' ? options.retainHours : undefined;
571
+ await sessionStore.ensureStorage();
572
+ await pruneOldSessions(retentionHours, (message) => console.log(chalk.dim(message)));
573
+ if (options.debugHelp) {
574
+ printDebugHelp(program.name());
575
+ return;
576
+ }
577
+ if (options.dryRun && options.renderMarkdown) {
578
+ throw new Error('--dry-run cannot be combined with --render-markdown.');
579
+ }
580
+ const preferredEngine = options.engine ?? userConfig.engine;
581
+ let engine = resolveEngine({ engine: preferredEngine, browserFlag: options.browser, env: process.env });
582
+ if (options.browser) {
583
+ console.log(chalk.yellow('`--browser` is deprecated; use `--engine browser` instead.'));
584
+ }
585
+ if (optionUsesDefault('model') && userConfig.model) {
586
+ options.model = userConfig.model;
587
+ }
588
+ if (optionUsesDefault('search') && userConfig.search) {
589
+ options.search = userConfig.search === 'on';
590
+ }
591
+ if (optionUsesDefault('filesReport') && userConfig.filesReport != null) {
592
+ options.filesReport = Boolean(userConfig.filesReport);
593
+ }
594
+ if (optionUsesDefault('heartbeat') && typeof userConfig.heartbeatSeconds === 'number') {
595
+ options.heartbeat = userConfig.heartbeatSeconds;
596
+ }
597
+ if (optionUsesDefault('baseUrl') && userConfig.apiBaseUrl) {
598
+ options.baseUrl = userConfig.apiBaseUrl;
599
+ }
600
+ if (remoteHost && engine !== 'browser') {
601
+ throw new Error('--remote-host requires --engine browser.');
602
+ }
603
+ if (remoteHost && options.remoteChrome) {
604
+ throw new Error('--remote-host cannot be combined with --remote-chrome.');
605
+ }
606
+ if (optionUsesDefault('azureEndpoint')) {
607
+ if (process.env.AZURE_OPENAI_ENDPOINT) {
608
+ options.azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
609
+ }
610
+ else if (userConfig.azure?.endpoint) {
611
+ options.azureEndpoint = userConfig.azure.endpoint;
612
+ }
613
+ }
614
+ if (optionUsesDefault('azureDeployment')) {
615
+ if (process.env.AZURE_OPENAI_DEPLOYMENT) {
616
+ options.azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT;
617
+ }
618
+ else if (userConfig.azure?.deployment) {
619
+ options.azureDeployment = userConfig.azure.deployment;
620
+ }
621
+ }
622
+ if (optionUsesDefault('azureApiVersion')) {
623
+ if (process.env.AZURE_OPENAI_API_VERSION) {
624
+ options.azureApiVersion = process.env.AZURE_OPENAI_API_VERSION;
625
+ }
626
+ else if (userConfig.azure?.apiVersion) {
627
+ options.azureApiVersion = userConfig.azure.apiVersion;
628
+ }
629
+ }
630
+ const normalizedMultiModels = multiModelProvided
631
+ ? Array.from(new Set(options.models.map((entry) => resolveApiModel(entry))))
632
+ : [];
633
+ const cliModelArg = normalizeModelOption(options.model) || (multiModelProvided ? '' : DEFAULT_MODEL);
634
+ const resolvedModelCandidate = multiModelProvided
635
+ ? normalizedMultiModels[0]
636
+ : engine === 'browser'
637
+ ? inferModelFromLabel(cliModelArg || DEFAULT_MODEL)
638
+ : resolveApiModel(cliModelArg || DEFAULT_MODEL);
639
+ const primaryModelCandidate = normalizedMultiModels[0] ?? resolvedModelCandidate;
640
+ const isGemini = primaryModelCandidate.startsWith('gemini');
641
+ const isCodex = primaryModelCandidate.startsWith('gpt-5.1-codex');
642
+ const isClaude = primaryModelCandidate.startsWith('claude');
643
+ const userForcedBrowser = options.browser || options.engine === 'browser';
644
+ const isBrowserCompatible = (model) => model.startsWith('gpt-') || model.startsWith('gemini');
645
+ const hasNonBrowserCompatibleTarget = (engine === 'browser' || userForcedBrowser) &&
646
+ (normalizedMultiModels.length > 0
647
+ ? normalizedMultiModels.some((model) => !isBrowserCompatible(model))
648
+ : !isBrowserCompatible(resolvedModelCandidate));
649
+ if (hasNonBrowserCompatibleTarget) {
650
+ throw new Error('Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.');
651
+ }
652
+ if (isClaude && engine === 'browser') {
653
+ console.log(chalk.dim('Browser engine is not supported for Claude models; switching to API.'));
654
+ engine = 'api';
655
+ }
656
+ if (isCodex && engine === 'browser') {
657
+ console.log(chalk.dim('Browser engine is not supported for gpt-5.1-codex; switching to API.'));
658
+ engine = 'api';
659
+ }
660
+ if (normalizedMultiModels.length > 0) {
661
+ engine = 'api';
662
+ }
663
+ if (remoteHost && normalizedMultiModels.length > 0) {
664
+ throw new Error('--remote-host does not support --models yet. Use API engine locally instead.');
665
+ }
666
+ const resolvedModel = normalizedMultiModels[0] ?? (isGemini ? resolveApiModel(cliModelArg) : resolvedModelCandidate);
667
+ const effectiveModelId = resolvedModel.startsWith('gemini')
668
+ ? resolveGeminiModelId(resolvedModel)
669
+ : isKnownModel(resolvedModel)
670
+ ? MODEL_CONFIGS[resolvedModel].apiModel ?? resolvedModel
671
+ : resolvedModel;
672
+ const resolvedBaseUrl = normalizeBaseUrl(options.baseUrl ?? (isClaude ? process.env.ANTHROPIC_BASE_URL : process.env.OPENAI_BASE_URL));
673
+ const { models: _rawModels, ...optionsWithoutModels } = options;
674
+ const resolvedOptions = { ...optionsWithoutModels, model: resolvedModel };
675
+ if (normalizedMultiModels.length > 0) {
676
+ resolvedOptions.models = normalizedMultiModels;
677
+ }
678
+ resolvedOptions.baseUrl = resolvedBaseUrl;
679
+ resolvedOptions.effectiveModelId = effectiveModelId;
680
+ resolvedOptions.writeOutputPath = resolveOutputPath(options.writeOutput, process.cwd());
681
+ // Decide whether to block until completion:
682
+ // - explicit --wait / --no-wait wins
683
+ // - otherwise block for fast models (gpt-5.1, browser) and detach by default for pro API runs
684
+ let waitPreference = resolveWaitFlag({
685
+ waitFlag: options.wait,
686
+ model: resolvedModel,
687
+ engine,
688
+ });
689
+ if (remoteHost && waitPreference === false) {
690
+ console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
691
+ waitPreference = true;
692
+ }
693
+ if (await handleStatusFlag(options, { attachSession, showStatus })) {
694
+ return;
695
+ }
696
+ if (await handleSessionAlias(options, { attachSession })) {
697
+ return;
698
+ }
699
+ if (options.execSession) {
700
+ await executeSession(options.execSession);
701
+ return;
702
+ }
703
+ if (renderMarkdown || copyMarkdown) {
704
+ if (!options.prompt) {
705
+ throw new Error('Prompt is required when using --render-markdown or --copy-markdown.');
706
+ }
707
+ const bundle = await buildMarkdownBundle({ prompt: options.prompt, file: options.file, system: options.system }, { cwd: process.cwd() });
708
+ const modelConfig = isKnownModel(resolvedModel) ? MODEL_CONFIGS[resolvedModel] : MODEL_CONFIGS['gpt-5.1'];
709
+ const requestBody = buildRequestBody({
710
+ modelConfig,
711
+ systemPrompt: bundle.systemPrompt,
712
+ userPrompt: bundle.promptWithFiles,
713
+ searchEnabled: options.search !== false,
714
+ background: false,
715
+ storeResponse: false,
716
+ });
717
+ const estimatedTokens = estimateRequestTokens(requestBody, modelConfig);
718
+ const warnThreshold = Math.min(196_000, modelConfig.inputLimit ?? 196_000);
719
+ warnIfOversizeBundle(estimatedTokens, warnThreshold, console.log);
720
+ if (renderMarkdown) {
721
+ const output = renderPlain
722
+ ? bundle.markdown
723
+ : await formatRenderedMarkdown(bundle.markdown, { richTty: isTty });
724
+ // Trim trailing newlines from the rendered bundle so we print exactly one blank before the summary line.
725
+ console.log(output.replace(/\n+$/u, ''));
726
+ }
727
+ if (copyMarkdown) {
728
+ const result = await copyToClipboard(bundle.markdown);
729
+ if (result.success) {
730
+ const filesPart = bundle.files.length > 0 ? `; ${bundle.files.length} files` : '';
731
+ const summary = `Copied markdown to clipboard (~${formatCompactNumber(estimatedTokens)} tokens${filesPart}).`;
732
+ console.log(chalk.green(summary));
733
+ }
734
+ else {
735
+ const reason = result.error instanceof Error ? result.error.message : String(result.error ?? 'unknown error');
736
+ console.log(chalk.dim(`Copy failed (${reason}); markdown not printed. Re-run with --render-markdown if you need the content.`));
737
+ }
738
+ }
739
+ return;
740
+ }
741
+ if (previewMode) {
742
+ if (!options.prompt) {
743
+ throw new Error('Prompt is required when using --dry-run/preview.');
744
+ }
745
+ if (userConfig.promptSuffix) {
746
+ options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
747
+ }
748
+ resolvedOptions.prompt = options.prompt;
749
+ const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode, baseUrl: resolvedBaseUrl });
750
+ if (engine === 'browser') {
751
+ await runBrowserPreview({
752
+ runOptions,
753
+ cwd: process.cwd(),
754
+ version: VERSION,
755
+ previewMode,
756
+ log: console.log,
757
+ }, {});
758
+ return;
759
+ }
760
+ // API dry-run/preview path
761
+ if (previewMode === 'summary') {
762
+ await runDryRunSummary({
763
+ engine,
764
+ runOptions,
765
+ cwd: process.cwd(),
766
+ version: VERSION,
767
+ log: console.log,
768
+ }, {});
769
+ return;
770
+ }
771
+ await runDryRunSummary({
772
+ engine,
773
+ runOptions,
774
+ cwd: process.cwd(),
775
+ version: VERSION,
776
+ log: console.log,
777
+ }, {});
778
+ return;
779
+ }
780
+ if (!options.prompt) {
781
+ throw new Error('Prompt is required when starting a new session.');
782
+ }
783
+ if (userConfig.promptSuffix) {
784
+ options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
785
+ }
786
+ resolvedOptions.prompt = options.prompt;
787
+ const duplicateBlocked = await shouldBlockDuplicatePrompt({
788
+ prompt: resolvedOptions.prompt,
789
+ force: options.force,
790
+ sessionStore,
791
+ log: console.log,
792
+ });
793
+ if (duplicateBlocked) {
794
+ process.exitCode = 1;
795
+ return;
796
+ }
797
+ if (options.file && options.file.length > 0) {
798
+ const isBrowserMode = engine === 'browser' || userForcedBrowser;
799
+ const filesToValidate = isBrowserMode ? options.file.filter((f) => !isMediaFile(f)) : options.file;
800
+ if (filesToValidate.length > 0) {
801
+ await readFiles(filesToValidate, { cwd: process.cwd() });
802
+ }
803
+ }
804
+ const getSource = (key) => program.getOptionValueSource?.(key) ?? undefined;
805
+ applyBrowserDefaultsFromConfig(options, userConfig, getSource);
806
+ const notifications = resolveNotificationSettings({
807
+ cliNotify: options.notify,
808
+ cliNotifySound: options.notifySound,
809
+ env: process.env,
810
+ config: userConfig.notify,
811
+ });
812
+ const sessionMode = engine === 'browser' ? 'browser' : 'api';
813
+ const browserModelLabelOverride = sessionMode === 'browser' ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
814
+ const browserConfig = sessionMode === 'browser'
815
+ ? await buildBrowserConfig({
816
+ ...options,
817
+ model: resolvedModel,
818
+ browserModelLabel: browserModelLabelOverride,
819
+ })
820
+ : undefined;
821
+ let browserDeps;
822
+ if (browserConfig && remoteHost) {
823
+ browserDeps = {
824
+ executeBrowser: createRemoteBrowserExecutor({ host: remoteHost, token: remoteToken }),
825
+ };
826
+ console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
827
+ }
828
+ else if (browserConfig && resolvedModel.startsWith('gemini')) {
829
+ browserDeps = {
830
+ executeBrowser: createGeminiWebExecutor({
831
+ youtube: options.youtube,
832
+ generateImage: options.generateImage,
833
+ editImage: options.editImage,
834
+ outputPath: options.output,
835
+ aspectRatio: options.aspect,
836
+ showThoughts: options.geminiShowThoughts,
837
+ }),
838
+ };
839
+ console.log(chalk.dim('Using Gemini web client for browser automation'));
840
+ if (browserConfig.modelStrategy && browserConfig.modelStrategy !== 'select') {
841
+ console.log(chalk.dim('Browser model strategy is ignored for Gemini web runs.'));
842
+ }
843
+ }
844
+ const remoteExecutionActive = Boolean(browserDeps);
845
+ if (options.dryRun) {
846
+ const baseRunOptions = buildRunOptions(resolvedOptions, {
847
+ preview: false,
848
+ previewMode: undefined,
849
+ baseUrl: resolvedBaseUrl,
850
+ });
851
+ await runDryRunSummary({
852
+ engine,
853
+ runOptions: baseRunOptions,
854
+ cwd: process.cwd(),
855
+ version: VERSION,
856
+ log: console.log,
857
+ browserConfig,
858
+ }, {});
859
+ return;
860
+ }
861
+ await sessionStore.ensureStorage();
862
+ const baseRunOptions = buildRunOptions(resolvedOptions, {
863
+ preview: false,
864
+ previewMode: undefined,
865
+ background: resolvedOptions.background ?? userConfig.background,
866
+ baseUrl: resolvedBaseUrl,
867
+ });
868
+ enforceBrowserSearchFlag(baseRunOptions, sessionMode, console.log);
869
+ if (sessionMode === 'browser' && baseRunOptions.search === false) {
870
+ console.log(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
871
+ baseRunOptions.search = undefined;
872
+ }
873
+ const sessionMeta = await sessionStore.createSession({
874
+ ...baseRunOptions,
875
+ mode: sessionMode,
876
+ browserConfig,
877
+ waitPreference,
878
+ youtube: options.youtube,
879
+ generateImage: options.generateImage,
880
+ editImage: options.editImage,
881
+ outputPath: options.output,
882
+ aspectRatio: options.aspect,
883
+ geminiShowThoughts: options.geminiShowThoughts,
884
+ }, process.cwd(), notifications);
885
+ const liveRunOptions = {
886
+ ...baseRunOptions,
887
+ sessionId: sessionMeta.id,
888
+ effectiveModelId,
889
+ };
890
+ const disableDetachEnv = process.env.ORACLE_NO_DETACH === '1';
891
+ const detachAllowed = remoteExecutionActive
892
+ ? false
893
+ : shouldDetachSession({
894
+ engine,
895
+ model: resolvedModel,
896
+ waitPreference,
897
+ disableDetachEnv,
898
+ });
899
+ const detached = !detachAllowed
900
+ ? false
901
+ : await launchDetachedSession(sessionMeta.id).catch((error) => {
902
+ const message = error instanceof Error ? error.message : String(error);
903
+ console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
904
+ return false;
905
+ });
906
+ if (!waitPreference) {
907
+ if (!detached) {
908
+ console.log(chalk.red('Unable to start in background; use --wait to run inline.'));
909
+ process.exitCode = 1;
910
+ return;
911
+ }
912
+ console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
913
+ console.log(chalk.dim('Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached.'));
914
+ return;
915
+ }
916
+ if (detached === false) {
917
+ await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, false, notifications, userConfig, true, browserDeps);
918
+ return;
919
+ }
920
+ if (detached) {
921
+ console.log(chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`));
922
+ await attachSession(sessionMeta.id, { suppressMetadata: true });
923
+ }
924
+ }
925
+ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig, suppressSummary = false, browserDeps, cwd = process.cwd()) {
926
+ const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionMeta.id);
927
+ let headerAugmented = false;
928
+ const combinedLog = (message = '') => {
929
+ if (!headerAugmented && message.startsWith('oracle (')) {
930
+ headerAugmented = true;
931
+ if (showReattachHint) {
932
+ console.log(`${message}\n${chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`)}`);
933
+ }
934
+ else {
935
+ console.log(message);
936
+ }
937
+ logLine(message);
938
+ return;
939
+ }
940
+ console.log(message);
941
+ logLine(message);
942
+ };
943
+ const combinedWrite = (chunk) => {
944
+ // runOracle handles stdout; keep this write hook for session logs only to avoid double-printing
945
+ writeChunk(chunk);
946
+ return true;
947
+ };
948
+ try {
949
+ await performSessionRun({
950
+ sessionMeta,
951
+ runOptions,
952
+ mode,
953
+ browserConfig,
954
+ cwd,
955
+ log: combinedLog,
956
+ write: combinedWrite,
957
+ version: VERSION,
958
+ notifications: notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env, userConfig?.notify),
959
+ browserDeps,
960
+ });
961
+ const latest = await sessionStore.readSession(sessionMeta.id);
962
+ if (!suppressSummary) {
963
+ const summary = latest ? formatCompletionSummary(latest, { includeSlug: true }) : null;
964
+ if (summary) {
965
+ console.log('\n' + chalk.green.bold(summary));
966
+ logLine(summary); // plain text in log, colored on stdout
967
+ }
968
+ }
969
+ }
970
+ catch (error) {
971
+ throw error;
972
+ }
973
+ finally {
974
+ stream.end();
975
+ }
976
+ }
977
+ async function launchDetachedSession(sessionId) {
978
+ return new Promise((resolve, reject) => {
979
+ try {
980
+ const args = ['--', CLI_ENTRYPOINT, '--exec-session', sessionId];
981
+ const child = spawn(process.execPath, args, {
982
+ detached: true,
983
+ stdio: 'ignore',
984
+ env: process.env,
985
+ });
986
+ child.once('error', reject);
987
+ child.once('spawn', () => {
988
+ child.unref();
989
+ resolve(true);
990
+ });
991
+ }
992
+ catch (error) {
993
+ reject(error);
994
+ }
995
+ });
996
+ }
997
+ async function restartSession(sessionId, options) {
998
+ const metadata = await sessionStore.readSession(sessionId);
999
+ if (!metadata) {
1000
+ console.error(chalk.red(`No session found with ID ${sessionId}`));
1001
+ process.exitCode = 1;
1002
+ return;
1003
+ }
1004
+ const runOptions = buildRunOptionsFromMetadata(metadata);
1005
+ if (!runOptions.prompt) {
1006
+ console.error(chalk.red(`Session ${sessionId} has no stored prompt; cannot restart.`));
1007
+ process.exitCode = 1;
1008
+ return;
1009
+ }
1010
+ const sessionMode = getSessionMode(metadata);
1011
+ const engine = sessionMode === 'browser' ? 'browser' : 'api';
1012
+ const browserConfig = getBrowserConfigFromMetadata(metadata);
1013
+ if (sessionMode === 'browser' && !browserConfig) {
1014
+ console.error(chalk.red(`Session ${sessionId} is missing browser config; cannot restart.`));
1015
+ process.exitCode = 1;
1016
+ return;
1017
+ }
1018
+ const userConfig = (await loadUserConfig()).config;
1019
+ const cwd = metadata.cwd ?? process.cwd();
1020
+ const storedOptions = metadata.options ?? {};
1021
+ if (runOptions.file && runOptions.file.length > 0) {
1022
+ const isBrowserMode = engine === 'browser';
1023
+ const filesToValidate = isBrowserMode ? runOptions.file.filter((f) => !isMediaFile(f)) : runOptions.file;
1024
+ if (filesToValidate.length > 0) {
1025
+ await readFiles(filesToValidate, { cwd });
1026
+ }
1027
+ }
1028
+ enforceBrowserSearchFlag(runOptions, sessionMode, console.log);
1029
+ let waitPreference = resolveRestartWaitPreference({
1030
+ waitFlag: options.wait,
1031
+ storedPreference: storedOptions.waitPreference,
1032
+ model: runOptions.model,
1033
+ engine,
1034
+ });
1035
+ const remoteConfig = resolveRemoteServiceConfig({
1036
+ cliHost: options.remoteHost,
1037
+ cliToken: options.remoteToken,
1038
+ userConfig,
1039
+ env: process.env,
1040
+ });
1041
+ const remoteHost = remoteConfig.host;
1042
+ const remoteToken = remoteConfig.token;
1043
+ if (remoteHost && engine !== 'browser') {
1044
+ throw new Error('--remote-host requires a browser session.');
1045
+ }
1046
+ if (remoteHost) {
1047
+ console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
1048
+ }
1049
+ if (remoteHost && waitPreference === false) {
1050
+ console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
1051
+ waitPreference = true;
1052
+ }
1053
+ let browserDeps;
1054
+ if (browserConfig && remoteHost) {
1055
+ browserDeps = {
1056
+ executeBrowser: createRemoteBrowserExecutor({ host: remoteHost, token: remoteToken }),
1057
+ };
1058
+ console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
1059
+ }
1060
+ else if (browserConfig && runOptions.model.startsWith('gemini')) {
1061
+ browserDeps = {
1062
+ executeBrowser: createGeminiWebExecutor({
1063
+ youtube: storedOptions.youtube,
1064
+ generateImage: storedOptions.generateImage,
1065
+ editImage: storedOptions.editImage,
1066
+ outputPath: storedOptions.outputPath,
1067
+ aspectRatio: storedOptions.aspectRatio,
1068
+ showThoughts: storedOptions.geminiShowThoughts,
1069
+ }),
1070
+ };
1071
+ console.log(chalk.dim('Using Gemini web client for browser automation'));
1072
+ if (browserConfig.modelStrategy && browserConfig.modelStrategy !== 'select') {
1073
+ console.log(chalk.dim('Browser model strategy is ignored for Gemini web runs.'));
1074
+ }
1075
+ }
1076
+ const remoteExecutionActive = Boolean(browserDeps);
1077
+ await sessionStore.ensureStorage();
1078
+ const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
1079
+ const sessionMeta = await sessionStore.createSession({
1080
+ ...runOptions,
1081
+ mode: sessionMode,
1082
+ browserConfig,
1083
+ waitPreference,
1084
+ youtube: storedOptions.youtube,
1085
+ generateImage: storedOptions.generateImage,
1086
+ editImage: storedOptions.editImage,
1087
+ outputPath: storedOptions.outputPath,
1088
+ aspectRatio: storedOptions.aspectRatio,
1089
+ geminiShowThoughts: storedOptions.geminiShowThoughts,
1090
+ }, cwd, notifications, sessionId);
1091
+ const liveRunOptions = {
1092
+ ...runOptions,
1093
+ sessionId: sessionMeta.id,
1094
+ effectiveModelId: resolveEffectiveModelIdForRun(runOptions.model, runOptions.effectiveModelId),
1095
+ };
1096
+ const disableDetachEnv = process.env.ORACLE_NO_DETACH === '1';
1097
+ const detachAllowed = remoteExecutionActive
1098
+ ? false
1099
+ : shouldDetachSession({
1100
+ engine,
1101
+ model: runOptions.model,
1102
+ waitPreference,
1103
+ disableDetachEnv,
1104
+ });
1105
+ const detached = !detachAllowed
1106
+ ? false
1107
+ : await launchDetachedSession(sessionMeta.id).catch((error) => {
1108
+ const message = error instanceof Error ? error.message : String(error);
1109
+ console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
1110
+ return false;
1111
+ });
1112
+ if (!waitPreference) {
1113
+ if (!detached) {
1114
+ console.log(chalk.red('Unable to start in background; use --wait to run inline.'));
1115
+ process.exitCode = 1;
1116
+ return;
1117
+ }
1118
+ console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
1119
+ console.log(chalk.dim('Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached.'));
1120
+ return;
1121
+ }
1122
+ if (detached === false) {
1123
+ await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, false, notifications, userConfig, true, browserDeps, cwd);
1124
+ return;
1125
+ }
1126
+ if (detached) {
1127
+ console.log(chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`));
1128
+ await attachSession(sessionMeta.id, { suppressMetadata: true });
1129
+ }
1130
+ }
1131
+ async function executeSession(sessionId) {
1132
+ const metadata = await sessionStore.readSession(sessionId);
1133
+ if (!metadata) {
1134
+ console.error(chalk.red(`No session found with ID ${sessionId}`));
1135
+ process.exitCode = 1;
1136
+ return;
1137
+ }
1138
+ const runOptions = buildRunOptionsFromMetadata(metadata);
1139
+ const sessionMode = getSessionMode(metadata);
1140
+ const browserConfig = getBrowserConfigFromMetadata(metadata);
1141
+ const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionId);
1142
+ const userConfig = (await loadUserConfig()).config;
1143
+ const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
1144
+ try {
1145
+ await performSessionRun({
1146
+ sessionMeta: metadata,
1147
+ runOptions,
1148
+ mode: sessionMode,
1149
+ browserConfig,
1150
+ cwd: metadata.cwd ?? process.cwd(),
1151
+ log: logLine,
1152
+ write: writeChunk,
1153
+ version: VERSION,
1154
+ notifications,
1155
+ });
1156
+ }
1157
+ catch {
1158
+ // Errors are already logged to the session log; keep quiet to mirror stored-session behavior.
1159
+ }
1160
+ finally {
1161
+ stream.end();
1162
+ }
1163
+ }
1164
+ function printDebugHelp(cliName) {
1165
+ console.log(chalk.bold('Advanced Options'));
1166
+ printDebugOptionGroup([
1167
+ ['--search <on|off>', 'Enable or disable the server-side search tool (default on).'],
1168
+ ['--max-input <tokens>', 'Override the input token budget.'],
1169
+ ['--max-output <tokens>', 'Override the max output tokens (model default otherwise).'],
1170
+ ]);
1171
+ console.log('');
1172
+ console.log(chalk.bold('Browser Options'));
1173
+ printDebugOptionGroup([
1174
+ ['--chatgpt-url <url>', 'Override the ChatGPT web URL (workspace/folder targets).'],
1175
+ ['--browser-chrome-profile <name>', 'Reuse cookies from a specific Chrome profile.'],
1176
+ ['--browser-chrome-path <path>', 'Point to a custom Chrome/Chromium binary.'],
1177
+ ['--browser-cookie-path <path>', 'Use a specific Chrome/Chromium cookie store file.'],
1178
+ ['--browser-url <url>', 'Alias for --chatgpt-url.'],
1179
+ ['--browser-timeout <ms|s|m>', 'Cap total wait time for the assistant response.'],
1180
+ ['--browser-input-timeout <ms|s|m>', 'Cap how long we wait for the composer textarea.'],
1181
+ ['--browser-recheck-delay <ms|s|m|h>', 'After timeout, wait then revisit the conversation to retry capture.'],
1182
+ ['--browser-recheck-timeout <ms|s|m|h>', 'Time budget for the delayed recheck attempt.'],
1183
+ ['--browser-reuse-wait <ms|s|m|h>', 'Wait for a shared Chrome profile before launching (parallel runs).'],
1184
+ ['--browser-profile-lock-timeout <ms|s|m|h>', 'Wait for the manual-login profile lock before sending.'],
1185
+ ['--browser-auto-reattach-delay <ms|s|m|h>', 'Delay before periodic auto-reattach attempts after a timeout.'],
1186
+ ['--browser-auto-reattach-interval <ms|s|m|h>', 'Interval between auto-reattach attempts (0 disables).'],
1187
+ ['--browser-auto-reattach-timeout <ms|s|m|h>', 'Time budget for each auto-reattach attempt.'],
1188
+ ['--browser-cookie-wait <ms|s|m>', 'Wait before retrying cookie sync when Chrome cookies are empty or locked.'],
1189
+ ['--browser-no-cookie-sync', 'Skip copying cookies from your main profile.'],
1190
+ ['--browser-manual-login', 'Skip cookie copy; reuse a persistent automation profile and log in manually.'],
1191
+ ['--browser-headless', 'Launch Chrome in headless mode.'],
1192
+ ['--browser-hide-window', 'Hide the Chrome window (macOS headful only).'],
1193
+ ['--browser-keep-browser', 'Leave Chrome running after completion.'],
1194
+ ]);
1195
+ console.log('');
1196
+ console.log(chalk.dim(`Tip: run \`${cliName} --help\` to see the primary option set.`));
1197
+ }
1198
+ function printDebugOptionGroup(entries) {
1199
+ const flagWidth = Math.max(...entries.map(([flag]) => flag.length));
1200
+ entries.forEach(([flag, description]) => {
1201
+ const label = chalk.cyan(flag.padEnd(flagWidth + 2));
1202
+ console.log(` ${label}${description}`);
1203
+ });
1204
+ }
1205
+ function resolveWaitFlag({ waitFlag, model, engine, }) {
1206
+ if (waitFlag === true)
1207
+ return true;
1208
+ if (waitFlag === false)
1209
+ return false;
1210
+ return defaultWaitPreference(model, engine);
1211
+ }
1212
+ function resolveRestartWaitPreference({ waitFlag, storedPreference, model, engine, }) {
1213
+ if (waitFlag === true)
1214
+ return true;
1215
+ if (waitFlag === false)
1216
+ return false;
1217
+ if (typeof storedPreference === 'boolean')
1218
+ return storedPreference;
1219
+ return defaultWaitPreference(model, engine);
1220
+ }
1221
+ function resolveEffectiveModelIdForRun(model, stored) {
1222
+ if (stored)
1223
+ return stored;
1224
+ if (model.startsWith('gemini')) {
1225
+ return resolveGeminiModelId(model);
1226
+ }
1227
+ return isKnownModel(model) ? MODEL_CONFIGS[model].apiModel ?? model : model;
1228
+ }
1229
+ program.action(async function () {
1230
+ const options = this.optsWithGlobals();
1231
+ await runRootCommand(options);
1232
+ });
1233
+ async function main() {
1234
+ const parsePromise = program.parseAsync(normalizedArgv);
1235
+ const sigintPromise = once(process, 'SIGINT').then(() => 'sigint');
1236
+ const result = await Promise.race([parsePromise.then(() => 'parsed'), sigintPromise]);
1237
+ if (result === 'sigint') {
1238
+ console.log(chalk.yellow('\nCancelled.'));
1239
+ process.exitCode = 130;
1240
+ }
1241
+ }
1242
+ void main().catch((error) => {
1243
+ if (error instanceof Error) {
1244
+ if (!isErrorLogged(error)) {
1245
+ console.error(chalk.red('✖'), error.message);
1246
+ }
1247
+ }
1248
+ else {
1249
+ console.error(chalk.red('✖'), error);
1250
+ }
1251
+ process.exitCode = 1;
1252
+ });