@steipete/oracle 1.1.0 → 1.2.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 (58) hide show
  1. package/README.md +29 -4
  2. package/assets-oracle-icon.png +0 -0
  3. package/dist/bin/oracle-cli.js +169 -18
  4. package/dist/bin/oracle-mcp.js +6 -0
  5. package/dist/src/browser/actions/modelSelection.js +117 -29
  6. package/dist/src/browser/cookies.js +1 -1
  7. package/dist/src/browser/index.js +2 -1
  8. package/dist/src/browser/prompt.js +6 -5
  9. package/dist/src/browser/sessionRunner.js +4 -2
  10. package/dist/src/cli/dryRun.js +41 -5
  11. package/dist/src/cli/engine.js +7 -0
  12. package/dist/src/cli/help.js +1 -1
  13. package/dist/src/cli/hiddenAliases.js +17 -0
  14. package/dist/src/cli/markdownRenderer.js +79 -0
  15. package/dist/src/cli/notifier.js +223 -0
  16. package/dist/src/cli/promptRequirement.js +3 -0
  17. package/dist/src/cli/runOptions.js +29 -0
  18. package/dist/src/cli/sessionCommand.js +1 -1
  19. package/dist/src/cli/sessionDisplay.js +94 -7
  20. package/dist/src/cli/sessionRunner.js +21 -2
  21. package/dist/src/cli/tui/index.js +436 -0
  22. package/dist/src/config.js +27 -0
  23. package/dist/src/mcp/server.js +36 -0
  24. package/dist/src/mcp/tools/consult.js +158 -0
  25. package/dist/src/mcp/tools/sessionResources.js +64 -0
  26. package/dist/src/mcp/tools/sessions.js +106 -0
  27. package/dist/src/mcp/types.js +17 -0
  28. package/dist/src/mcp/utils.js +24 -0
  29. package/dist/src/oracle/files.js +143 -6
  30. package/dist/src/oracle/run.js +41 -20
  31. package/dist/src/oracle/tokenEstimate.js +34 -0
  32. package/dist/src/sessionManager.js +48 -3
  33. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  34. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  35. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  36. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  37. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  38. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  39. package/dist/vendor/oracle-notifier/README.md +24 -0
  40. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  41. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  42. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  43. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  44. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  45. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  46. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
  47. package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
  48. package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
  49. package/package.json +39 -13
  50. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  51. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  52. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  53. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  54. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  55. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  56. package/vendor/oracle-notifier/README.md +24 -0
  57. package/vendor/oracle-notifier/build-notifier.sh +93 -0
  58. package/dist/.DS_Store +0 -0
package/README.md CHANGED
@@ -20,6 +20,9 @@ Oracle gives your agents a simple, reliable way to **bundle a prompt plus the ri
20
20
 
21
21
  If you omit `--engine`, Oracle prefers the API engine when `OPENAI_API_KEY` is present; otherwise it falls back to browser mode. Switch explicitly with `-e, --engine {api|browser}` when you want to override the auto choice. Everything else (prompt assembly, file handling, session logging) stays the same.
22
22
 
23
+ Note: Browser engine is considered experimental, requires an OpenAI Pro account and only works on macOS with Chrome.
24
+ Your system password is needed to copy cookies. API engine is stable and should be preferred.
25
+
23
26
  ## Quick start
24
27
 
25
28
  ```bash
@@ -43,8 +46,23 @@ oracle session <id> # replay a run locally
43
46
 
44
47
  ## How do I integrate this?
45
48
 
46
- - **One-liner in CI** — `OPENAI_API_KEY=sk-... npx -y @steipete/oracle --prompt "Smoke-check latest PR" --file src/ docs/ --preview summary` (add to your pipeline as a non-blocking report step).
47
- - **Package script** In `package.json`: `"oracle": "oracle --prompt \"Review the diff\" --file ."` then run `OPENAI_API_KEY=... pnpm oracle`.
49
+ **CLI** (direct calls; great for CI or scripted tasks)
50
+ - One-liner in CI: `OPENAI_API_KEY=sk-... npx -y @steipete/oracle --prompt "Smoke-check latest PR" --file src/ docs/ --preview summary`.
51
+ - Package script: add `"oracle": "oracle --prompt \"Review the diff\" --file ."` to `package.json`, then run `OPENAI_API_KEY=... pnpm oracle`.
52
+ - Don’t want to export the key? Inline works: `OPENAI_API_KEY=sk-... oracle -p "Quick check" --file src/`.
53
+
54
+ **MCP** (tools + resources; mix-and-match with the CLI sessions)
55
+ - Run the bundled stdio server: `pnpm mcp` (or `oracle-mcp`) after `pnpm build`. Tools: `consult`, `sessions`; resources: `oracle-session://{id}/{metadata|log|request}`. Details in [docs/mcp.md](docs/mcp.md).
56
+ - mcporter config (stdio):
57
+ ```json
58
+ {
59
+ "name": "oracle",
60
+ "type": "stdio",
61
+ "command": "npx",
62
+ "args": ["-y", "@steipete/oracle", "oracle-mcp"]
63
+ }
64
+ ```
65
+ - You can call the MCP tools against sessions created by the CLI (shared `~/.oracle/sessions`), and vice versa.
48
66
 
49
67
  ## Highlights
50
68
 
@@ -56,17 +74,22 @@ oracle session <id> # replay a run locally
56
74
  - **File safety** — Per-file token accounting and size guards; `--files-report` shows exactly what you’re sending.
57
75
  - **Readable previews** — `--preview` / `--render-markdown` let you inspect the bundle before spending.
58
76
 
77
+ ## Configuration
78
+
79
+ Put per-user defaults in `~/.oracle/config.json` (parsed as JSON5, so comments/trailing commas are fine). Example settings cover default engine/model, notifications, browser defaults, and prompt suffixes. See `docs/configuration.md` for a complete example and precedence.
80
+
59
81
  ## Flags you’ll actually use
60
82
 
61
83
  | Flag | Purpose |
62
84
  | --- | --- |
63
85
  | `-p, --prompt <text>` | Required prompt. |
64
86
  | `-f, --file <paths...>` | Attach files/dirs (supports globs and `!` excludes). |
65
- | `-e, --engine <api|browser>` | Choose API or browser automation. Omitted: API when `OPENAI_API_KEY` is set, otherwise browser. |
87
+ | `-e, --engine <api\|browser>` | Choose API or browser automation. Omitted: API when `OPENAI_API_KEY` is set, otherwise browser. |
66
88
  | `-m, --model <name>` | `gpt-5-pro` (default) or `gpt-5.1`. |
67
89
  | `--files-report` | Print per-file token usage. |
68
- | `--preview [summary|json|full]` | Inspect the request without sending. |
90
+ | `--preview [summary\|json\|full]` | Inspect the request without sending. |
69
91
  | `--render-markdown` | Print the assembled `[SYSTEM]/[USER]/[FILE]` bundle. |
92
+ | `--wait` / `--no-wait` | Block until completion. Default: `wait` for gpt-5.1/browser; `no-wait` for gpt-5-pro API (reattach later). |
70
93
  | `-v, --verbose` | Extra logging (also surfaces advanced flags with `--help`). |
71
94
 
72
95
  More knobs (`--max-input`, cookie sync controls for browser mode, etc.) live behind `oracle --help --verbose`.
@@ -78,6 +101,8 @@ Add `--render` (alias `--render-markdown`) when attaching to pretty-print the st
78
101
 
79
102
  **Recommendation:** Prefer the API engine when you have an API key (`--engine api` or just set `OPENAI_API_KEY`). The API delivers more reliable results and supports longer, uninterrupted runs than the browser engine in most cases.
80
103
 
104
+ **Wait vs no-wait:** gpt-5-pro API runs default to detaching (shows a reattach hint); add `--wait` to stay attached. gpt-5.1 and browser runs block by default. You can reattach anytime via `oracle session <id>`.
105
+
81
106
  ## Testing
82
107
 
83
108
  ```bash
Binary file
@@ -3,7 +3,7 @@ import 'dotenv/config';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { Command, Option } from 'commander';
6
- import { resolveEngine } from '../src/cli/engine.js';
6
+ import { resolveEngine, defaultWaitPreference } from '../src/cli/engine.js';
7
7
  import { shouldRequirePrompt } from '../src/cli/promptRequirement.js';
8
8
  import chalk from 'chalk';
9
9
  import { ensureSessionStorage, initializeSession, readSessionMetadata, createSessionLogWriter, deleteSessionsOlderThan, } from '../src/sessionManager.js';
@@ -11,34 +11,45 @@ import { runOracle, renderPromptMarkdown, readFiles } from '../src/oracle.js';
11
11
  import { CHATGPT_URL } from '../src/browserMode.js';
12
12
  import { applyHelpStyling } from '../src/cli/help.js';
13
13
  import { collectPaths, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, } from '../src/cli/options.js';
14
+ import { applyHiddenAliases } from '../src/cli/hiddenAliases.js';
14
15
  import { buildBrowserConfig, resolveBrowserModelLabel } from '../src/cli/browserConfig.js';
15
16
  import { performSessionRun } from '../src/cli/sessionRunner.js';
16
- import { attachSession, showStatus } from '../src/cli/sessionDisplay.js';
17
+ import { attachSession, showStatus, formatCompletionSummary } from '../src/cli/sessionDisplay.js';
17
18
  import { handleSessionCommand, formatSessionCleanupMessage } from '../src/cli/sessionCommand.js';
18
19
  import { isErrorLogged } from '../src/cli/errorUtils.js';
19
20
  import { handleSessionAlias, handleStatusFlag } from '../src/cli/rootAlias.js';
20
21
  import { getCliVersion } from '../src/version.js';
21
- import { runDryRunSummary } from '../src/cli/dryRun.js';
22
+ import { runDryRunSummary, runBrowserPreview } from '../src/cli/dryRun.js';
23
+ import { launchTui } from '../src/cli/tui/index.js';
24
+ import { resolveNotificationSettings, deriveNotificationSettingsFromMetadata, } from '../src/cli/notifier.js';
25
+ import { loadUserConfig } from '../src/config.js';
22
26
  const VERSION = getCliVersion();
23
27
  const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
24
28
  const rawCliArgs = process.argv.slice(2);
29
+ const userCliArgs = rawCliArgs[0] === CLI_ENTRYPOINT ? rawCliArgs.slice(1) : rawCliArgs;
25
30
  const isTty = process.stdout.isTTY;
31
+ const tuiEnabled = () => isTty && process.env.ORACLE_NO_TUI !== '1';
26
32
  const program = new Command();
27
33
  applyHelpStyling(program, VERSION, isTty);
28
34
  program.hook('preAction', (thisCommand) => {
29
35
  if (thisCommand !== program) {
30
36
  return;
31
37
  }
32
- if (rawCliArgs.some((arg) => arg === '--help' || arg === '-h')) {
38
+ if (userCliArgs.some((arg) => arg === '--help' || arg === '-h')) {
39
+ return;
40
+ }
41
+ if (userCliArgs.length === 0 && tuiEnabled()) {
42
+ // Skip prompt enforcement; runRootCommand will launch the TUI.
33
43
  return;
34
44
  }
35
45
  const opts = thisCommand.optsWithGlobals();
46
+ applyHiddenAliases(opts, (key, value) => thisCommand.setOptionValue(key, value));
36
47
  const positional = thisCommand.args?.[0];
37
48
  if (!opts.prompt && positional) {
38
49
  opts.prompt = positional;
39
50
  thisCommand.setOptionValue('prompt', positional);
40
51
  }
41
- if (shouldRequirePrompt(rawCliArgs, opts)) {
52
+ if (shouldRequirePrompt(userCliArgs, opts)) {
42
53
  console.log(chalk.yellow('Prompt is required. Provide it via --prompt "<text>" or positional [prompt].'));
43
54
  thisCommand.help({ error: false });
44
55
  process.exitCode = 1;
@@ -51,12 +62,20 @@ program
51
62
  .version(VERSION)
52
63
  .argument('[prompt]', 'Prompt text (shorthand for --prompt).')
53
64
  .option('-p, --prompt <text>', 'User prompt to send to the model.')
65
+ .addOption(new Option('--message <text>', 'Alias for --prompt.').hideHelp())
54
66
  .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, [])
67
+ .addOption(new Option('--include <paths...>', 'Alias for --file.')
68
+ .argParser(collectPaths)
69
+ .default([])
70
+ .hideHelp())
55
71
  .option('-s, --slug <words>', 'Custom session slug (3-5 words).')
56
72
  .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']))
73
+ .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
74
  .option('--files-report', 'Show token usage per attached file (also prints automatically when files exceed the token budget).', false)
59
75
  .option('-v, --verbose', 'Enable verbose logging for all operations.', false)
76
+ .addOption(new Option('--[no-]notify', 'Desktop notification when a session finishes (default on unless CI/SSH).')
77
+ .default(undefined))
78
+ .addOption(new Option('--[no-]notify-sound', 'Play a notification sound on completion (default off).').default(undefined))
60
79
  .option('--dry-run', 'Validate inputs and show token estimates without calling the model.', false)
61
80
  .addOption(new Option('--preview [mode]', 'Preview the request without calling the API (summary | json | full).')
62
81
  .choices(['summary', 'json', 'full'])
@@ -87,8 +106,11 @@ program
87
106
  .addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
88
107
  .addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
89
108
  .addOption(new Option('--browser-inline-files', 'Paste files directly into the ChatGPT composer instead of uploading attachments.').default(false))
109
+ .addOption(new Option('--browser-bundle-files', 'Bundle all attachments into a single archive before uploading.').default(false))
90
110
  .option('--debug-help', 'Show the advanced/debug option set and exit.', false)
91
111
  .option('--heartbeat <seconds>', 'Emit periodic in-progress updates (0 to disable).', parseHeartbeatOption, 30)
112
+ .addOption(new Option('--wait').default(undefined))
113
+ .addOption(new Option('--no-wait').default(undefined).hideHelp())
92
114
  .showHelpAfterError('(use --help for usage)');
93
115
  program.addHelpText('after', `
94
116
  Examples:
@@ -106,6 +128,7 @@ const sessionCommand = program
106
128
  .option('--limit <count>', 'Maximum sessions to show when listing (max 1000).', parseIntOption, 100)
107
129
  .option('--all', 'Include all stored sessions regardless of age.', false)
108
130
  .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
131
+ .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
109
132
  .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
110
133
  .option('--render-markdown', 'Alias for --render.', false)
111
134
  .option('--path', 'Print the stored session paths instead of attaching.', false)
@@ -122,6 +145,7 @@ const statusCommand = program
122
145
  .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
123
146
  .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
124
147
  .option('--render-markdown', 'Alias for --render.', false)
148
+ .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
125
149
  .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
126
150
  .action(async (sessionId, _options, command) => {
127
151
  const statusOptions = command.opts();
@@ -145,7 +169,11 @@ const statusCommand = program
145
169
  return;
146
170
  }
147
171
  if (sessionId) {
148
- await attachSession(sessionId);
172
+ const autoRender = !command.getOptionValueSource?.('render') && !command.getOptionValueSource?.('renderMarkdown')
173
+ ? process.stdout.isTTY
174
+ : false;
175
+ const renderMarkdown = Boolean(statusOptions.render || statusOptions.renderMarkdown || autoRender);
176
+ await attachSession(sessionId, { renderMarkdown, renderPrompt: !statusOptions.hidePrompt });
149
177
  return;
150
178
  }
151
179
  const showExamples = usesDefaultStatusFilters(command);
@@ -178,9 +206,16 @@ function buildRunOptions(options, overrides = {}) {
178
206
  verbose: overrides.verbose ?? options.verbose,
179
207
  heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
180
208
  browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
209
+ browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
181
210
  background: overrides.background ?? undefined,
182
211
  };
183
212
  }
213
+ export function enforceBrowserSearchFlag(runOptions, sessionMode, logFn = console.log) {
214
+ if (sessionMode === 'browser' && runOptions.search === false) {
215
+ logFn(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
216
+ runOptions.search = undefined;
217
+ }
218
+ }
184
219
  function resolveHeartbeatIntervalMs(seconds) {
185
220
  if (typeof seconds !== 'number' || seconds <= 0) {
186
221
  return undefined;
@@ -199,7 +234,7 @@ function buildRunOptionsFromMetadata(metadata) {
199
234
  maxOutput: stored.maxOutput,
200
235
  system: stored.system,
201
236
  silent: stored.silent,
202
- search: undefined,
237
+ search: stored.search,
203
238
  preview: false,
204
239
  previewMode: undefined,
205
240
  apiKey: undefined,
@@ -207,6 +242,7 @@ function buildRunOptionsFromMetadata(metadata) {
207
242
  verbose: stored.verbose,
208
243
  heartbeatIntervalMs: stored.heartbeatIntervalMs,
209
244
  browserInlineFiles: stored.browserInlineFiles,
245
+ browserBundleFiles: stored.browserBundleFiles,
210
246
  background: stored.background,
211
247
  };
212
248
  }
@@ -217,6 +253,7 @@ function getBrowserConfigFromMetadata(metadata) {
217
253
  return metadata.options?.browserConfig ?? metadata.browser?.config;
218
254
  }
219
255
  async function runRootCommand(options) {
256
+ const userConfig = (await loadUserConfig()).config;
220
257
  const helpRequested = rawCliArgs.some((arg) => arg === '--help' || arg === '-h');
221
258
  if (helpRequested) {
222
259
  if (options.verbose) {
@@ -228,7 +265,11 @@ async function runRootCommand(options) {
228
265
  return;
229
266
  }
230
267
  const previewMode = resolvePreviewMode(options.preview);
231
- if (rawCliArgs.length === 0) {
268
+ if (userCliArgs.length === 0) {
269
+ if (tuiEnabled()) {
270
+ await launchTui({ version: VERSION });
271
+ return;
272
+ }
232
273
  console.log(chalk.yellow('No prompt or subcommand supplied. See `oracle --help` for usage.'));
233
274
  program.help({ error: false });
234
275
  return;
@@ -243,13 +284,35 @@ async function runRootCommand(options) {
243
284
  if (options.dryRun && options.renderMarkdown) {
244
285
  throw new Error('--dry-run cannot be combined with --render-markdown.');
245
286
  }
246
- const engine = resolveEngine({ engine: options.engine, browserFlag: options.browser, env: process.env });
287
+ const preferredEngine = options.engine ?? userConfig.engine;
288
+ const engine = resolveEngine({ engine: preferredEngine, browserFlag: options.browser, env: process.env });
247
289
  if (options.browser) {
248
290
  console.log(chalk.yellow('`--browser` is deprecated; use `--engine browser` instead.'));
249
291
  }
292
+ if (program.getOptionValueSource?.('model') === 'default' && userConfig.model) {
293
+ options.model = userConfig.model;
294
+ }
295
+ if (program.getOptionValueSource?.('search') === 'default' && userConfig.search) {
296
+ options.search = userConfig.search === 'on';
297
+ }
298
+ if (program.getOptionValueSource?.('filesReport') === 'default' && userConfig.filesReport != null) {
299
+ options.filesReport = Boolean(userConfig.filesReport);
300
+ }
301
+ if (program.getOptionValueSource?.('heartbeat') === 'default' && typeof userConfig.heartbeatSeconds === 'number') {
302
+ options.heartbeat = userConfig.heartbeatSeconds;
303
+ }
250
304
  const cliModelArg = normalizeModelOption(options.model) || 'gpt-5-pro';
251
305
  const resolvedModel = engine === 'browser' ? inferModelFromLabel(cliModelArg) : resolveApiModel(cliModelArg);
252
306
  const resolvedOptions = { ...options, model: resolvedModel };
307
+ // Decide whether to block until completion:
308
+ // - explicit --wait / --no-wait wins
309
+ // - otherwise block for fast models (gpt-5.1, browser) and detach by default for gpt-5-pro API
310
+ const waitPreference = resolveWaitFlag({
311
+ waitFlag: options.wait,
312
+ noWaitFlag: options.noWait,
313
+ model: resolvedModel,
314
+ engine,
315
+ });
253
316
  if (await handleStatusFlag(options, { attachSession, showStatus })) {
254
317
  return;
255
318
  }
@@ -269,19 +332,34 @@ async function runRootCommand(options) {
269
332
  return;
270
333
  }
271
334
  if (previewMode) {
272
- if (engine === 'browser') {
273
- throw new Error('--engine browser cannot be combined with --preview.');
274
- }
275
335
  if (!options.prompt) {
276
336
  throw new Error('Prompt is required when using --preview.');
277
337
  }
338
+ if (userConfig.promptSuffix) {
339
+ options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
340
+ }
341
+ resolvedOptions.prompt = options.prompt;
278
342
  const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode });
343
+ if (engine === 'browser') {
344
+ await runBrowserPreview({
345
+ runOptions,
346
+ cwd: process.cwd(),
347
+ version: VERSION,
348
+ previewMode,
349
+ log: console.log,
350
+ }, {});
351
+ return;
352
+ }
279
353
  await runOracle(runOptions, { log: console.log, write: (chunk) => process.stdout.write(chunk) });
280
354
  return;
281
355
  }
282
356
  if (!options.prompt) {
283
357
  throw new Error('Prompt is required when starting a new session.');
284
358
  }
359
+ if (userConfig.promptSuffix) {
360
+ options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
361
+ }
362
+ resolvedOptions.prompt = options.prompt;
285
363
  if (options.dryRun) {
286
364
  const baseRunOptions = buildRunOptions(resolvedOptions, { preview: false, previewMode: undefined });
287
365
  await runDryRunSummary({
@@ -296,6 +374,13 @@ async function runRootCommand(options) {
296
374
  if (options.file && options.file.length > 0) {
297
375
  await readFiles(options.file, { cwd: process.cwd() });
298
376
  }
377
+ applyBrowserDefaultsFromConfig(options, userConfig);
378
+ const notifications = resolveNotificationSettings({
379
+ cliNotify: options.notify,
380
+ cliNotifySound: options.notifySound,
381
+ env: process.env,
382
+ config: userConfig.notify,
383
+ });
299
384
  const sessionMode = engine === 'browser' ? 'browser' : 'api';
300
385
  const browserModelLabelOverride = sessionMode === 'browser' ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
301
386
  const browserConfig = sessionMode === 'browser'
@@ -306,12 +391,21 @@ async function runRootCommand(options) {
306
391
  })
307
392
  : undefined;
308
393
  await ensureSessionStorage();
309
- const baseRunOptions = buildRunOptions(resolvedOptions, { preview: false, previewMode: undefined });
394
+ const baseRunOptions = buildRunOptions(resolvedOptions, {
395
+ preview: false,
396
+ previewMode: undefined,
397
+ background: userConfig.background ?? resolvedOptions.background,
398
+ });
399
+ enforceBrowserSearchFlag(baseRunOptions, sessionMode, console.log);
400
+ if (sessionMode === 'browser' && baseRunOptions.search === false) {
401
+ console.log(chalk.dim('Note: search is not available in browser engine; ignoring search=false.'));
402
+ baseRunOptions.search = undefined;
403
+ }
310
404
  const sessionMeta = await initializeSession({
311
405
  ...baseRunOptions,
312
406
  mode: sessionMode,
313
407
  browserConfig,
314
- }, process.cwd());
408
+ }, process.cwd(), notifications);
315
409
  const reattachCommand = `pnpm oracle session ${sessionMeta.id}`;
316
410
  console.log(chalk.bold(`Reattach later with: ${chalk.cyan(reattachCommand)}`));
317
411
  console.log('');
@@ -324,8 +418,18 @@ async function runRootCommand(options) {
324
418
  console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
325
419
  return false;
326
420
  });
421
+ if (!waitPreference) {
422
+ if (!detached) {
423
+ console.log(chalk.red('Unable to start in background; use --wait to run inline.'));
424
+ process.exitCode = 1;
425
+ return;
426
+ }
427
+ console.log(chalk.blue(`Session running in background. Reattach via: oracle session ${sessionMeta.id}`));
428
+ console.log(chalk.dim('Pro runs can take up to 10 minutes. Add --wait to stay attached.'));
429
+ return;
430
+ }
327
431
  if (detached === false) {
328
- await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, true);
432
+ await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, true, notifications, userConfig);
329
433
  console.log(chalk.bold(`Session ${sessionMeta.id} completed`));
330
434
  return;
331
435
  }
@@ -335,11 +439,11 @@ async function runRootCommand(options) {
335
439
  console.log(chalk.bold(`Session ${sessionMeta.id} completed`));
336
440
  }
337
441
  }
338
- async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true) {
442
+ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig) {
339
443
  const { logLine, writeChunk, stream } = createSessionLogWriter(sessionMeta.id);
340
444
  let headerAugmented = false;
341
445
  const combinedLog = (message = '') => {
342
- if (!headerAugmented && message.startsWith('Oracle (')) {
446
+ if (!headerAugmented && message.startsWith('oracle (')) {
343
447
  headerAugmented = true;
344
448
  if (showReattachHint) {
345
449
  console.log(`${message}\n${chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`)}`);
@@ -367,7 +471,14 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
367
471
  log: combinedLog,
368
472
  write: combinedWrite,
369
473
  version: VERSION,
474
+ notifications: notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env, userConfig?.notify),
370
475
  });
476
+ const latest = await readSessionMetadata(sessionMeta.id);
477
+ const summary = latest ? formatCompletionSummary(latest, { includeSlug: true }) : null;
478
+ if (summary) {
479
+ console.log('\n' + chalk.green.bold(summary));
480
+ logLine(summary); // plain text in log, colored on stdout
481
+ }
371
482
  }
372
483
  catch (error) {
373
484
  throw error;
@@ -407,6 +518,8 @@ async function executeSession(sessionId) {
407
518
  const sessionMode = getSessionMode(metadata);
408
519
  const browserConfig = getBrowserConfigFromMetadata(metadata);
409
520
  const { logLine, writeChunk, stream } = createSessionLogWriter(sessionId);
521
+ const userConfig = (await loadUserConfig()).config;
522
+ const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
410
523
  try {
411
524
  await performSessionRun({
412
525
  sessionMeta: metadata,
@@ -417,6 +530,7 @@ async function executeSession(sessionId) {
417
530
  log: logLine,
418
531
  write: writeChunk,
419
532
  version: VERSION,
533
+ notifications,
420
534
  });
421
535
  }
422
536
  catch {
@@ -456,6 +570,43 @@ function printDebugOptionGroup(entries) {
456
570
  console.log(` ${label}${description}`);
457
571
  });
458
572
  }
573
+ function resolveWaitFlag({ waitFlag, noWaitFlag, model, engine, }) {
574
+ if (waitFlag === true)
575
+ return true;
576
+ if (noWaitFlag === true)
577
+ return false;
578
+ return defaultWaitPreference(model, engine);
579
+ }
580
+ function applyBrowserDefaultsFromConfig(options, config) {
581
+ const browser = config.browser;
582
+ if (!browser)
583
+ return;
584
+ const source = (key) => program.getOptionValueSource?.(key);
585
+ if (source('browserChromeProfile') === 'default' && browser.chromeProfile !== undefined) {
586
+ options.browserChromeProfile = browser.chromeProfile ?? undefined;
587
+ }
588
+ if (source('browserChromePath') === 'default' && browser.chromePath !== undefined) {
589
+ options.browserChromePath = browser.chromePath ?? undefined;
590
+ }
591
+ if (source('browserUrl') === 'default' && browser.url !== undefined) {
592
+ options.browserUrl = browser.url;
593
+ }
594
+ if (source('browserTimeout') === 'default' && typeof browser.timeoutMs === 'number') {
595
+ options.browserTimeout = String(browser.timeoutMs);
596
+ }
597
+ if (source('browserInputTimeout') === 'default' && typeof browser.inputTimeoutMs === 'number') {
598
+ options.browserInputTimeout = String(browser.inputTimeoutMs);
599
+ }
600
+ if (source('browserHeadless') === 'default' && browser.headless !== undefined) {
601
+ options.browserHeadless = browser.headless;
602
+ }
603
+ if (source('browserHideWindow') === 'default' && browser.hideWindow !== undefined) {
604
+ options.browserHideWindow = browser.hideWindow;
605
+ }
606
+ if (source('browserKeepBrowser') === 'default' && browser.keepBrowser !== undefined) {
607
+ options.browserKeepBrowser = browser.keepBrowser;
608
+ }
609
+ }
459
610
  program.action(async function () {
460
611
  const options = this.optsWithGlobals();
461
612
  await runRootCommand(options);
@@ -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
+ });