@steipete/oracle 1.0.8 → 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 (106) hide show
  1. package/README.md +32 -4
  2. package/assets-oracle-icon.png +0 -0
  3. package/dist/bin/oracle-cli.js +178 -21
  4. package/dist/bin/oracle-mcp.js +6 -0
  5. package/dist/markdansi/types/index.js +4 -0
  6. package/dist/oracle/bin/oracle-cli.js +472 -0
  7. package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
  8. package/dist/oracle/src/browser/actions/attachments.js +82 -0
  9. package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
  10. package/dist/oracle/src/browser/actions/navigation.js +75 -0
  11. package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
  12. package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
  13. package/dist/oracle/src/browser/config.js +33 -0
  14. package/dist/oracle/src/browser/constants.js +40 -0
  15. package/dist/oracle/src/browser/cookies.js +210 -0
  16. package/dist/oracle/src/browser/domDebug.js +36 -0
  17. package/dist/oracle/src/browser/index.js +331 -0
  18. package/dist/oracle/src/browser/pageActions.js +5 -0
  19. package/dist/oracle/src/browser/prompt.js +88 -0
  20. package/dist/oracle/src/browser/promptSummary.js +20 -0
  21. package/dist/oracle/src/browser/sessionRunner.js +80 -0
  22. package/dist/oracle/src/browser/types.js +1 -0
  23. package/dist/oracle/src/browser/utils.js +62 -0
  24. package/dist/oracle/src/browserMode.js +1 -0
  25. package/dist/oracle/src/cli/browserConfig.js +44 -0
  26. package/dist/oracle/src/cli/dryRun.js +59 -0
  27. package/dist/oracle/src/cli/engine.js +17 -0
  28. package/dist/oracle/src/cli/errorUtils.js +9 -0
  29. package/dist/oracle/src/cli/help.js +70 -0
  30. package/dist/oracle/src/cli/markdownRenderer.js +15 -0
  31. package/dist/oracle/src/cli/options.js +103 -0
  32. package/dist/oracle/src/cli/promptRequirement.js +14 -0
  33. package/dist/oracle/src/cli/rootAlias.js +30 -0
  34. package/dist/oracle/src/cli/sessionCommand.js +77 -0
  35. package/dist/oracle/src/cli/sessionDisplay.js +270 -0
  36. package/dist/oracle/src/cli/sessionRunner.js +94 -0
  37. package/dist/oracle/src/heartbeat.js +43 -0
  38. package/dist/oracle/src/oracle/client.js +48 -0
  39. package/dist/oracle/src/oracle/config.js +29 -0
  40. package/dist/oracle/src/oracle/errors.js +101 -0
  41. package/dist/oracle/src/oracle/files.js +220 -0
  42. package/dist/oracle/src/oracle/format.js +33 -0
  43. package/dist/oracle/src/oracle/fsAdapter.js +7 -0
  44. package/dist/oracle/src/oracle/oscProgress.js +60 -0
  45. package/dist/oracle/src/oracle/request.js +48 -0
  46. package/dist/oracle/src/oracle/run.js +444 -0
  47. package/dist/oracle/src/oracle/tokenStats.js +39 -0
  48. package/dist/oracle/src/oracle/types.js +1 -0
  49. package/dist/oracle/src/oracle.js +9 -0
  50. package/dist/oracle/src/sessionManager.js +205 -0
  51. package/dist/oracle/src/version.js +39 -0
  52. package/dist/src/browser/actions/modelSelection.js +117 -29
  53. package/dist/src/browser/cookies.js +1 -1
  54. package/dist/src/browser/index.js +2 -1
  55. package/dist/src/browser/prompt.js +6 -5
  56. package/dist/src/browser/sessionRunner.js +4 -2
  57. package/dist/src/cli/dryRun.js +41 -5
  58. package/dist/src/cli/engine.js +7 -0
  59. package/dist/src/cli/help.js +1 -1
  60. package/dist/src/cli/hiddenAliases.js +17 -0
  61. package/dist/src/cli/markdownRenderer.js +97 -0
  62. package/dist/src/cli/notifier.js +223 -0
  63. package/dist/src/cli/promptRequirement.js +3 -0
  64. package/dist/src/cli/rootAlias.js +14 -0
  65. package/dist/src/cli/runOptions.js +29 -0
  66. package/dist/src/cli/sessionCommand.js +60 -2
  67. package/dist/src/cli/sessionDisplay.js +222 -10
  68. package/dist/src/cli/sessionRunner.js +21 -2
  69. package/dist/src/cli/tui/index.js +436 -0
  70. package/dist/src/config.js +27 -0
  71. package/dist/src/mcp/server.js +36 -0
  72. package/dist/src/mcp/tools/consult.js +158 -0
  73. package/dist/src/mcp/tools/sessionResources.js +64 -0
  74. package/dist/src/mcp/tools/sessions.js +106 -0
  75. package/dist/src/mcp/types.js +17 -0
  76. package/dist/src/mcp/utils.js +24 -0
  77. package/dist/src/oracle/files.js +143 -6
  78. package/dist/src/oracle/oscProgress.js +60 -0
  79. package/dist/src/oracle/run.js +104 -71
  80. package/dist/src/oracle/tokenEstimate.js +34 -0
  81. package/dist/src/sessionManager.js +65 -3
  82. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  83. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  84. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  85. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  86. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  87. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  88. package/dist/vendor/oracle-notifier/README.md +24 -0
  89. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  90. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  91. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  92. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  93. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  94. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  95. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
  96. package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
  97. package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
  98. package/package.json +27 -9
  99. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  100. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  101. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  102. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  103. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  104. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  105. package/vendor/oracle-notifier/README.md +24 -0
  106. package/vendor/oracle-notifier/build-notifier.sh +93 -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`.
@@ -74,6 +97,11 @@ More knobs (`--max-input`, cookie sync controls for browser mode, etc.) live beh
74
97
  ## Sessions & background runs
75
98
 
76
99
  Every non-preview run writes to `~/.oracle/sessions/<slug>` with usage, cost hints, and logs. Use `oracle status` to list sessions, `oracle session <id>` to replay, and `oracle status --clear --hours 168` to prune. Set `ORACLE_HOME_DIR` to relocate storage.
100
+ Add `--render` (alias `--render-markdown`) when attaching to pretty-print the stored markdown if your terminal supports color; falls back to raw text otherwise.
101
+
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.
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>`.
77
105
 
78
106
  ## Testing
79
107
 
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
- import { handleStatusFlag } from '../src/cli/rootAlias.js';
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,19 +62,29 @@ 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'])
63
82
  .preset('summary'))
64
83
  .addOption(new Option('--exec-session <id>').hideHelp())
84
+ .addOption(new Option('--session <id>').hideHelp())
65
85
  .addOption(new Option('--status', 'Show stored sessions (alias for `oracle status`).').default(false).hideHelp())
66
86
  .option('--render-markdown', 'Emit the assembled markdown bundle for prompt + files and exit.', false)
87
+ .option('--verbose-render', 'Show render/TTY diagnostics when replaying sessions.', false)
67
88
  .addOption(new Option('--search <mode>', 'Set server-side search behavior (on/off).')
68
89
  .argParser(parseSearchOption)
69
90
  .hideHelp())
@@ -85,8 +106,11 @@ program
85
106
  .addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
86
107
  .addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
87
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))
88
110
  .option('--debug-help', 'Show the advanced/debug option set and exit.', false)
89
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())
90
114
  .showHelpAfterError('(use --help for usage)');
91
115
  program.addHelpText('after', `
92
116
  Examples:
@@ -104,6 +128,10 @@ const sessionCommand = program
104
128
  .option('--limit <count>', 'Maximum sessions to show when listing (max 1000).', parseIntOption, 100)
105
129
  .option('--all', 'Include all stored sessions regardless of age.', false)
106
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)
132
+ .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
133
+ .option('--render-markdown', 'Alias for --render.', false)
134
+ .option('--path', 'Print the stored session paths instead of attaching.', false)
107
135
  .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
108
136
  .action(async (sessionId, _options, cmd) => {
109
137
  await handleSessionCommand(sessionId, cmd);
@@ -115,6 +143,9 @@ const statusCommand = program
115
143
  .option('--limit <count>', 'Maximum sessions to show (max 1000).', parseIntOption, 100)
116
144
  .option('--all', 'Include all stored sessions regardless of age.', false)
117
145
  .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
146
+ .option('--render', 'Render completed session output as markdown (rich TTY only).', false)
147
+ .option('--render-markdown', 'Alias for --render.', false)
148
+ .option('--hide-prompt', 'Hide stored prompt when displaying a session.', false)
118
149
  .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
119
150
  .action(async (sessionId, _options, command) => {
120
151
  const statusOptions = command.opts();
@@ -138,7 +169,11 @@ const statusCommand = program
138
169
  return;
139
170
  }
140
171
  if (sessionId) {
141
- 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 });
142
177
  return;
143
178
  }
144
179
  const showExamples = usesDefaultStatusFilters(command);
@@ -171,9 +206,16 @@ function buildRunOptions(options, overrides = {}) {
171
206
  verbose: overrides.verbose ?? options.verbose,
172
207
  heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
173
208
  browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
209
+ browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
174
210
  background: overrides.background ?? undefined,
175
211
  };
176
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
+ }
177
219
  function resolveHeartbeatIntervalMs(seconds) {
178
220
  if (typeof seconds !== 'number' || seconds <= 0) {
179
221
  return undefined;
@@ -192,7 +234,7 @@ function buildRunOptionsFromMetadata(metadata) {
192
234
  maxOutput: stored.maxOutput,
193
235
  system: stored.system,
194
236
  silent: stored.silent,
195
- search: undefined,
237
+ search: stored.search,
196
238
  preview: false,
197
239
  previewMode: undefined,
198
240
  apiKey: undefined,
@@ -200,6 +242,7 @@ function buildRunOptionsFromMetadata(metadata) {
200
242
  verbose: stored.verbose,
201
243
  heartbeatIntervalMs: stored.heartbeatIntervalMs,
202
244
  browserInlineFiles: stored.browserInlineFiles,
245
+ browserBundleFiles: stored.browserBundleFiles,
203
246
  background: stored.background,
204
247
  };
205
248
  }
@@ -210,6 +253,7 @@ function getBrowserConfigFromMetadata(metadata) {
210
253
  return metadata.options?.browserConfig ?? metadata.browser?.config;
211
254
  }
212
255
  async function runRootCommand(options) {
256
+ const userConfig = (await loadUserConfig()).config;
213
257
  const helpRequested = rawCliArgs.some((arg) => arg === '--help' || arg === '-h');
214
258
  if (helpRequested) {
215
259
  if (options.verbose) {
@@ -221,7 +265,11 @@ async function runRootCommand(options) {
221
265
  return;
222
266
  }
223
267
  const previewMode = resolvePreviewMode(options.preview);
224
- if (rawCliArgs.length === 0) {
268
+ if (userCliArgs.length === 0) {
269
+ if (tuiEnabled()) {
270
+ await launchTui({ version: VERSION });
271
+ return;
272
+ }
225
273
  console.log(chalk.yellow('No prompt or subcommand supplied. See `oracle --help` for usage.'));
226
274
  program.help({ error: false });
227
275
  return;
@@ -236,18 +284,39 @@ async function runRootCommand(options) {
236
284
  if (options.dryRun && options.renderMarkdown) {
237
285
  throw new Error('--dry-run cannot be combined with --render-markdown.');
238
286
  }
239
- 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 });
240
289
  if (options.browser) {
241
290
  console.log(chalk.yellow('`--browser` is deprecated; use `--engine browser` instead.'));
242
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
+ }
243
304
  const cliModelArg = normalizeModelOption(options.model) || 'gpt-5-pro';
244
305
  const resolvedModel = engine === 'browser' ? inferModelFromLabel(cliModelArg) : resolveApiModel(cliModelArg);
245
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
+ });
246
316
  if (await handleStatusFlag(options, { attachSession, showStatus })) {
247
317
  return;
248
318
  }
249
- if (options.session) {
250
- await attachSession(options.session);
319
+ if (await handleSessionAlias(options, { attachSession })) {
251
320
  return;
252
321
  }
253
322
  if (options.execSession) {
@@ -263,19 +332,34 @@ async function runRootCommand(options) {
263
332
  return;
264
333
  }
265
334
  if (previewMode) {
266
- if (engine === 'browser') {
267
- throw new Error('--engine browser cannot be combined with --preview.');
268
- }
269
335
  if (!options.prompt) {
270
336
  throw new Error('Prompt is required when using --preview.');
271
337
  }
338
+ if (userConfig.promptSuffix) {
339
+ options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
340
+ }
341
+ resolvedOptions.prompt = options.prompt;
272
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
+ }
273
353
  await runOracle(runOptions, { log: console.log, write: (chunk) => process.stdout.write(chunk) });
274
354
  return;
275
355
  }
276
356
  if (!options.prompt) {
277
357
  throw new Error('Prompt is required when starting a new session.');
278
358
  }
359
+ if (userConfig.promptSuffix) {
360
+ options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
361
+ }
362
+ resolvedOptions.prompt = options.prompt;
279
363
  if (options.dryRun) {
280
364
  const baseRunOptions = buildRunOptions(resolvedOptions, { preview: false, previewMode: undefined });
281
365
  await runDryRunSummary({
@@ -290,6 +374,13 @@ async function runRootCommand(options) {
290
374
  if (options.file && options.file.length > 0) {
291
375
  await readFiles(options.file, { cwd: process.cwd() });
292
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
+ });
293
384
  const sessionMode = engine === 'browser' ? 'browser' : 'api';
294
385
  const browserModelLabelOverride = sessionMode === 'browser' ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
295
386
  const browserConfig = sessionMode === 'browser'
@@ -300,12 +391,21 @@ async function runRootCommand(options) {
300
391
  })
301
392
  : undefined;
302
393
  await ensureSessionStorage();
303
- 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
+ }
304
404
  const sessionMeta = await initializeSession({
305
405
  ...baseRunOptions,
306
406
  mode: sessionMode,
307
407
  browserConfig,
308
- }, process.cwd());
408
+ }, process.cwd(), notifications);
309
409
  const reattachCommand = `pnpm oracle session ${sessionMeta.id}`;
310
410
  console.log(chalk.bold(`Reattach later with: ${chalk.cyan(reattachCommand)}`));
311
411
  console.log('');
@@ -318,8 +418,18 @@ async function runRootCommand(options) {
318
418
  console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
319
419
  return false;
320
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
+ }
321
431
  if (detached === false) {
322
- await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, true);
432
+ await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, true, notifications, userConfig);
323
433
  console.log(chalk.bold(`Session ${sessionMeta.id} completed`));
324
434
  return;
325
435
  }
@@ -329,11 +439,11 @@ async function runRootCommand(options) {
329
439
  console.log(chalk.bold(`Session ${sessionMeta.id} completed`));
330
440
  }
331
441
  }
332
- async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true) {
442
+ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true, notifications, userConfig) {
333
443
  const { logLine, writeChunk, stream } = createSessionLogWriter(sessionMeta.id);
334
444
  let headerAugmented = false;
335
445
  const combinedLog = (message = '') => {
336
- if (!headerAugmented && message.startsWith('Oracle (')) {
446
+ if (!headerAugmented && message.startsWith('oracle (')) {
337
447
  headerAugmented = true;
338
448
  if (showReattachHint) {
339
449
  console.log(`${message}\n${chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`)}`);
@@ -361,7 +471,14 @@ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfi
361
471
  log: combinedLog,
362
472
  write: combinedWrite,
363
473
  version: VERSION,
474
+ notifications: notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env, userConfig?.notify),
364
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
+ }
365
482
  }
366
483
  catch (error) {
367
484
  throw error;
@@ -401,6 +518,8 @@ async function executeSession(sessionId) {
401
518
  const sessionMode = getSessionMode(metadata);
402
519
  const browserConfig = getBrowserConfigFromMetadata(metadata);
403
520
  const { logLine, writeChunk, stream } = createSessionLogWriter(sessionId);
521
+ const userConfig = (await loadUserConfig()).config;
522
+ const notifications = deriveNotificationSettingsFromMetadata(metadata, process.env, userConfig.notify);
404
523
  try {
405
524
  await performSessionRun({
406
525
  sessionMeta: metadata,
@@ -411,6 +530,7 @@ async function executeSession(sessionId) {
411
530
  log: logLine,
412
531
  write: writeChunk,
413
532
  version: VERSION,
533
+ notifications,
414
534
  });
415
535
  }
416
536
  catch {
@@ -450,6 +570,43 @@ function printDebugOptionGroup(entries) {
450
570
  console.log(` ${label}${description}`);
451
571
  });
452
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
+ }
453
610
  program.action(async function () {
454
611
  const options = this.optsWithGlobals();
455
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
+ });
@@ -0,0 +1,4 @@
1
+ // Public API typings for Markdansi.
2
+ // Hand-authored source for `pnpm types` (tsc --emitDeclarationOnly)
3
+ // to produce dist/index.d.ts. Keep in sync with src/ changes.
4
+ export {};