@steipete/oracle 1.1.0 → 1.3.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 (69) hide show
  1. package/README.md +40 -7
  2. package/assets-oracle-icon.png +0 -0
  3. package/dist/.DS_Store +0 -0
  4. package/dist/bin/oracle-cli.js +315 -47
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/src/browser/actions/modelSelection.js +117 -29
  7. package/dist/src/browser/config.js +6 -0
  8. package/dist/src/browser/cookies.js +50 -12
  9. package/dist/src/browser/index.js +19 -5
  10. package/dist/src/browser/prompt.js +6 -5
  11. package/dist/src/browser/sessionRunner.js +14 -3
  12. package/dist/src/cli/browserConfig.js +109 -2
  13. package/dist/src/cli/detach.js +12 -0
  14. package/dist/src/cli/dryRun.js +60 -8
  15. package/dist/src/cli/engine.js +7 -0
  16. package/dist/src/cli/help.js +3 -1
  17. package/dist/src/cli/hiddenAliases.js +17 -0
  18. package/dist/src/cli/markdownRenderer.js +79 -0
  19. package/dist/src/cli/notifier.js +223 -0
  20. package/dist/src/cli/options.js +22 -0
  21. package/dist/src/cli/promptRequirement.js +3 -0
  22. package/dist/src/cli/runOptions.js +43 -0
  23. package/dist/src/cli/sessionCommand.js +1 -1
  24. package/dist/src/cli/sessionDisplay.js +94 -7
  25. package/dist/src/cli/sessionRunner.js +32 -2
  26. package/dist/src/cli/tui/index.js +457 -0
  27. package/dist/src/config.js +27 -0
  28. package/dist/src/mcp/server.js +36 -0
  29. package/dist/src/mcp/tools/consult.js +158 -0
  30. package/dist/src/mcp/tools/sessionResources.js +64 -0
  31. package/dist/src/mcp/tools/sessions.js +106 -0
  32. package/dist/src/mcp/types.js +17 -0
  33. package/dist/src/mcp/utils.js +24 -0
  34. package/dist/src/oracle/client.js +24 -6
  35. package/dist/src/oracle/config.js +10 -0
  36. package/dist/src/oracle/files.js +151 -8
  37. package/dist/src/oracle/format.js +2 -7
  38. package/dist/src/oracle/fsAdapter.js +4 -1
  39. package/dist/src/oracle/gemini.js +161 -0
  40. package/dist/src/oracle/logging.js +36 -0
  41. package/dist/src/oracle/oscProgress.js +7 -1
  42. package/dist/src/oracle/run.js +148 -64
  43. package/dist/src/oracle/tokenEstimate.js +34 -0
  44. package/dist/src/oracle.js +1 -0
  45. package/dist/src/sessionManager.js +50 -3
  46. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  47. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  48. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  49. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  50. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  51. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  52. package/dist/vendor/oracle-notifier/README.md +24 -0
  53. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  54. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  55. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  56. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  57. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  58. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  59. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
  60. package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
  61. package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
  62. package/package.json +22 -6
  63. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  64. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  65. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  66. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  67. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  68. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  69. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -2,43 +2,57 @@
2
2
  import 'dotenv/config';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { once } from 'node:events';
5
6
  import { Command, Option } from 'commander';
6
- import { resolveEngine } from '../src/cli/engine.js';
7
+ import { resolveEngine, defaultWaitPreference } from '../src/cli/engine.js';
7
8
  import { shouldRequirePrompt } from '../src/cli/promptRequirement.js';
8
9
  import chalk from 'chalk';
9
10
  import { ensureSessionStorage, initializeSession, readSessionMetadata, createSessionLogWriter, deleteSessionsOlderThan, } from '../src/sessionManager.js';
10
- import { runOracle, renderPromptMarkdown, readFiles } from '../src/oracle.js';
11
+ import { renderPromptMarkdown, readFiles } from '../src/oracle.js';
11
12
  import { CHATGPT_URL } from '../src/browserMode.js';
12
13
  import { applyHelpStyling } from '../src/cli/help.js';
13
- import { collectPaths, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, } from '../src/cli/options.js';
14
+ import { collectPaths, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, } from '../src/cli/options.js';
15
+ import { shouldDetachSession } from '../src/cli/detach.js';
16
+ import { applyHiddenAliases } from '../src/cli/hiddenAliases.js';
14
17
  import { buildBrowserConfig, resolveBrowserModelLabel } from '../src/cli/browserConfig.js';
15
18
  import { performSessionRun } from '../src/cli/sessionRunner.js';
16
- import { attachSession, showStatus } from '../src/cli/sessionDisplay.js';
19
+ import { attachSession, showStatus, formatCompletionSummary } from '../src/cli/sessionDisplay.js';
20
+ import { resolveGeminiModelId } from '../src/oracle/gemini.js';
17
21
  import { handleSessionCommand, formatSessionCleanupMessage } from '../src/cli/sessionCommand.js';
18
22
  import { isErrorLogged } from '../src/cli/errorUtils.js';
19
23
  import { handleSessionAlias, handleStatusFlag } from '../src/cli/rootAlias.js';
20
24
  import { getCliVersion } from '../src/version.js';
21
- import { runDryRunSummary } from '../src/cli/dryRun.js';
25
+ import { runDryRunSummary, runBrowserPreview } from '../src/cli/dryRun.js';
26
+ import { launchTui } from '../src/cli/tui/index.js';
27
+ import { resolveNotificationSettings, deriveNotificationSettingsFromMetadata, } from '../src/cli/notifier.js';
28
+ import { loadUserConfig } from '../src/config.js';
22
29
  const VERSION = getCliVersion();
23
30
  const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
24
31
  const rawCliArgs = process.argv.slice(2);
32
+ const userCliArgs = rawCliArgs[0] === CLI_ENTRYPOINT ? rawCliArgs.slice(1) : rawCliArgs;
25
33
  const isTty = process.stdout.isTTY;
34
+ const tuiEnabled = () => isTty && process.env.ORACLE_NO_TUI !== '1';
26
35
  const program = new Command();
27
36
  applyHelpStyling(program, VERSION, isTty);
28
37
  program.hook('preAction', (thisCommand) => {
29
38
  if (thisCommand !== program) {
30
39
  return;
31
40
  }
32
- if (rawCliArgs.some((arg) => arg === '--help' || arg === '-h')) {
41
+ if (userCliArgs.some((arg) => arg === '--help' || arg === '-h')) {
42
+ return;
43
+ }
44
+ if (userCliArgs.length === 0 && tuiEnabled()) {
45
+ // Skip prompt enforcement; runRootCommand will launch the TUI.
33
46
  return;
34
47
  }
35
48
  const opts = thisCommand.optsWithGlobals();
49
+ applyHiddenAliases(opts, (key, value) => thisCommand.setOptionValue(key, value));
36
50
  const positional = thisCommand.args?.[0];
37
51
  if (!opts.prompt && positional) {
38
52
  opts.prompt = positional;
39
53
  thisCommand.setOptionValue('prompt', positional);
40
54
  }
41
- if (shouldRequirePrompt(rawCliArgs, opts)) {
55
+ if (shouldRequirePrompt(userCliArgs, opts)) {
42
56
  console.log(chalk.yellow('Prompt is required. Provide it via --prompt "<text>" or positional [prompt].'));
43
57
  thisCommand.help({ error: false });
44
58
  process.exitCode = 1;
@@ -51,16 +65,31 @@ program
51
65
  .version(VERSION)
52
66
  .argument('[prompt]', 'Prompt text (shorthand for --prompt).')
53
67
  .option('-p, --prompt <text>', 'User prompt to send to the model.')
68
+ .addOption(new Option('--message <text>', 'Alias for --prompt.').hideHelp())
54
69
  .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, [])
70
+ .addOption(new Option('--include <paths...>', 'Alias for --file.')
71
+ .argParser(collectPaths)
72
+ .default([])
73
+ .hideHelp())
55
74
  .option('-s, --slug <words>', 'Custom session slug (3-5 words).')
56
75
  .option('-m, --model <model>', 'Model to target (gpt-5-pro | gpt-5.1, or ChatGPT labels like "5.1 Instant" for browser runs).', normalizeModelOption, 'gpt-5-pro')
57
- .addOption(new Option('-e, --engine <mode>', 'Execution engine (api | browser). If omitted, Oracle picks api when OPENAI_API_KEY is set, otherwise browser.').choices(['api', 'browser']))
76
+ .addOption(new Option('-e, --engine <mode>', 'Execution engine (api | browser). If omitted, oracle picks api when OPENAI_API_KEY is set, otherwise browser.').choices(['api', 'browser']))
58
77
  .option('--files-report', 'Show token usage per attached file (also prints automatically when files exceed the token budget).', false)
59
78
  .option('-v, --verbose', 'Enable verbose logging for all operations.', false)
60
- .option('--dry-run', 'Validate inputs and show token estimates without calling the model.', false)
61
- .addOption(new Option('--preview [mode]', 'Preview the request without calling the API (summary | json | full).')
79
+ .addOption(new Option('--[no-]notify', 'Desktop notification when a session finishes (default on unless CI/SSH).')
80
+ .default(undefined))
81
+ .addOption(new Option('--[no-]notify-sound', 'Play a notification sound on completion (default off).').default(undefined))
82
+ .addOption(new Option('--timeout <seconds|auto>', 'Overall timeout before aborting the API call (auto = 20m for gpt-5-pro, 30s otherwise).')
83
+ .argParser(parseTimeoutOption)
84
+ .default('auto'))
85
+ .addOption(new Option('--preview [mode]', '(alias) Preview the request without calling the model (summary | json | full). Deprecated: use --dry-run instead.')
86
+ .hideHelp()
62
87
  .choices(['summary', 'json', 'full'])
63
88
  .preset('summary'))
89
+ .addOption(new Option('--dry-run [mode]', 'Preview without calling the model (summary | json | full).')
90
+ .choices(['summary', 'json', 'full'])
91
+ .preset('summary')
92
+ .default(false))
64
93
  .addOption(new Option('--exec-session <id>').hideHelp())
65
94
  .addOption(new Option('--session <id>').hideHelp())
66
95
  .addOption(new Option('--status', 'Show stored sessions (alias for `oracle status`).').default(false).hideHelp())
@@ -75,20 +104,30 @@ program
75
104
  .addOption(new Option('--max-output <tokens>', 'Override the max output tokens for the selected model.')
76
105
  .argParser(parseIntOption)
77
106
  .hideHelp())
107
+ .option('--base-url <url>', 'Override the OpenAI-compatible base URL for API runs (e.g. LiteLLM proxy endpoint).')
108
+ .option('--azure-endpoint <url>', 'Azure OpenAI Endpoint (e.g. https://resource.openai.azure.com/).')
109
+ .option('--azure-deployment <name>', 'Azure OpenAI Deployment Name.')
110
+ .option('--azure-api-version <version>', 'Azure OpenAI API Version.')
78
111
  .addOption(new Option('--browser', '(deprecated) Use --engine browser instead.').default(false).hideHelp())
79
112
  .addOption(new Option('--browser-chrome-profile <name>', 'Chrome profile name/path for cookie reuse.').hideHelp())
80
113
  .addOption(new Option('--browser-chrome-path <path>', 'Explicit Chrome or Chromium executable path.').hideHelp())
81
114
  .addOption(new Option('--browser-url <url>', `Override the ChatGPT URL (default ${CHATGPT_URL}).`).hideHelp())
82
115
  .addOption(new Option('--browser-timeout <ms|s|m>', 'Maximum time to wait for an answer (default 900s).').hideHelp())
83
116
  .addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 30s).').hideHelp())
117
+ .addOption(new Option('--browser-cookie-names <names>', 'Comma-separated cookie allowlist for sync.').hideHelp())
118
+ .addOption(new Option('--browser-inline-cookies <jsonOrBase64>', 'Inline cookies payload (JSON array or base64-encoded JSON).').hideHelp())
119
+ .addOption(new Option('--browser-inline-cookies-file <path>', 'Load inline cookies from file (JSON or base64 JSON).').hideHelp())
84
120
  .addOption(new Option('--browser-no-cookie-sync', 'Skip copying cookies from Chrome.').hideHelp())
85
121
  .addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
86
122
  .addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
87
123
  .addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
88
124
  .addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
89
125
  .addOption(new Option('--browser-inline-files', 'Paste files directly into the ChatGPT composer instead of uploading attachments.').default(false))
126
+ .addOption(new Option('--browser-bundle-files', 'Bundle all attachments into a single archive before uploading.').default(false))
90
127
  .option('--debug-help', 'Show the advanced/debug option set and exit.', false)
91
128
  .option('--heartbeat <seconds>', 'Emit periodic in-progress updates (0 to disable).', parseHeartbeatOption, 30)
129
+ .addOption(new Option('--wait').default(undefined))
130
+ .addOption(new Option('--no-wait').default(undefined).hideHelp())
92
131
  .showHelpAfterError('(use --help for usage)');
93
132
  program.addHelpText('after', `
94
133
  Examples:
@@ -106,6 +145,7 @@ const sessionCommand = program
106
145
  .option('--limit <count>', 'Maximum sessions to show when listing (max 1000).', parseIntOption, 100)
107
146
  .option('--all', 'Include all stored sessions regardless of age.', false)
108
147
  .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
148
+ .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
109
149
  .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
110
150
  .option('--render-markdown', 'Alias for --render.', false)
111
151
  .option('--path', 'Print the stored session paths instead of attaching.', false)
@@ -122,6 +162,7 @@ const statusCommand = program
122
162
  .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
123
163
  .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
124
164
  .option('--render-markdown', 'Alias for --render.', false)
165
+ .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
125
166
  .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
126
167
  .action(async (sessionId, _options, command) => {
127
168
  const statusOptions = command.opts();
@@ -145,7 +186,11 @@ const statusCommand = program
145
186
  return;
146
187
  }
147
188
  if (sessionId) {
148
- await attachSession(sessionId);
189
+ const autoRender = !command.getOptionValueSource?.('render') && !command.getOptionValueSource?.('renderMarkdown')
190
+ ? process.stdout.isTTY
191
+ : false;
192
+ const renderMarkdown = Boolean(statusOptions.render || statusOptions.renderMarkdown || autoRender);
193
+ await attachSession(sessionId, { renderMarkdown, renderPrompt: !statusOptions.hidePrompt });
149
194
  return;
150
195
  }
151
196
  const showExamples = usesDefaultStatusFilters(command);
@@ -160,27 +205,46 @@ function buildRunOptions(options, overrides = {}) {
160
205
  if (!options.prompt) {
161
206
  throw new Error('Prompt is required.');
162
207
  }
208
+ const normalizedBaseUrl = normalizeBaseUrl(overrides.baseUrl ?? options.baseUrl);
209
+ const azure = options.azureEndpoint || overrides.azure?.endpoint
210
+ ? {
211
+ endpoint: overrides.azure?.endpoint ?? options.azureEndpoint,
212
+ deployment: overrides.azure?.deployment ?? options.azureDeployment,
213
+ apiVersion: overrides.azure?.apiVersion ?? options.azureApiVersion,
214
+ }
215
+ : undefined;
163
216
  return {
164
217
  prompt: options.prompt,
165
218
  model: options.model,
219
+ effectiveModelId: overrides.effectiveModelId ?? options.effectiveModelId ?? options.model,
166
220
  file: overrides.file ?? options.file ?? [],
167
221
  slug: overrides.slug ?? options.slug,
168
222
  filesReport: overrides.filesReport ?? options.filesReport,
169
223
  maxInput: overrides.maxInput ?? options.maxInput,
170
224
  maxOutput: overrides.maxOutput ?? options.maxOutput,
171
225
  system: overrides.system ?? options.system,
226
+ timeoutSeconds: overrides.timeoutSeconds ?? options.timeout,
172
227
  silent: overrides.silent ?? options.silent,
173
228
  search: overrides.search ?? options.search,
174
229
  preview: overrides.preview ?? undefined,
175
230
  previewMode: overrides.previewMode ?? options.previewMode,
176
231
  apiKey: overrides.apiKey ?? options.apiKey,
232
+ baseUrl: normalizedBaseUrl,
233
+ azure,
177
234
  sessionId: overrides.sessionId ?? options.sessionId,
178
235
  verbose: overrides.verbose ?? options.verbose,
179
236
  heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
180
237
  browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
238
+ browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
181
239
  background: overrides.background ?? undefined,
182
240
  };
183
241
  }
242
+ export function enforceBrowserSearchFlag(runOptions, sessionMode, logFn = console.log) {
243
+ if (sessionMode === 'browser' && runOptions.search === false) {
244
+ logFn(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
245
+ runOptions.search = undefined;
246
+ }
247
+ }
184
248
  function resolveHeartbeatIntervalMs(seconds) {
185
249
  if (typeof seconds !== 'number' || seconds <= 0) {
186
250
  return undefined;
@@ -192,6 +256,7 @@ function buildRunOptionsFromMetadata(metadata) {
192
256
  return {
193
257
  prompt: stored.prompt ?? '',
194
258
  model: stored.model ?? 'gpt-5-pro',
259
+ effectiveModelId: stored.effectiveModelId ?? stored.model,
195
260
  file: stored.file ?? [],
196
261
  slug: stored.slug,
197
262
  filesReport: stored.filesReport,
@@ -199,14 +264,17 @@ function buildRunOptionsFromMetadata(metadata) {
199
264
  maxOutput: stored.maxOutput,
200
265
  system: stored.system,
201
266
  silent: stored.silent,
202
- search: undefined,
267
+ search: stored.search,
203
268
  preview: false,
204
269
  previewMode: undefined,
205
270
  apiKey: undefined,
271
+ baseUrl: normalizeBaseUrl(stored.baseUrl),
272
+ azure: stored.azure,
206
273
  sessionId: metadata.id,
207
274
  verbose: stored.verbose,
208
275
  heartbeatIntervalMs: stored.heartbeatIntervalMs,
209
276
  browserInlineFiles: stored.browserInlineFiles,
277
+ browserBundleFiles: stored.browserBundleFiles,
210
278
  background: stored.background,
211
279
  };
212
280
  }
@@ -217,7 +285,18 @@ function getBrowserConfigFromMetadata(metadata) {
217
285
  return metadata.options?.browserConfig ?? metadata.browser?.config;
218
286
  }
219
287
  async function runRootCommand(options) {
288
+ if (process.env.ORACLE_FORCE_TUI === '1') {
289
+ await ensureSessionStorage();
290
+ await launchTui({ version: VERSION });
291
+ return;
292
+ }
293
+ const userConfig = (await loadUserConfig()).config;
220
294
  const helpRequested = rawCliArgs.some((arg) => arg === '--help' || arg === '-h');
295
+ const optionUsesDefault = (name) => {
296
+ // Commander reports undefined for untouched options, so treat undefined/default the same
297
+ const source = program.getOptionValueSource?.(name);
298
+ return source == null || source === 'default';
299
+ };
221
300
  if (helpRequested) {
222
301
  if (options.verbose) {
223
302
  console.log('');
@@ -227,8 +306,12 @@ async function runRootCommand(options) {
227
306
  program.help({ error: false });
228
307
  return;
229
308
  }
230
- const previewMode = resolvePreviewMode(options.preview);
231
- if (rawCliArgs.length === 0) {
309
+ const previewMode = resolvePreviewMode(options.dryRun || options.preview);
310
+ if (userCliArgs.length === 0) {
311
+ if (tuiEnabled()) {
312
+ await launchTui({ version: VERSION });
313
+ return;
314
+ }
232
315
  console.log(chalk.yellow('No prompt or subcommand supplied. See `oracle --help` for usage.'));
233
316
  program.help({ error: false });
234
317
  return;
@@ -237,19 +320,77 @@ async function runRootCommand(options) {
237
320
  printDebugHelp(program.name());
238
321
  return;
239
322
  }
240
- if (options.dryRun && previewMode) {
241
- throw new Error('--dry-run cannot be combined with --preview.');
242
- }
243
323
  if (options.dryRun && options.renderMarkdown) {
244
324
  throw new Error('--dry-run cannot be combined with --render-markdown.');
245
325
  }
246
- const engine = resolveEngine({ engine: options.engine, browserFlag: options.browser, env: process.env });
326
+ const preferredEngine = options.engine ?? userConfig.engine;
327
+ let engine = resolveEngine({ engine: preferredEngine, browserFlag: options.browser, env: process.env });
247
328
  if (options.browser) {
248
329
  console.log(chalk.yellow('`--browser` is deprecated; use `--engine browser` instead.'));
249
330
  }
331
+ if (optionUsesDefault('model') && userConfig.model) {
332
+ options.model = userConfig.model;
333
+ }
334
+ if (optionUsesDefault('search') && userConfig.search) {
335
+ options.search = userConfig.search === 'on';
336
+ }
337
+ if (optionUsesDefault('filesReport') && userConfig.filesReport != null) {
338
+ options.filesReport = Boolean(userConfig.filesReport);
339
+ }
340
+ if (optionUsesDefault('heartbeat') && typeof userConfig.heartbeatSeconds === 'number') {
341
+ options.heartbeat = userConfig.heartbeatSeconds;
342
+ }
343
+ if (optionUsesDefault('baseUrl') && userConfig.apiBaseUrl) {
344
+ options.baseUrl = userConfig.apiBaseUrl;
345
+ }
346
+ if (optionUsesDefault('azureEndpoint')) {
347
+ if (process.env.AZURE_OPENAI_ENDPOINT) {
348
+ options.azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
349
+ }
350
+ else if (userConfig.azure?.endpoint) {
351
+ options.azureEndpoint = userConfig.azure.endpoint;
352
+ }
353
+ }
354
+ if (optionUsesDefault('azureDeployment')) {
355
+ if (process.env.AZURE_OPENAI_DEPLOYMENT) {
356
+ options.azureDeployment = process.env.AZURE_OPENAI_DEPLOYMENT;
357
+ }
358
+ else if (userConfig.azure?.deployment) {
359
+ options.azureDeployment = userConfig.azure.deployment;
360
+ }
361
+ }
362
+ if (optionUsesDefault('azureApiVersion')) {
363
+ if (process.env.AZURE_OPENAI_API_VERSION) {
364
+ options.azureApiVersion = process.env.AZURE_OPENAI_API_VERSION;
365
+ }
366
+ else if (userConfig.azure?.apiVersion) {
367
+ options.azureApiVersion = userConfig.azure.apiVersion;
368
+ }
369
+ }
250
370
  const cliModelArg = normalizeModelOption(options.model) || 'gpt-5-pro';
251
- const resolvedModel = engine === 'browser' ? inferModelFromLabel(cliModelArg) : resolveApiModel(cliModelArg);
371
+ const resolvedModelCandidate = engine === 'browser' ? inferModelFromLabel(cliModelArg) : resolveApiModel(cliModelArg);
372
+ const isGemini = resolvedModelCandidate.startsWith('gemini');
373
+ const userForcedBrowser = options.browser || options.engine === 'browser';
374
+ if (isGemini && userForcedBrowser) {
375
+ throw new Error('Gemini is only supported via API. Use --engine api.');
376
+ }
377
+ if (isGemini && engine === 'browser') {
378
+ engine = 'api';
379
+ }
380
+ const resolvedModel = isGemini ? resolveApiModel(cliModelArg) : resolvedModelCandidate;
381
+ const effectiveModelId = resolvedModel.startsWith('gemini') ? resolveGeminiModelId(resolvedModel) : resolvedModel;
382
+ const resolvedBaseUrl = normalizeBaseUrl(options.baseUrl ?? process.env.OPENAI_BASE_URL);
252
383
  const resolvedOptions = { ...options, model: resolvedModel };
384
+ resolvedOptions.baseUrl = resolvedBaseUrl;
385
+ // Decide whether to block until completion:
386
+ // - explicit --wait / --no-wait wins
387
+ // - otherwise block for fast models (gpt-5.1, browser) and detach by default for gpt-5-pro API
388
+ const waitPreference = resolveWaitFlag({
389
+ waitFlag: options.wait,
390
+ noWaitFlag: options.noWait,
391
+ model: resolvedModel,
392
+ engine,
393
+ });
253
394
  if (await handleStatusFlag(options, { attachSession, showStatus })) {
254
395
  return;
255
396
  }
@@ -269,77 +410,146 @@ async function runRootCommand(options) {
269
410
  return;
270
411
  }
271
412
  if (previewMode) {
413
+ if (!options.prompt) {
414
+ throw new Error('Prompt is required when using --dry-run/preview.');
415
+ }
416
+ if (userConfig.promptSuffix) {
417
+ options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
418
+ }
419
+ resolvedOptions.prompt = options.prompt;
420
+ const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode, baseUrl: resolvedBaseUrl });
272
421
  if (engine === 'browser') {
273
- throw new Error('--engine browser cannot be combined with --preview.');
422
+ await runBrowserPreview({
423
+ runOptions,
424
+ cwd: process.cwd(),
425
+ version: VERSION,
426
+ previewMode,
427
+ log: console.log,
428
+ }, {});
429
+ return;
274
430
  }
275
- if (!options.prompt) {
276
- throw new Error('Prompt is required when using --preview.');
431
+ // API dry-run/preview path
432
+ if (previewMode === 'summary') {
433
+ await runDryRunSummary({
434
+ engine,
435
+ runOptions,
436
+ cwd: process.cwd(),
437
+ version: VERSION,
438
+ log: console.log,
439
+ }, {});
440
+ return;
277
441
  }
278
- const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode });
279
- await runOracle(runOptions, { log: console.log, write: (chunk) => process.stdout.write(chunk) });
280
- return;
281
- }
282
- if (!options.prompt) {
283
- throw new Error('Prompt is required when starting a new session.');
284
- }
285
- if (options.dryRun) {
286
- const baseRunOptions = buildRunOptions(resolvedOptions, { preview: false, previewMode: undefined });
287
442
  await runDryRunSummary({
288
443
  engine,
289
- runOptions: baseRunOptions,
444
+ runOptions,
290
445
  cwd: process.cwd(),
291
446
  version: VERSION,
292
447
  log: console.log,
293
448
  }, {});
294
449
  return;
295
450
  }
451
+ if (!options.prompt) {
452
+ throw new Error('Prompt is required when starting a new session.');
453
+ }
454
+ if (userConfig.promptSuffix) {
455
+ options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
456
+ }
457
+ resolvedOptions.prompt = options.prompt;
296
458
  if (options.file && options.file.length > 0) {
297
459
  await readFiles(options.file, { cwd: process.cwd() });
298
460
  }
461
+ applyBrowserDefaultsFromConfig(options, userConfig);
462
+ const notifications = resolveNotificationSettings({
463
+ cliNotify: options.notify,
464
+ cliNotifySound: options.notifySound,
465
+ env: process.env,
466
+ config: userConfig.notify,
467
+ });
299
468
  const sessionMode = engine === 'browser' ? 'browser' : 'api';
300
469
  const browserModelLabelOverride = sessionMode === 'browser' ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
301
470
  const browserConfig = sessionMode === 'browser'
302
- ? buildBrowserConfig({
471
+ ? await buildBrowserConfig({
303
472
  ...options,
304
473
  model: resolvedModel,
305
474
  browserModelLabel: browserModelLabelOverride,
306
475
  })
307
476
  : undefined;
477
+ if (options.dryRun) {
478
+ const baseRunOptions = buildRunOptions(resolvedOptions, {
479
+ preview: false,
480
+ previewMode: undefined,
481
+ baseUrl: resolvedBaseUrl,
482
+ });
483
+ await runDryRunSummary({
484
+ engine,
485
+ runOptions: baseRunOptions,
486
+ cwd: process.cwd(),
487
+ version: VERSION,
488
+ log: console.log,
489
+ browserConfig,
490
+ }, {});
491
+ return;
492
+ }
308
493
  await ensureSessionStorage();
309
- const baseRunOptions = buildRunOptions(resolvedOptions, { preview: false, previewMode: undefined });
494
+ const baseRunOptions = buildRunOptions(resolvedOptions, {
495
+ preview: false,
496
+ previewMode: undefined,
497
+ background: userConfig.background ?? resolvedOptions.background,
498
+ baseUrl: resolvedBaseUrl,
499
+ });
500
+ enforceBrowserSearchFlag(baseRunOptions, sessionMode, console.log);
501
+ if (sessionMode === 'browser' && baseRunOptions.search === false) {
502
+ console.log(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
503
+ baseRunOptions.search = undefined;
504
+ }
310
505
  const sessionMeta = await initializeSession({
311
506
  ...baseRunOptions,
312
507
  mode: sessionMode,
313
508
  browserConfig,
314
- }, process.cwd());
315
- const reattachCommand = `pnpm oracle session ${sessionMeta.id}`;
316
- console.log(chalk.bold(`Reattach later with: ${chalk.cyan(reattachCommand)}`));
317
- console.log('');
318
- const liveRunOptions = { ...baseRunOptions, sessionId: sessionMeta.id };
319
- const disableDetach = process.env.ORACLE_NO_DETACH === '1';
320
- const detached = disableDetach
509
+ }, process.cwd(), notifications);
510
+ const liveRunOptions = {
511
+ ...baseRunOptions,
512
+ sessionId: sessionMeta.id,
513
+ effectiveModelId,
514
+ };
515
+ const disableDetachEnv = process.env.ORACLE_NO_DETACH === '1';
516
+ const detachAllowed = shouldDetachSession({
517
+ engine,
518
+ model: resolvedModel,
519
+ waitPreference,
520
+ disableDetachEnv,
521
+ });
522
+ const detached = !detachAllowed
321
523
  ? false
322
524
  : await launchDetachedSession(sessionMeta.id).catch((error) => {
323
525
  const message = error instanceof Error ? error.message : String(error);
324
526
  console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
325
527
  return false;
326
528
  });
529
+ if (!waitPreference) {
530
+ if (!detached) {
531
+ console.log(chalk.red('Unable to start in background; use --wait to run inline.'));
532
+ process.exitCode = 1;
533
+ return;
534
+ }
535
+ console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
536
+ console.log(chalk.dim('Pro runs can take up to 10 minutes. Add --wait to stay attached.'));
537
+ return;
538
+ }
327
539
  if (detached === false) {
328
- await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, true);
329
- console.log(chalk.bold(`Session ${sessionMeta.id} completed`));
540
+ await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, false, notifications, userConfig, true);
330
541
  return;
331
542
  }
332
543
  if (detached) {
333
544
  console.log(chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`));
334
545
  await attachSession(sessionMeta.id, { suppressMetadata: true });
335
- console.log(chalk.bold(`Session ${sessionMeta.id} completed`));
336
546
  }
337
547
  }
338
- async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true) {
548
+ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig, suppressSummary = false) {
339
549
  const { logLine, writeChunk, stream } = createSessionLogWriter(sessionMeta.id);
340
550
  let headerAugmented = false;
341
551
  const combinedLog = (message = '') => {
342
- if (!headerAugmented && message.startsWith('Oracle (')) {
552
+ if (!headerAugmented && message.startsWith('oracle (')) {
343
553
  headerAugmented = true;
344
554
  if (showReattachHint) {
345
555
  console.log(`${message}\n${chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`)}`);
@@ -367,7 +577,16 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
367
577
  log: combinedLog,
368
578
  write: combinedWrite,
369
579
  version: VERSION,
580
+ notifications: notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env, userConfig?.notify),
370
581
  });
582
+ const latest = await readSessionMetadata(sessionMeta.id);
583
+ if (!suppressSummary) {
584
+ const summary = latest ? formatCompletionSummary(latest, { includeSlug: true }) : null;
585
+ if (summary) {
586
+ console.log('\n' + chalk.green.bold(summary));
587
+ logLine(summary); // plain text in log, colored on stdout
588
+ }
589
+ }
371
590
  }
372
591
  catch (error) {
373
592
  throw error;
@@ -407,6 +626,8 @@ async function executeSession(sessionId) {
407
626
  const sessionMode = getSessionMode(metadata);
408
627
  const browserConfig = getBrowserConfigFromMetadata(metadata);
409
628
  const { logLine, writeChunk, stream } = createSessionLogWriter(sessionId);
629
+ const userConfig = (await loadUserConfig()).config;
630
+ const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
410
631
  try {
411
632
  await performSessionRun({
412
633
  sessionMeta: metadata,
@@ -417,6 +638,7 @@ async function executeSession(sessionId) {
417
638
  log: logLine,
418
639
  write: writeChunk,
419
640
  version: VERSION,
641
+ notifications,
420
642
  });
421
643
  }
422
644
  catch {
@@ -456,11 +678,57 @@ function printDebugOptionGroup(entries) {
456
678
  console.log(` ${label}${description}`);
457
679
  });
458
680
  }
681
+ function resolveWaitFlag({ waitFlag, noWaitFlag, model, engine, }) {
682
+ if (waitFlag === true)
683
+ return true;
684
+ if (noWaitFlag === true)
685
+ return false;
686
+ return defaultWaitPreference(model, engine);
687
+ }
688
+ function applyBrowserDefaultsFromConfig(options, config) {
689
+ const browser = config.browser;
690
+ if (!browser)
691
+ return;
692
+ const source = (key) => program.getOptionValueSource?.(key);
693
+ if (source('browserChromeProfile') === 'default' && browser.chromeProfile !== undefined) {
694
+ options.browserChromeProfile = browser.chromeProfile ?? undefined;
695
+ }
696
+ if (source('browserChromePath') === 'default' && browser.chromePath !== undefined) {
697
+ options.browserChromePath = browser.chromePath ?? undefined;
698
+ }
699
+ if (source('browserUrl') === 'default' && browser.url !== undefined) {
700
+ options.browserUrl = browser.url;
701
+ }
702
+ if (source('browserTimeout') === 'default' && typeof browser.timeoutMs === 'number') {
703
+ options.browserTimeout = String(browser.timeoutMs);
704
+ }
705
+ if (source('browserInputTimeout') === 'default' && typeof browser.inputTimeoutMs === 'number') {
706
+ options.browserInputTimeout = String(browser.inputTimeoutMs);
707
+ }
708
+ if (source('browserHeadless') === 'default' && browser.headless !== undefined) {
709
+ options.browserHeadless = browser.headless;
710
+ }
711
+ if (source('browserHideWindow') === 'default' && browser.hideWindow !== undefined) {
712
+ options.browserHideWindow = browser.hideWindow;
713
+ }
714
+ if (source('browserKeepBrowser') === 'default' && browser.keepBrowser !== undefined) {
715
+ options.browserKeepBrowser = browser.keepBrowser;
716
+ }
717
+ }
459
718
  program.action(async function () {
460
719
  const options = this.optsWithGlobals();
461
720
  await runRootCommand(options);
462
721
  });
463
- await program.parseAsync(process.argv).catch((error) => {
722
+ async function main() {
723
+ const parsePromise = program.parseAsync(process.argv);
724
+ const sigintPromise = once(process, 'SIGINT').then(() => 'sigint');
725
+ const result = await Promise.race([parsePromise.then(() => 'parsed'), sigintPromise]);
726
+ if (result === 'sigint') {
727
+ console.log(chalk.yellow('\nCancelled.'));
728
+ process.exitCode = 130;
729
+ }
730
+ }
731
+ void main().catch((error) => {
464
732
  if (error instanceof Error) {
465
733
  if (!isErrorLogged(error)) {
466
734
  console.error(chalk.red('✖'), error.message);
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { startMcpServer } from '../src/mcp/server.js';
3
+ startMcpServer().catch((error) => {
4
+ console.error('oracle-mcp exited with an error:', error);
5
+ process.exitCode = 1;
6
+ });