@steipete/oracle 0.4.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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/assets-oracle-icon.png +0 -0
  4. package/dist/bin/oracle-cli.js +954 -0
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/bin/oracle.js +683 -0
  7. package/dist/markdansi/types/index.js +4 -0
  8. package/dist/oracle/bin/oracle-cli.js +472 -0
  9. package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
  10. package/dist/oracle/src/browser/actions/attachments.js +82 -0
  11. package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
  12. package/dist/oracle/src/browser/actions/navigation.js +75 -0
  13. package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
  14. package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
  15. package/dist/oracle/src/browser/config.js +33 -0
  16. package/dist/oracle/src/browser/constants.js +40 -0
  17. package/dist/oracle/src/browser/cookies.js +210 -0
  18. package/dist/oracle/src/browser/domDebug.js +36 -0
  19. package/dist/oracle/src/browser/index.js +331 -0
  20. package/dist/oracle/src/browser/pageActions.js +5 -0
  21. package/dist/oracle/src/browser/prompt.js +88 -0
  22. package/dist/oracle/src/browser/promptSummary.js +20 -0
  23. package/dist/oracle/src/browser/sessionRunner.js +80 -0
  24. package/dist/oracle/src/browser/types.js +1 -0
  25. package/dist/oracle/src/browser/utils.js +62 -0
  26. package/dist/oracle/src/browserMode.js +1 -0
  27. package/dist/oracle/src/cli/browserConfig.js +44 -0
  28. package/dist/oracle/src/cli/dryRun.js +59 -0
  29. package/dist/oracle/src/cli/engine.js +17 -0
  30. package/dist/oracle/src/cli/errorUtils.js +9 -0
  31. package/dist/oracle/src/cli/help.js +70 -0
  32. package/dist/oracle/src/cli/markdownRenderer.js +15 -0
  33. package/dist/oracle/src/cli/options.js +103 -0
  34. package/dist/oracle/src/cli/promptRequirement.js +14 -0
  35. package/dist/oracle/src/cli/rootAlias.js +30 -0
  36. package/dist/oracle/src/cli/sessionCommand.js +77 -0
  37. package/dist/oracle/src/cli/sessionDisplay.js +270 -0
  38. package/dist/oracle/src/cli/sessionRunner.js +94 -0
  39. package/dist/oracle/src/heartbeat.js +43 -0
  40. package/dist/oracle/src/oracle/client.js +48 -0
  41. package/dist/oracle/src/oracle/config.js +29 -0
  42. package/dist/oracle/src/oracle/errors.js +101 -0
  43. package/dist/oracle/src/oracle/files.js +220 -0
  44. package/dist/oracle/src/oracle/format.js +33 -0
  45. package/dist/oracle/src/oracle/fsAdapter.js +7 -0
  46. package/dist/oracle/src/oracle/oscProgress.js +60 -0
  47. package/dist/oracle/src/oracle/request.js +48 -0
  48. package/dist/oracle/src/oracle/run.js +444 -0
  49. package/dist/oracle/src/oracle/tokenStats.js +39 -0
  50. package/dist/oracle/src/oracle/types.js +1 -0
  51. package/dist/oracle/src/oracle.js +9 -0
  52. package/dist/oracle/src/sessionManager.js +205 -0
  53. package/dist/oracle/src/version.js +39 -0
  54. package/dist/scripts/browser-tools.js +536 -0
  55. package/dist/scripts/check.js +21 -0
  56. package/dist/scripts/chrome/browser-tools.js +295 -0
  57. package/dist/scripts/run-cli.js +14 -0
  58. package/dist/src/browser/actions/assistantResponse.js +555 -0
  59. package/dist/src/browser/actions/attachments.js +82 -0
  60. package/dist/src/browser/actions/modelSelection.js +300 -0
  61. package/dist/src/browser/actions/navigation.js +175 -0
  62. package/dist/src/browser/actions/promptComposer.js +167 -0
  63. package/dist/src/browser/actions/remoteFileTransfer.js +154 -0
  64. package/dist/src/browser/chromeCookies.js +274 -0
  65. package/dist/src/browser/chromeLifecycle.js +107 -0
  66. package/dist/src/browser/config.js +49 -0
  67. package/dist/src/browser/constants.js +42 -0
  68. package/dist/src/browser/cookies.js +130 -0
  69. package/dist/src/browser/domDebug.js +36 -0
  70. package/dist/src/browser/index.js +541 -0
  71. package/dist/src/browser/keytarShim.js +56 -0
  72. package/dist/src/browser/pageActions.js +5 -0
  73. package/dist/src/browser/policies.js +43 -0
  74. package/dist/src/browser/prompt.js +82 -0
  75. package/dist/src/browser/promptSummary.js +20 -0
  76. package/dist/src/browser/sessionRunner.js +96 -0
  77. package/dist/src/browser/types.js +1 -0
  78. package/dist/src/browser/utils.js +112 -0
  79. package/dist/src/browser/windowsCookies.js +218 -0
  80. package/dist/src/browserMode.js +1 -0
  81. package/dist/src/cli/browserConfig.js +193 -0
  82. package/dist/src/cli/bundleWarnings.js +9 -0
  83. package/dist/src/cli/clipboard.js +10 -0
  84. package/dist/src/cli/detach.js +11 -0
  85. package/dist/src/cli/dryRun.js +103 -0
  86. package/dist/src/cli/duplicatePromptGuard.js +14 -0
  87. package/dist/src/cli/engine.js +25 -0
  88. package/dist/src/cli/errorUtils.js +9 -0
  89. package/dist/src/cli/format.js +13 -0
  90. package/dist/src/cli/help.js +77 -0
  91. package/dist/src/cli/hiddenAliases.js +22 -0
  92. package/dist/src/cli/markdownBundle.js +17 -0
  93. package/dist/src/cli/markdownRenderer.js +97 -0
  94. package/dist/src/cli/notifier.js +300 -0
  95. package/dist/src/cli/options.js +193 -0
  96. package/dist/src/cli/oscUtils.js +20 -0
  97. package/dist/src/cli/promptRequirement.js +17 -0
  98. package/dist/src/cli/renderFlags.js +9 -0
  99. package/dist/src/cli/renderOutput.js +26 -0
  100. package/dist/src/cli/rootAlias.js +30 -0
  101. package/dist/src/cli/runOptions.js +62 -0
  102. package/dist/src/cli/sessionCommand.js +111 -0
  103. package/dist/src/cli/sessionDisplay.js +540 -0
  104. package/dist/src/cli/sessionRunner.js +419 -0
  105. package/dist/src/cli/tagline.js +258 -0
  106. package/dist/src/cli/tui/index.js +520 -0
  107. package/dist/src/cli/writeOutputPath.js +21 -0
  108. package/dist/src/config.js +27 -0
  109. package/dist/src/heartbeat.js +43 -0
  110. package/dist/src/mcp/server.js +36 -0
  111. package/dist/src/mcp/tools/consult.js +221 -0
  112. package/dist/src/mcp/tools/sessionResources.js +75 -0
  113. package/dist/src/mcp/tools/sessions.js +96 -0
  114. package/dist/src/mcp/types.js +18 -0
  115. package/dist/src/mcp/utils.js +27 -0
  116. package/dist/src/oracle/background.js +134 -0
  117. package/dist/src/oracle/claude.js +95 -0
  118. package/dist/src/oracle/client.js +87 -0
  119. package/dist/src/oracle/config.js +92 -0
  120. package/dist/src/oracle/errors.js +104 -0
  121. package/dist/src/oracle/files.js +371 -0
  122. package/dist/src/oracle/format.js +30 -0
  123. package/dist/src/oracle/fsAdapter.js +10 -0
  124. package/dist/src/oracle/gemini.js +185 -0
  125. package/dist/src/oracle/logging.js +36 -0
  126. package/dist/src/oracle/markdown.js +46 -0
  127. package/dist/src/oracle/multiModelRunner.js +164 -0
  128. package/dist/src/oracle/oscProgress.js +66 -0
  129. package/dist/src/oracle/promptAssembly.js +13 -0
  130. package/dist/src/oracle/request.js +49 -0
  131. package/dist/src/oracle/run.js +492 -0
  132. package/dist/src/oracle/runUtils.js +27 -0
  133. package/dist/src/oracle/tokenEstimate.js +37 -0
  134. package/dist/src/oracle/tokenStats.js +39 -0
  135. package/dist/src/oracle/tokenStringifier.js +24 -0
  136. package/dist/src/oracle/types.js +1 -0
  137. package/dist/src/oracle.js +12 -0
  138. package/dist/src/remote/client.js +128 -0
  139. package/dist/src/remote/server.js +294 -0
  140. package/dist/src/remote/types.js +1 -0
  141. package/dist/src/sessionManager.js +462 -0
  142. package/dist/src/sessionStore.js +56 -0
  143. package/dist/src/version.js +39 -0
  144. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  145. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  146. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  147. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  148. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  149. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  150. package/dist/vendor/oracle-notifier/README.md +24 -0
  151. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  152. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  153. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  154. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  155. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  156. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  157. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
  158. package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
  159. package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
  160. package/package.json +102 -0
  161. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  162. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  163. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  164. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  165. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  166. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  167. package/vendor/oracle-notifier/README.md +24 -0
  168. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,954 @@
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
+ import { resolveEngine, defaultWaitPreference } from '../src/cli/engine.js';
8
+ import { shouldRequirePrompt } from '../src/cli/promptRequirement.js';
9
+ import chalk from 'chalk';
10
+ import { sessionStore, pruneOldSessions } from '../src/sessionStore.js';
11
+ import { DEFAULT_MODEL, MODEL_CONFIGS, readFiles, estimateRequestTokens, buildRequestBody } from '../src/oracle.js';
12
+ import { CHATGPT_URL, normalizeChatgptUrl } from '../src/browserMode.js';
13
+ import { createRemoteBrowserExecutor } from '../src/remote/client.js';
14
+ import { applyHelpStyling } from '../src/cli/help.js';
15
+ import { collectPaths, collectModelList, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, mergePathLikeOptions, } from '../src/cli/options.js';
16
+ import { copyToClipboard } from '../src/cli/clipboard.js';
17
+ import { buildMarkdownBundle } from '../src/cli/markdownBundle.js';
18
+ import { shouldDetachSession } from '../src/cli/detach.js';
19
+ import { applyHiddenAliases } from '../src/cli/hiddenAliases.js';
20
+ import { buildBrowserConfig, resolveBrowserModelLabel } from '../src/cli/browserConfig.js';
21
+ import { performSessionRun } from '../src/cli/sessionRunner.js';
22
+ import { attachSession, showStatus, formatCompletionSummary } from '../src/cli/sessionDisplay.js';
23
+ import { formatCompactNumber } from '../src/cli/format.js';
24
+ import { formatIntroLine } from '../src/cli/tagline.js';
25
+ import { warnIfOversizeBundle } from '../src/cli/bundleWarnings.js';
26
+ import { formatRenderedMarkdown } from '../src/cli/renderOutput.js';
27
+ import { resolveRenderFlag, resolveRenderPlain } from '../src/cli/renderFlags.js';
28
+ import { resolveGeminiModelId } from '../src/oracle/gemini.js';
29
+ import { handleSessionCommand, formatSessionCleanupMessage } from '../src/cli/sessionCommand.js';
30
+ import { isErrorLogged } from '../src/cli/errorUtils.js';
31
+ import { handleSessionAlias, handleStatusFlag } from '../src/cli/rootAlias.js';
32
+ import { resolveOutputPath } from '../src/cli/writeOutputPath.js';
33
+ import { getCliVersion } from '../src/version.js';
34
+ import { runDryRunSummary, runBrowserPreview } from '../src/cli/dryRun.js';
35
+ import { launchTui } from '../src/cli/tui/index.js';
36
+ import { resolveNotificationSettings, deriveNotificationSettingsFromMetadata, } from '../src/cli/notifier.js';
37
+ import { loadUserConfig } from '../src/config.js';
38
+ import { shouldBlockDuplicatePrompt } from '../src/cli/duplicatePromptGuard.js';
39
+ const VERSION = getCliVersion();
40
+ const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
41
+ const rawCliArgs = process.argv.slice(2);
42
+ const userCliArgs = rawCliArgs[0] === CLI_ENTRYPOINT ? rawCliArgs.slice(1) : rawCliArgs;
43
+ const isTty = process.stdout.isTTY;
44
+ const tuiEnabled = () => isTty && process.env.ORACLE_NO_TUI !== '1';
45
+ const program = new Command();
46
+ let introPrinted = false;
47
+ program.hook('preAction', () => {
48
+ if (introPrinted)
49
+ return;
50
+ console.log(formatIntroLine(VERSION, { env: process.env, richTty: isTty }));
51
+ introPrinted = true;
52
+ });
53
+ applyHelpStyling(program, VERSION, isTty);
54
+ program.hook('preAction', (thisCommand) => {
55
+ if (thisCommand !== program) {
56
+ return;
57
+ }
58
+ if (userCliArgs.some((arg) => arg === '--help' || arg === '-h')) {
59
+ return;
60
+ }
61
+ if (userCliArgs.length === 0 && tuiEnabled()) {
62
+ // Skip prompt enforcement; runRootCommand will launch the TUI.
63
+ return;
64
+ }
65
+ const opts = thisCommand.optsWithGlobals();
66
+ applyHiddenAliases(opts, (key, value) => thisCommand.setOptionValue(key, value));
67
+ const positional = thisCommand.args?.[0];
68
+ if (!opts.prompt && positional) {
69
+ opts.prompt = positional;
70
+ thisCommand.setOptionValue('prompt', positional);
71
+ }
72
+ if (shouldRequirePrompt(userCliArgs, opts)) {
73
+ console.log(chalk.yellow('Prompt is required. Provide it via --prompt "<text>" or positional [prompt].'));
74
+ thisCommand.help({ error: false });
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+ });
79
+ program
80
+ .name('oracle')
81
+ .description('One-shot GPT-5.1 Pro / GPT-5.1 / GPT-5.1 Codex tool for hard questions that benefit from large file context and server-side search.')
82
+ .version(VERSION)
83
+ .argument('[prompt]', 'Prompt text (shorthand for --prompt).')
84
+ .option('-p, --prompt <text>', 'User prompt to send to the model.')
85
+ .addOption(new Option('--message <text>', 'Alias for --prompt.').hideHelp())
86
+ .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, [])
87
+ .addOption(new Option('--include <paths...>', 'Alias for --file.')
88
+ .argParser(collectPaths)
89
+ .default([])
90
+ .hideHelp())
91
+ .addOption(new Option('--files <paths...>', 'Alias for --file.')
92
+ .argParser(collectPaths)
93
+ .default([])
94
+ .hideHelp())
95
+ .addOption(new Option('--path <paths...>', 'Alias for --file.')
96
+ .argParser(collectPaths)
97
+ .default([])
98
+ .hideHelp())
99
+ .addOption(new Option('--paths <paths...>', 'Alias for --file.')
100
+ .argParser(collectPaths)
101
+ .default([])
102
+ .hideHelp())
103
+ .addOption(new Option('--copy-markdown', 'Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.').default(false))
104
+ .addOption(new Option('--copy').hideHelp().default(false))
105
+ .option('-s, --slug <words>', 'Custom session slug (3-5 words).')
106
+ .option('-m, --model <model>', 'Model to target (gpt-5.1-pro default; also gpt-5-pro, gpt-5.1, gpt-5.1-codex API-only, gemini-3-pro, claude-4.5-sonnet, claude-4.1-opus, or ChatGPT labels like "5.1 Instant" for browser runs).', normalizeModelOption)
107
+ .addOption(new Option('--models <models>', 'Comma-separated API model list to query in parallel (e.g., "gpt-5.1-pro,gemini-3-pro").')
108
+ .argParser(collectModelList)
109
+ .default([]))
110
+ .addOption(new Option('-e, --engine <mode>', 'Execution engine (api | browser). Engine is preferred; --mode is a legacy alias. If omitted, oracle picks api when OPENAI_API_KEY is set, otherwise browser.').choices(['api', 'browser']))
111
+ .addOption(new Option('--mode <mode>', 'Alias for --engine (api | browser).').choices(['api', 'browser']).hideHelp())
112
+ .option('--files-report', 'Show token usage per attached file (also prints automatically when files exceed the token budget).', false)
113
+ .option('-v, --verbose', 'Enable verbose logging for all operations.', false)
114
+ .addOption(new Option('--[no-]notify', 'Desktop notification when a session finishes (default on unless CI/SSH).')
115
+ .default(undefined))
116
+ .addOption(new Option('--[no-]notify-sound', 'Play a notification sound on completion (default off).').default(undefined))
117
+ .addOption(new Option('--timeout <seconds|auto>', 'Overall timeout before aborting the API call (auto = 60m for gpt-5.1-pro, 120s otherwise).')
118
+ .argParser(parseTimeoutOption)
119
+ .default('auto'))
120
+ .addOption(new Option('--preview [mode]', '(alias) Preview the request without calling the model (summary | json | full). Deprecated: use --dry-run instead.')
121
+ .hideHelp()
122
+ .choices(['summary', 'json', 'full'])
123
+ .preset('summary'))
124
+ .addOption(new Option('--dry-run [mode]', 'Preview without calling the model (summary | json | full).')
125
+ .choices(['summary', 'json', 'full'])
126
+ .preset('summary')
127
+ .default(false))
128
+ .addOption(new Option('--exec-session <id>').hideHelp())
129
+ .addOption(new Option('--session <id>').hideHelp())
130
+ .addOption(new Option('--status', 'Show stored sessions (alias for `oracle status`).').default(false).hideHelp())
131
+ .option('--render-markdown', 'Print the assembled markdown bundle for prompt + files and exit; pair with --copy to put it on the clipboard.', false)
132
+ .option('--render', 'Alias for --render-markdown.', false)
133
+ .option('--render-plain', 'Render markdown without ANSI/highlighting (use plain text even in a TTY).', false)
134
+ .option('--write-output <path>', 'Write only the final assistant message to this file (overwrites; multi-model appends .<model> before the extension).')
135
+ .option('--verbose-render', 'Show render/TTY diagnostics when replaying sessions.', false)
136
+ .addOption(new Option('--search <mode>', 'Set server-side search behavior (on/off).')
137
+ .argParser(parseSearchOption)
138
+ .hideHelp())
139
+ .addOption(new Option('--max-input <tokens>', 'Override the input token budget for the selected model.')
140
+ .argParser(parseIntOption)
141
+ .hideHelp())
142
+ .addOption(new Option('--max-output <tokens>', 'Override the max output tokens for the selected model.')
143
+ .argParser(parseIntOption)
144
+ .hideHelp())
145
+ .option('--base-url <url>', 'Override the OpenAI-compatible base URL for API runs (e.g. LiteLLM proxy endpoint).')
146
+ .option('--azure-endpoint <url>', 'Azure OpenAI Endpoint (e.g. https://resource.openai.azure.com/).')
147
+ .option('--azure-deployment <name>', 'Azure OpenAI Deployment Name.')
148
+ .option('--azure-api-version <version>', 'Azure OpenAI API Version.')
149
+ .addOption(new Option('--browser', '(deprecated) Use --engine browser instead.').default(false).hideHelp())
150
+ .addOption(new Option('--browser-chrome-profile <name>', 'Chrome profile name/path for cookie reuse.').hideHelp())
151
+ .addOption(new Option('--browser-chrome-path <path>', 'Explicit Chrome or Chromium executable path.').hideHelp())
152
+ .addOption(new Option('--browser-cookie-path <path>', 'Explicit Chrome/Chromium cookie DB path for session reuse.'))
153
+ .addOption(new Option('--chatgpt-url <url>', `Override the ChatGPT web URL (e.g., workspace/folder like https://chatgpt.com/g/.../project; default ${CHATGPT_URL}).`))
154
+ .addOption(new Option('--browser-url <url>', `Alias for --chatgpt-url (default ${CHATGPT_URL}).`).hideHelp())
155
+ .addOption(new Option('--browser-timeout <ms|s|m>', 'Maximum time to wait for an answer (default 1200s / 20m).').hideHelp())
156
+ .addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 30s).').hideHelp())
157
+ .addOption(new Option('--browser-cookie-names <names>', 'Comma-separated cookie allowlist for sync.').hideHelp())
158
+ .addOption(new Option('--browser-inline-cookies <jsonOrBase64>', 'Inline cookies payload (JSON array or base64-encoded JSON).').hideHelp())
159
+ .addOption(new Option('--browser-inline-cookies-file <path>', 'Load inline cookies from file (JSON or base64 JSON).').hideHelp())
160
+ .addOption(new Option('--browser-no-cookie-sync', 'Skip copying cookies from Chrome.').hideHelp())
161
+ .addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
162
+ .addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
163
+ .addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
164
+ .addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
165
+ .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).'))
166
+ .addOption(new Option('--remote-host <host:port>', 'Delegate browser runs to a remote `oracle serve` instance.'))
167
+ .addOption(new Option('--remote-token <token>', 'Access token for the remote `oracle serve` instance.'))
168
+ .addOption(new Option('--browser-inline-files', 'Paste files directly into the ChatGPT composer instead of uploading attachments.').default(false))
169
+ .addOption(new Option('--browser-bundle-files', 'Bundle all attachments into a single archive before uploading.').default(false))
170
+ .option('--retain-hours <hours>', 'Prune stored sessions older than this many hours before running (set 0 to disable).', parseFloatOption)
171
+ .option('--force', 'Force start a new session even if an identical prompt is already running.', false)
172
+ .option('--debug-help', 'Show the advanced/debug option set and exit.', false)
173
+ .option('--heartbeat <seconds>', 'Emit periodic in-progress updates (0 to disable).', parseHeartbeatOption, 30)
174
+ .addOption(new Option('--wait').default(undefined))
175
+ .addOption(new Option('--no-wait').default(undefined).hideHelp())
176
+ .showHelpAfterError('(use --help for usage)');
177
+ program.addHelpText('after', `
178
+ Examples:
179
+ # Quick API run with two files
180
+ oracle --prompt "Summarize the risk register" --file docs/risk-register.md docs/risk-matrix.md
181
+
182
+ # Browser run (no API key) + globbed TypeScript sources, excluding tests
183
+ oracle --engine browser --prompt "Review the TS data layer" \\
184
+ --file "src/**/*.ts" --file "!src/**/*.test.ts"
185
+
186
+ # Build, print, and copy a markdown bundle (semi-manual)
187
+ oracle --render --copy -p "Review the TS data layer" --file "src/**/*.ts" --file "!src/**/*.test.ts"
188
+ `);
189
+ program
190
+ .command('serve')
191
+ .description('Run Oracle browser automation as a remote service for other machines.')
192
+ .option('--host <address>', 'Interface to bind (default 0.0.0.0).')
193
+ .option('--port <number>', 'Port to listen on (default random).', parseIntOption)
194
+ .option('--token <value>', 'Access token clients must provide (random if omitted).')
195
+ .action(async (commandOptions) => {
196
+ const { serveRemote } = await import('../src/remote/server.js');
197
+ await serveRemote({
198
+ host: commandOptions.host,
199
+ port: commandOptions.port,
200
+ token: commandOptions.token,
201
+ });
202
+ });
203
+ const sessionCommand = program
204
+ .command('session [id]')
205
+ .description('Attach to a stored session or list recent sessions when no ID is provided.')
206
+ .option('--hours <hours>', 'Look back this many hours when listing sessions (default 24).', parseFloatOption, 24)
207
+ .option('--limit <count>', 'Maximum sessions to show when listing (max 1000).', parseIntOption, 100)
208
+ .option('--all', 'Include all stored sessions regardless of age.', false)
209
+ .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
210
+ .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
211
+ .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
212
+ .option('--render-markdown', 'Alias for --render.', false)
213
+ .option('--model <name>', 'Filter sessions/output for a specific model.', '')
214
+ .option('--path', 'Print the stored session paths instead of attaching.', false)
215
+ .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
216
+ .action(async (sessionId, _options, cmd) => {
217
+ await handleSessionCommand(sessionId, cmd);
218
+ });
219
+ const statusCommand = program
220
+ .command('status [id]')
221
+ .description('List recent sessions (24h window by default) or attach to a session when an ID is provided.')
222
+ .option('--hours <hours>', 'Look back this many hours (default 24).', parseFloatOption, 24)
223
+ .option('--limit <count>', 'Maximum sessions to show (max 1000).', parseIntOption, 100)
224
+ .option('--all', 'Include all stored sessions regardless of age.', false)
225
+ .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
226
+ .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
227
+ .option('--render-markdown', 'Alias for --render.', false)
228
+ .option('--model <name>', 'Filter sessions/output for a specific model.', '')
229
+ .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
230
+ .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
231
+ .action(async (sessionId, _options, command) => {
232
+ const statusOptions = command.opts();
233
+ const clearRequested = Boolean(statusOptions.clear || statusOptions.clean);
234
+ if (clearRequested) {
235
+ if (sessionId) {
236
+ console.error('Cannot combine a session ID with --clear. Remove the ID to delete cached sessions.');
237
+ process.exitCode = 1;
238
+ return;
239
+ }
240
+ const hours = statusOptions.hours;
241
+ const includeAll = statusOptions.all;
242
+ const result = await sessionStore.deleteOlderThan({ hours, includeAll });
243
+ const scope = includeAll ? 'all stored sessions' : `sessions older than ${hours}h`;
244
+ console.log(formatSessionCleanupMessage(result, scope));
245
+ return;
246
+ }
247
+ if (sessionId === 'clear' || sessionId === 'clean') {
248
+ console.error('Session cleanup now uses --clear. Run "oracle status --clear --hours <n>" instead.');
249
+ process.exitCode = 1;
250
+ return;
251
+ }
252
+ if (sessionId) {
253
+ const autoRender = !command.getOptionValueSource?.('render') && !command.getOptionValueSource?.('renderMarkdown')
254
+ ? process.stdout.isTTY
255
+ : false;
256
+ const renderMarkdown = Boolean(statusOptions.render || statusOptions.renderMarkdown || autoRender);
257
+ await attachSession(sessionId, { renderMarkdown, renderPrompt: !statusOptions.hidePrompt });
258
+ return;
259
+ }
260
+ const showExamples = usesDefaultStatusFilters(command);
261
+ await showStatus({
262
+ hours: statusOptions.all ? Infinity : statusOptions.hours,
263
+ includeAll: statusOptions.all,
264
+ limit: statusOptions.limit,
265
+ showExamples,
266
+ });
267
+ });
268
+ function buildRunOptions(options, overrides = {}) {
269
+ if (!options.prompt) {
270
+ throw new Error('Prompt is required.');
271
+ }
272
+ const normalizedBaseUrl = normalizeBaseUrl(overrides.baseUrl ?? options.baseUrl);
273
+ const azure = options.azureEndpoint || overrides.azure?.endpoint
274
+ ? {
275
+ endpoint: overrides.azure?.endpoint ?? options.azureEndpoint,
276
+ deployment: overrides.azure?.deployment ?? options.azureDeployment,
277
+ apiVersion: overrides.azure?.apiVersion ?? options.azureApiVersion,
278
+ }
279
+ : undefined;
280
+ return {
281
+ prompt: options.prompt,
282
+ model: options.model,
283
+ models: overrides.models ?? options.models,
284
+ effectiveModelId: overrides.effectiveModelId ?? options.effectiveModelId ?? options.model,
285
+ file: overrides.file ?? options.file ?? [],
286
+ slug: overrides.slug ?? options.slug,
287
+ filesReport: overrides.filesReport ?? options.filesReport,
288
+ maxInput: overrides.maxInput ?? options.maxInput,
289
+ maxOutput: overrides.maxOutput ?? options.maxOutput,
290
+ system: overrides.system ?? options.system,
291
+ timeoutSeconds: overrides.timeoutSeconds ?? options.timeout,
292
+ silent: overrides.silent ?? options.silent,
293
+ search: overrides.search ?? options.search,
294
+ preview: overrides.preview ?? undefined,
295
+ previewMode: overrides.previewMode ?? options.previewMode,
296
+ apiKey: overrides.apiKey ?? options.apiKey,
297
+ baseUrl: normalizedBaseUrl,
298
+ azure,
299
+ sessionId: overrides.sessionId ?? options.sessionId,
300
+ verbose: overrides.verbose ?? options.verbose,
301
+ heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
302
+ browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
303
+ browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
304
+ background: overrides.background ?? undefined,
305
+ renderPlain: overrides.renderPlain ?? options.renderPlain ?? false,
306
+ writeOutputPath: overrides.writeOutputPath ?? options.writeOutputPath,
307
+ };
308
+ }
309
+ export function enforceBrowserSearchFlag(runOptions, sessionMode, logFn = console.log) {
310
+ if (sessionMode === 'browser' && runOptions.search === false) {
311
+ logFn(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
312
+ runOptions.search = undefined;
313
+ }
314
+ }
315
+ function resolveHeartbeatIntervalMs(seconds) {
316
+ if (typeof seconds !== 'number' || seconds <= 0) {
317
+ return undefined;
318
+ }
319
+ return Math.round(seconds * 1000);
320
+ }
321
+ function buildRunOptionsFromMetadata(metadata) {
322
+ const stored = metadata.options ?? {};
323
+ return {
324
+ prompt: stored.prompt ?? '',
325
+ model: stored.model ?? DEFAULT_MODEL,
326
+ models: stored.models,
327
+ effectiveModelId: stored.effectiveModelId ?? stored.model,
328
+ file: stored.file ?? [],
329
+ slug: stored.slug,
330
+ filesReport: stored.filesReport,
331
+ maxInput: stored.maxInput,
332
+ maxOutput: stored.maxOutput,
333
+ system: stored.system,
334
+ silent: stored.silent,
335
+ search: stored.search,
336
+ preview: false,
337
+ previewMode: undefined,
338
+ apiKey: undefined,
339
+ baseUrl: normalizeBaseUrl(stored.baseUrl),
340
+ azure: stored.azure,
341
+ sessionId: metadata.id,
342
+ verbose: stored.verbose,
343
+ heartbeatIntervalMs: stored.heartbeatIntervalMs,
344
+ browserInlineFiles: stored.browserInlineFiles,
345
+ browserBundleFiles: stored.browserBundleFiles,
346
+ background: stored.background,
347
+ renderPlain: stored.renderPlain,
348
+ writeOutputPath: stored.writeOutputPath,
349
+ };
350
+ }
351
+ function getSessionMode(metadata) {
352
+ return metadata.mode ?? metadata.options?.mode ?? 'api';
353
+ }
354
+ function getBrowserConfigFromMetadata(metadata) {
355
+ return metadata.options?.browserConfig ?? metadata.browser?.config;
356
+ }
357
+ async function runRootCommand(options) {
358
+ if (process.env.ORACLE_FORCE_TUI === '1') {
359
+ await sessionStore.ensureStorage();
360
+ await launchTui({ version: VERSION });
361
+ return;
362
+ }
363
+ const userConfig = (await loadUserConfig()).config;
364
+ const helpRequested = rawCliArgs.some((arg) => arg === '--help' || arg === '-h');
365
+ const multiModelProvided = Array.isArray(options.models) && options.models.length > 0;
366
+ if (multiModelProvided) {
367
+ const modelFromConfigOrCli = normalizeModelOption(options.model ?? userConfig.model ?? '');
368
+ if (modelFromConfigOrCli) {
369
+ throw new Error('--models cannot be combined with --model.');
370
+ }
371
+ }
372
+ const optionUsesDefault = (name) => {
373
+ // Commander reports undefined for untouched options, so treat undefined/default the same
374
+ const source = program.getOptionValueSource?.(name);
375
+ return source == null || source === 'default';
376
+ };
377
+ if (helpRequested) {
378
+ if (options.verbose) {
379
+ console.log('');
380
+ printDebugHelp(program.name());
381
+ console.log('');
382
+ }
383
+ program.help({ error: false });
384
+ return;
385
+ }
386
+ const previewMode = resolvePreviewMode(options.dryRun || options.preview);
387
+ const mergedFileInputs = mergePathLikeOptions(options.file, options.include, options.files, options.path, options.paths);
388
+ if (mergedFileInputs.length > 0) {
389
+ options.file = mergedFileInputs;
390
+ }
391
+ const copyMarkdown = options.copyMarkdown || options.copy;
392
+ const renderMarkdown = resolveRenderFlag(options.render, options.renderMarkdown);
393
+ const renderPlain = resolveRenderPlain(options.renderPlain, options.render, options.renderMarkdown);
394
+ const applyRetentionOption = () => {
395
+ if (optionUsesDefault('retainHours') && typeof userConfig.sessionRetentionHours === 'number') {
396
+ options.retainHours = userConfig.sessionRetentionHours;
397
+ }
398
+ const envRetention = process.env.ORACLE_RETAIN_HOURS;
399
+ if (optionUsesDefault('retainHours') && envRetention) {
400
+ const parsed = Number.parseFloat(envRetention);
401
+ if (!Number.isNaN(parsed)) {
402
+ options.retainHours = parsed;
403
+ }
404
+ }
405
+ };
406
+ applyRetentionOption();
407
+ const remoteHost = options.remoteHost ?? userConfig.remoteHost ?? userConfig.remote?.host ?? process.env.ORACLE_REMOTE_HOST;
408
+ const remoteToken = options.remoteToken ?? userConfig.remoteToken ?? userConfig.remote?.token ?? process.env.ORACLE_REMOTE_TOKEN;
409
+ if (remoteHost) {
410
+ console.log(chalk.dim(`Remote browser host detected: ${remoteHost}`));
411
+ }
412
+ if (userCliArgs.length === 0) {
413
+ if (tuiEnabled()) {
414
+ await launchTui({ version: VERSION });
415
+ return;
416
+ }
417
+ console.log(chalk.yellow('No prompt or subcommand supplied. See `oracle --help` for usage.'));
418
+ program.help({ error: false });
419
+ return;
420
+ }
421
+ const retentionHours = typeof options.retainHours === 'number' ? options.retainHours : undefined;
422
+ await sessionStore.ensureStorage();
423
+ await pruneOldSessions(retentionHours, (message) => console.log(chalk.dim(message)));
424
+ if (options.debugHelp) {
425
+ printDebugHelp(program.name());
426
+ return;
427
+ }
428
+ if (options.dryRun && options.renderMarkdown) {
429
+ throw new Error('--dry-run cannot be combined with --render-markdown.');
430
+ }
431
+ const preferredEngine = options.engine ?? userConfig.engine;
432
+ let engine = resolveEngine({ engine: preferredEngine, browserFlag: options.browser, env: process.env });
433
+ if (options.browser) {
434
+ console.log(chalk.yellow('`--browser` is deprecated; use `--engine browser` instead.'));
435
+ }
436
+ if (optionUsesDefault('model') && userConfig.model) {
437
+ options.model = userConfig.model;
438
+ }
439
+ if (optionUsesDefault('search') && userConfig.search) {
440
+ options.search = userConfig.search === 'on';
441
+ }
442
+ if (optionUsesDefault('filesReport') && userConfig.filesReport != null) {
443
+ options.filesReport = Boolean(userConfig.filesReport);
444
+ }
445
+ if (optionUsesDefault('heartbeat') && typeof userConfig.heartbeatSeconds === 'number') {
446
+ options.heartbeat = userConfig.heartbeatSeconds;
447
+ }
448
+ if (optionUsesDefault('baseUrl') && userConfig.apiBaseUrl) {
449
+ options.baseUrl = userConfig.apiBaseUrl;
450
+ }
451
+ if (remoteHost && engine !== 'browser') {
452
+ throw new Error('--remote-host requires --engine browser.');
453
+ }
454
+ if (remoteHost && options.remoteChrome) {
455
+ throw new Error('--remote-host cannot be combined with --remote-chrome.');
456
+ }
457
+ if (optionUsesDefault('azureEndpoint')) {
458
+ if (process.env.AZURE_OPENAI_ENDPOINT) {
459
+ options.azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
460
+ }
461
+ else if (userConfig.azure?.endpoint) {
462
+ options.azureEndpoint = userConfig.azure.endpoint;
463
+ }
464
+ }
465
+ if (optionUsesDefault('azureDeployment')) {
466
+ if (process.env.AZURE_OPENAI_DEPLOYMENT) {
467
+ options.azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT;
468
+ }
469
+ else if (userConfig.azure?.deployment) {
470
+ options.azureDeployment = userConfig.azure.deployment;
471
+ }
472
+ }
473
+ if (optionUsesDefault('azureApiVersion')) {
474
+ if (process.env.AZURE_OPENAI_API_VERSION) {
475
+ options.azureApiVersion = process.env.AZURE_OPENAI_API_VERSION;
476
+ }
477
+ else if (userConfig.azure?.apiVersion) {
478
+ options.azureApiVersion = userConfig.azure.apiVersion;
479
+ }
480
+ }
481
+ const normalizedMultiModels = multiModelProvided
482
+ ? Array.from(new Set(options.models.map((entry) => resolveApiModel(entry))))
483
+ : [];
484
+ const cliModelArg = normalizeModelOption(options.model) || (multiModelProvided ? '' : DEFAULT_MODEL);
485
+ const resolvedModelCandidate = multiModelProvided
486
+ ? normalizedMultiModels[0]
487
+ : engine === 'browser'
488
+ ? inferModelFromLabel(cliModelArg || DEFAULT_MODEL)
489
+ : resolveApiModel(cliModelArg || DEFAULT_MODEL);
490
+ const primaryModelCandidate = normalizedMultiModels[0] ?? resolvedModelCandidate;
491
+ const isGemini = primaryModelCandidate.startsWith('gemini');
492
+ const isCodex = primaryModelCandidate.startsWith('gpt-5.1-codex');
493
+ const isClaude = primaryModelCandidate.startsWith('claude');
494
+ const userForcedBrowser = options.browser || options.engine === 'browser';
495
+ if (isGemini && userForcedBrowser) {
496
+ throw new Error('Gemini is only supported via API. Use --engine api.');
497
+ }
498
+ if (isGemini && engine === 'browser') {
499
+ engine = 'api';
500
+ }
501
+ if (isClaude && engine === 'browser') {
502
+ console.log(chalk.dim('Browser engine is not supported for Claude models; switching to API.'));
503
+ engine = 'api';
504
+ }
505
+ if (isCodex && engine === 'browser') {
506
+ console.log(chalk.dim('Browser engine is not supported for gpt-5.1-codex; switching to API.'));
507
+ engine = 'api';
508
+ }
509
+ if (normalizedMultiModels.length > 0) {
510
+ engine = 'api';
511
+ }
512
+ if (remoteHost && normalizedMultiModels.length > 0) {
513
+ throw new Error('--remote-host does not support --models yet. Use API engine locally instead.');
514
+ }
515
+ const resolvedModel = normalizedMultiModels[0] ?? (isGemini ? resolveApiModel(cliModelArg) : resolvedModelCandidate);
516
+ const effectiveModelId = resolvedModel.startsWith('gemini')
517
+ ? resolveGeminiModelId(resolvedModel)
518
+ : MODEL_CONFIGS[resolvedModel]?.apiModel ?? resolvedModel;
519
+ const resolvedBaseUrl = normalizeBaseUrl(options.baseUrl ?? (isClaude ? process.env.ANTHROPIC_BASE_URL : process.env.OPENAI_BASE_URL));
520
+ const { models: _rawModels, ...optionsWithoutModels } = options;
521
+ const resolvedOptions = { ...optionsWithoutModels, model: resolvedModel };
522
+ if (normalizedMultiModels.length > 0) {
523
+ resolvedOptions.models = normalizedMultiModels;
524
+ }
525
+ resolvedOptions.baseUrl = resolvedBaseUrl;
526
+ resolvedOptions.effectiveModelId = effectiveModelId;
527
+ resolvedOptions.writeOutputPath = resolveOutputPath(options.writeOutput, process.cwd());
528
+ // Decide whether to block until completion:
529
+ // - explicit --wait / --no-wait wins
530
+ // - otherwise block for fast models (gpt-5.1, browser) and detach by default for pro API runs
531
+ let waitPreference = resolveWaitFlag({
532
+ waitFlag: options.wait,
533
+ noWaitFlag: options.noWait,
534
+ model: resolvedModel,
535
+ engine,
536
+ });
537
+ if (remoteHost && !waitPreference) {
538
+ console.log(chalk.dim('Remote browser runs require --wait; ignoring --no-wait.'));
539
+ waitPreference = true;
540
+ }
541
+ if (await handleStatusFlag(options, { attachSession, showStatus })) {
542
+ return;
543
+ }
544
+ if (await handleSessionAlias(options, { attachSession })) {
545
+ return;
546
+ }
547
+ if (options.execSession) {
548
+ await executeSession(options.execSession);
549
+ return;
550
+ }
551
+ if (renderMarkdown || copyMarkdown) {
552
+ if (!options.prompt) {
553
+ throw new Error('Prompt is required when using --render-markdown or --copy-markdown.');
554
+ }
555
+ const bundle = await buildMarkdownBundle({ prompt: options.prompt, file: options.file, system: options.system }, { cwd: process.cwd() });
556
+ const modelConfig = MODEL_CONFIGS[resolvedModel];
557
+ const requestBody = buildRequestBody({
558
+ modelConfig,
559
+ systemPrompt: bundle.systemPrompt,
560
+ userPrompt: bundle.promptWithFiles,
561
+ searchEnabled: options.search !== false,
562
+ background: false,
563
+ storeResponse: false,
564
+ });
565
+ const estimatedTokens = estimateRequestTokens(requestBody, modelConfig);
566
+ const warnThreshold = Math.min(196_000, modelConfig.inputLimit ?? 196_000);
567
+ warnIfOversizeBundle(estimatedTokens, warnThreshold, console.log);
568
+ if (renderMarkdown) {
569
+ const output = renderPlain
570
+ ? bundle.markdown
571
+ : await formatRenderedMarkdown(bundle.markdown, { richTty: isTty });
572
+ // Trim trailing newlines from the rendered bundle so we print exactly one blank before the summary line.
573
+ console.log(output.replace(/\n+$/u, ''));
574
+ }
575
+ if (copyMarkdown) {
576
+ const result = await copyToClipboard(bundle.markdown);
577
+ if (result.success) {
578
+ const filesPart = bundle.files.length > 0 ? `; ${bundle.files.length} files` : '';
579
+ const summary = `Copied markdown to clipboard (~${formatCompactNumber(estimatedTokens)} tokens${filesPart}).`;
580
+ console.log(chalk.green(summary));
581
+ }
582
+ else {
583
+ const reason = result.error instanceof Error ? result.error.message : String(result.error ?? 'unknown error');
584
+ console.log(chalk.dim(`Copy failed (${reason}); markdown not printed. Re-run with --render-markdown if you need the content.`));
585
+ }
586
+ }
587
+ return;
588
+ }
589
+ if (previewMode) {
590
+ if (!options.prompt) {
591
+ throw new Error('Prompt is required when using --dry-run/preview.');
592
+ }
593
+ if (userConfig.promptSuffix) {
594
+ options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
595
+ }
596
+ resolvedOptions.prompt = options.prompt;
597
+ const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode, baseUrl: resolvedBaseUrl });
598
+ if (engine === 'browser') {
599
+ await runBrowserPreview({
600
+ runOptions,
601
+ cwd: process.cwd(),
602
+ version: VERSION,
603
+ previewMode,
604
+ log: console.log,
605
+ }, {});
606
+ return;
607
+ }
608
+ // API dry-run/preview path
609
+ if (previewMode === 'summary') {
610
+ await runDryRunSummary({
611
+ engine,
612
+ runOptions,
613
+ cwd: process.cwd(),
614
+ version: VERSION,
615
+ log: console.log,
616
+ }, {});
617
+ return;
618
+ }
619
+ await runDryRunSummary({
620
+ engine,
621
+ runOptions,
622
+ cwd: process.cwd(),
623
+ version: VERSION,
624
+ log: console.log,
625
+ }, {});
626
+ return;
627
+ }
628
+ if (!options.prompt) {
629
+ throw new Error('Prompt is required when starting a new session.');
630
+ }
631
+ if (userConfig.promptSuffix) {
632
+ options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
633
+ }
634
+ resolvedOptions.prompt = options.prompt;
635
+ const duplicateBlocked = await shouldBlockDuplicatePrompt({
636
+ prompt: resolvedOptions.prompt,
637
+ force: options.force,
638
+ sessionStore,
639
+ log: console.log,
640
+ });
641
+ if (duplicateBlocked) {
642
+ process.exitCode = 1;
643
+ return;
644
+ }
645
+ if (options.file && options.file.length > 0) {
646
+ await readFiles(options.file, { cwd: process.cwd() });
647
+ }
648
+ applyBrowserDefaultsFromConfig(options, userConfig);
649
+ const notifications = resolveNotificationSettings({
650
+ cliNotify: options.notify,
651
+ cliNotifySound: options.notifySound,
652
+ env: process.env,
653
+ config: userConfig.notify,
654
+ });
655
+ const sessionMode = engine === 'browser' ? 'browser' : 'api';
656
+ const browserModelLabelOverride = sessionMode === 'browser' ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
657
+ const browserConfig = sessionMode === 'browser'
658
+ ? await buildBrowserConfig({
659
+ ...options,
660
+ model: resolvedModel,
661
+ browserModelLabel: browserModelLabelOverride,
662
+ })
663
+ : undefined;
664
+ let browserDeps;
665
+ if (browserConfig && remoteHost) {
666
+ browserDeps = {
667
+ executeBrowser: createRemoteBrowserExecutor({ host: remoteHost, token: remoteToken }),
668
+ };
669
+ console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
670
+ }
671
+ const remoteExecutionActive = Boolean(browserDeps);
672
+ if (options.dryRun) {
673
+ const baseRunOptions = buildRunOptions(resolvedOptions, {
674
+ preview: false,
675
+ previewMode: undefined,
676
+ baseUrl: resolvedBaseUrl,
677
+ });
678
+ await runDryRunSummary({
679
+ engine,
680
+ runOptions: baseRunOptions,
681
+ cwd: process.cwd(),
682
+ version: VERSION,
683
+ log: console.log,
684
+ browserConfig,
685
+ }, {});
686
+ return;
687
+ }
688
+ await sessionStore.ensureStorage();
689
+ const baseRunOptions = buildRunOptions(resolvedOptions, {
690
+ preview: false,
691
+ previewMode: undefined,
692
+ background: userConfig.background ?? resolvedOptions.background,
693
+ baseUrl: resolvedBaseUrl,
694
+ });
695
+ enforceBrowserSearchFlag(baseRunOptions, sessionMode, console.log);
696
+ if (sessionMode === 'browser' && baseRunOptions.search === false) {
697
+ console.log(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
698
+ baseRunOptions.search = undefined;
699
+ }
700
+ const sessionMeta = await sessionStore.createSession({
701
+ ...baseRunOptions,
702
+ mode: sessionMode,
703
+ browserConfig,
704
+ }, process.cwd(), notifications);
705
+ const liveRunOptions = {
706
+ ...baseRunOptions,
707
+ sessionId: sessionMeta.id,
708
+ effectiveModelId,
709
+ };
710
+ const disableDetachEnv = process.env.ORACLE_NO_DETACH === '1';
711
+ const detachAllowed = remoteExecutionActive
712
+ ? false
713
+ : shouldDetachSession({
714
+ engine,
715
+ model: resolvedModel,
716
+ waitPreference,
717
+ disableDetachEnv,
718
+ });
719
+ const detached = !detachAllowed
720
+ ? false
721
+ : await launchDetachedSession(sessionMeta.id).catch((error) => {
722
+ const message = error instanceof Error ? error.message : String(error);
723
+ console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
724
+ return false;
725
+ });
726
+ if (!waitPreference) {
727
+ if (!detached) {
728
+ console.log(chalk.red('Unable to start in background; use --wait to run inline.'));
729
+ process.exitCode = 1;
730
+ return;
731
+ }
732
+ console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
733
+ console.log(chalk.dim('Pro runs can take up to 60 minutes (usually 10-15). Add --wait to stay attached.'));
734
+ return;
735
+ }
736
+ if (detached === false) {
737
+ await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, false, notifications, userConfig, true, browserDeps);
738
+ return;
739
+ }
740
+ if (detached) {
741
+ console.log(chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`));
742
+ await attachSession(sessionMeta.id, { suppressMetadata: true });
743
+ }
744
+ }
745
+ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig, suppressSummary = false, browserDeps) {
746
+ const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionMeta.id);
747
+ let headerAugmented = false;
748
+ const combinedLog = (message = '') => {
749
+ if (!headerAugmented && message.startsWith('oracle (')) {
750
+ headerAugmented = true;
751
+ if (showReattachHint) {
752
+ console.log(`${message}\n${chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`)}`);
753
+ }
754
+ else {
755
+ console.log(message);
756
+ }
757
+ logLine(message);
758
+ return;
759
+ }
760
+ console.log(message);
761
+ logLine(message);
762
+ };
763
+ const combinedWrite = (chunk) => {
764
+ // runOracle handles stdout; keep this write hook for session logs only to avoid double-printing
765
+ writeChunk(chunk);
766
+ return true;
767
+ };
768
+ try {
769
+ await performSessionRun({
770
+ sessionMeta,
771
+ runOptions,
772
+ mode,
773
+ browserConfig,
774
+ cwd: process.cwd(),
775
+ log: combinedLog,
776
+ write: combinedWrite,
777
+ version: VERSION,
778
+ notifications: notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env, userConfig?.notify),
779
+ browserDeps,
780
+ });
781
+ const latest = await sessionStore.readSession(sessionMeta.id);
782
+ if (!suppressSummary) {
783
+ const summary = latest ? formatCompletionSummary(latest, { includeSlug: true }) : null;
784
+ if (summary) {
785
+ console.log('\n' + chalk.green.bold(summary));
786
+ logLine(summary); // plain text in log, colored on stdout
787
+ }
788
+ }
789
+ }
790
+ catch (error) {
791
+ throw error;
792
+ }
793
+ finally {
794
+ stream.end();
795
+ }
796
+ }
797
+ async function launchDetachedSession(sessionId) {
798
+ return new Promise((resolve, reject) => {
799
+ try {
800
+ const args = ['--', CLI_ENTRYPOINT, '--exec-session', sessionId];
801
+ const child = spawn(process.execPath, args, {
802
+ detached: true,
803
+ stdio: 'ignore',
804
+ env: process.env,
805
+ });
806
+ child.once('error', reject);
807
+ child.once('spawn', () => {
808
+ child.unref();
809
+ resolve(true);
810
+ });
811
+ }
812
+ catch (error) {
813
+ reject(error);
814
+ }
815
+ });
816
+ }
817
+ async function executeSession(sessionId) {
818
+ const metadata = await sessionStore.readSession(sessionId);
819
+ if (!metadata) {
820
+ console.error(chalk.red(`No session found with ID ${sessionId}`));
821
+ process.exitCode = 1;
822
+ return;
823
+ }
824
+ const runOptions = buildRunOptionsFromMetadata(metadata);
825
+ const sessionMode = getSessionMode(metadata);
826
+ const browserConfig = getBrowserConfigFromMetadata(metadata);
827
+ const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionId);
828
+ const userConfig = (await loadUserConfig()).config;
829
+ const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
830
+ try {
831
+ await performSessionRun({
832
+ sessionMeta: metadata,
833
+ runOptions,
834
+ mode: sessionMode,
835
+ browserConfig,
836
+ cwd: metadata.cwd ?? process.cwd(),
837
+ log: logLine,
838
+ write: writeChunk,
839
+ version: VERSION,
840
+ notifications,
841
+ });
842
+ }
843
+ catch {
844
+ // Errors are already logged to the session log; keep quiet to mirror stored-session behavior.
845
+ }
846
+ finally {
847
+ stream.end();
848
+ }
849
+ }
850
+ function printDebugHelp(cliName) {
851
+ console.log(chalk.bold('Advanced Options'));
852
+ printDebugOptionGroup([
853
+ ['--search <on|off>', 'Enable or disable the server-side search tool (default on).'],
854
+ ['--max-input <tokens>', 'Override the input token budget.'],
855
+ ['--max-output <tokens>', 'Override the max output tokens (model default otherwise).'],
856
+ ]);
857
+ console.log('');
858
+ console.log(chalk.bold('Browser Options'));
859
+ printDebugOptionGroup([
860
+ ['--chatgpt-url <url>', 'Override the ChatGPT web URL (workspace/folder targets).'],
861
+ ['--browser-chrome-profile <name>', 'Reuse cookies from a specific Chrome profile.'],
862
+ ['--browser-chrome-path <path>', 'Point to a custom Chrome/Chromium binary.'],
863
+ ['--browser-cookie-path <path>', 'Use a specific Chrome/Chromium cookie store file.'],
864
+ ['--browser-url <url>', 'Alias for --chatgpt-url.'],
865
+ ['--browser-timeout <ms|s|m>', 'Cap total wait time for the assistant response.'],
866
+ ['--browser-input-timeout <ms|s|m>', 'Cap how long we wait for the composer textarea.'],
867
+ ['--browser-no-cookie-sync', 'Skip copying cookies from your main profile.'],
868
+ ['--browser-headless', 'Launch Chrome in headless mode.'],
869
+ ['--browser-hide-window', 'Hide the Chrome window (macOS headful only).'],
870
+ ['--browser-keep-browser', 'Leave Chrome running after completion.'],
871
+ ]);
872
+ console.log('');
873
+ console.log(chalk.dim(`Tip: run \`${cliName} --help\` to see the primary option set.`));
874
+ }
875
+ function printDebugOptionGroup(entries) {
876
+ const flagWidth = Math.max(...entries.map(([flag]) => flag.length));
877
+ entries.forEach(([flag, description]) => {
878
+ const label = chalk.cyan(flag.padEnd(flagWidth + 2));
879
+ console.log(` ${label}${description}`);
880
+ });
881
+ }
882
+ function resolveWaitFlag({ waitFlag, noWaitFlag, model, engine, }) {
883
+ if (waitFlag === true)
884
+ return true;
885
+ if (noWaitFlag === true)
886
+ return false;
887
+ return defaultWaitPreference(model, engine);
888
+ }
889
+ function applyBrowserDefaultsFromConfig(options, config) {
890
+ const browser = config.browser;
891
+ if (!browser)
892
+ return;
893
+ const source = (key) => program.getOptionValueSource?.(key);
894
+ const configuredChatgptUrl = browser.chatgptUrl ?? browser.url;
895
+ if (source('chatgptUrl') === 'default' && configuredChatgptUrl !== undefined) {
896
+ try {
897
+ options.chatgptUrl = normalizeChatgptUrl(configuredChatgptUrl ?? '', CHATGPT_URL);
898
+ }
899
+ catch (error) {
900
+ throw error instanceof Error ? error : new Error(String(error));
901
+ }
902
+ }
903
+ if (source('browserChromeProfile') === 'default' && browser.chromeProfile !== undefined) {
904
+ options.browserChromeProfile = browser.chromeProfile ?? undefined;
905
+ }
906
+ if (source('browserChromePath') === 'default' && browser.chromePath !== undefined) {
907
+ options.browserChromePath = browser.chromePath ?? undefined;
908
+ }
909
+ if (source('browserCookiePath') === 'default' && browser.chromeCookiePath !== undefined) {
910
+ options.browserCookiePath = browser.chromeCookiePath ?? undefined;
911
+ }
912
+ if (source('browserUrl') === 'default' && browser.url !== undefined) {
913
+ options.browserUrl = browser.url;
914
+ }
915
+ if (source('browserTimeout') === 'default' && typeof browser.timeoutMs === 'number') {
916
+ options.browserTimeout = String(browser.timeoutMs);
917
+ }
918
+ if (source('browserInputTimeout') === 'default' && typeof browser.inputTimeoutMs === 'number') {
919
+ options.browserInputTimeout = String(browser.inputTimeoutMs);
920
+ }
921
+ if (source('browserHeadless') === 'default' && browser.headless !== undefined) {
922
+ options.browserHeadless = browser.headless;
923
+ }
924
+ if (source('browserHideWindow') === 'default' && browser.hideWindow !== undefined) {
925
+ options.browserHideWindow = browser.hideWindow;
926
+ }
927
+ if (source('browserKeepBrowser') === 'default' && browser.keepBrowser !== undefined) {
928
+ options.browserKeepBrowser = browser.keepBrowser;
929
+ }
930
+ }
931
+ program.action(async function () {
932
+ const options = this.optsWithGlobals();
933
+ await runRootCommand(options);
934
+ });
935
+ async function main() {
936
+ const parsePromise = program.parseAsync(process.argv);
937
+ const sigintPromise = once(process, 'SIGINT').then(() => 'sigint');
938
+ const result = await Promise.race([parsePromise.then(() => 'parsed'), sigintPromise]);
939
+ if (result === 'sigint') {
940
+ console.log(chalk.yellow('\nCancelled.'));
941
+ process.exitCode = 130;
942
+ }
943
+ }
944
+ void main().catch((error) => {
945
+ if (error instanceof Error) {
946
+ if (!isErrorLogged(error)) {
947
+ console.error(chalk.red('✖'), error.message);
948
+ }
949
+ }
950
+ else {
951
+ console.error(chalk.red('✖'), error);
952
+ }
953
+ process.exitCode = 1;
954
+ });