@steipete/oracle 1.0.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -0
  3. package/dist/bin/oracle-cli.js +458 -0
  4. package/dist/bin/oracle.js +683 -0
  5. package/dist/scripts/browser-tools.js +536 -0
  6. package/dist/scripts/check.js +21 -0
  7. package/dist/scripts/chrome/browser-tools.js +295 -0
  8. package/dist/scripts/run-cli.js +14 -0
  9. package/dist/src/browser/actions/assistantResponse.js +471 -0
  10. package/dist/src/browser/actions/attachments.js +82 -0
  11. package/dist/src/browser/actions/modelSelection.js +190 -0
  12. package/dist/src/browser/actions/navigation.js +75 -0
  13. package/dist/src/browser/actions/promptComposer.js +167 -0
  14. package/dist/src/browser/chromeLifecycle.js +104 -0
  15. package/dist/src/browser/config.js +33 -0
  16. package/dist/src/browser/constants.js +40 -0
  17. package/dist/src/browser/cookies.js +210 -0
  18. package/dist/src/browser/domDebug.js +36 -0
  19. package/dist/src/browser/index.js +319 -0
  20. package/dist/src/browser/pageActions.js +5 -0
  21. package/dist/src/browser/prompt.js +56 -0
  22. package/dist/src/browser/promptSummary.js +20 -0
  23. package/dist/src/browser/sessionRunner.js +77 -0
  24. package/dist/src/browser/types.js +1 -0
  25. package/dist/src/browser/utils.js +62 -0
  26. package/dist/src/browserMode.js +1 -0
  27. package/dist/src/cli/browserConfig.js +44 -0
  28. package/dist/src/cli/dryRun.js +59 -0
  29. package/dist/src/cli/engine.js +17 -0
  30. package/dist/src/cli/errorUtils.js +9 -0
  31. package/dist/src/cli/help.js +68 -0
  32. package/dist/src/cli/options.js +103 -0
  33. package/dist/src/cli/promptRequirement.js +14 -0
  34. package/dist/src/cli/rootAlias.js +16 -0
  35. package/dist/src/cli/sessionCommand.js +48 -0
  36. package/dist/src/cli/sessionDisplay.js +222 -0
  37. package/dist/src/cli/sessionRunner.js +94 -0
  38. package/dist/src/heartbeat.js +43 -0
  39. package/dist/src/oracle/client.js +48 -0
  40. package/dist/src/oracle/config.js +29 -0
  41. package/dist/src/oracle/errors.js +101 -0
  42. package/dist/src/oracle/files.js +220 -0
  43. package/dist/src/oracle/format.js +33 -0
  44. package/dist/src/oracle/fsAdapter.js +7 -0
  45. package/dist/src/oracle/request.js +48 -0
  46. package/dist/src/oracle/run.js +411 -0
  47. package/dist/src/oracle/tokenStats.js +39 -0
  48. package/dist/src/oracle/types.js +1 -0
  49. package/dist/src/oracle.js +9 -0
  50. package/dist/src/sessionManager.js +205 -0
  51. package/dist/src/version.js +39 -0
  52. package/package.json +69 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Peter Steinberger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # oracle — Whispering your tokens to the silicon sage
2
+
3
+ <p align="center">
4
+ <img src="./README-header.png" alt="Oracle CLI header banner" width="1100">
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@steipete/oracle"><img src="https://img.shields.io/npm/v/@steipete/oracle?style=for-the-badge&logo=npm&logoColor=white" alt="npm version"></a>
9
+ <a href="https://github.com/steipete/oracle/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/steipete/oracle/ci.yml?branch=main&style=for-the-badge&label=tests" alt="CI Status"></a>
10
+ <a href="https://github.com/steipete/oracle"><img src="https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20Windows-blue?style=for-the-badge" alt="Platforms"></a>
11
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="MIT License"></a>
12
+ </p>
13
+
14
+ Oracle gives your agents a simple, reliable way to **bundle a prompt plus the right files and hand them to another AI**. It currently speaks GPT-5.1 and GPT-5 Pro; Pro runs can take up to ten minutes and often return remarkably strong answers.
15
+
16
+ ## Two engines, one CLI
17
+
18
+ - **API engine** — Calls the OpenAI Responses API. Needs `OPENAI_API_KEY`.
19
+ - **Browser engine** — Automates ChatGPT in Chrome so you can use your Pro account directly. Toggle with `--engine browser`; no API key required.
20
+
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
+
23
+ ## Quick start
24
+
25
+ ```bash
26
+ # One-off (no install)
27
+ OPENAI_API_KEY=sk-... npx @steipete/oracle -p "Summarize the risk register" --file docs/risk-register.md docs/risk-matrix.md
28
+
29
+ # Browser engine (no API key)
30
+ npx @steipete/oracle --engine browser -p "Summarize the risk register" --file docs/risk-register.md docs/risk-matrix.md
31
+
32
+ # Globs/exclusions
33
+ npx @steipete/oracle -p "Review the TS data layer" --file "src/**/*.ts" --file "!src/**/*.test.ts"
34
+
35
+ # Inspect past sessions
36
+ oracle status --clear --hours 168 # prune a week of cached runs
37
+ oracle status # list runs; grab an ID
38
+ oracle session <id> # replay a run locally
39
+ ```
40
+
41
+ ## How do I integrate this?
42
+
43
+ - **One-liner in CI** — `OPENAI_API_KEY=sk-... npx @steipete/oracle --prompt "Smoke-check latest PR" --file src/ docs/ --preview summary` (add to your pipeline as a non-blocking report step).
44
+ - **Package script** — In `package.json`: `"oracle": "oracle --prompt \"Review the diff\" --file ."` then run `OPENAI_API_KEY=... pnpm oracle`.
45
+
46
+ ## Highlights
47
+
48
+ - **Bundle once, reuse anywhere** — Prompt + files become a markdown package the model can cite.
49
+ - **Flexible file selection** — Glob patterns and `!` excludes let you scoop up or skip files without scripting.
50
+ - **Pro-friendly** — GPT-5 Pro background runs stay alive for ~10 minutes with reconnection + token/cost tracking.
51
+ - **Two paths, one UX** — API or browser, same flags and session logs.
52
+ - **Search on by default** — The model can ground answers with fresh citations.
53
+ - **File safety** — Per-file token accounting and size guards; `--files-report` shows exactly what you’re sending.
54
+ - **Readable previews** — `--preview` / `--render-markdown` let you inspect the bundle before spending.
55
+
56
+ ## Flags you’ll actually use
57
+
58
+ | Flag | Purpose |
59
+ | --- | --- |
60
+ | `-p, --prompt <text>` | Required prompt. |
61
+ | `-f, --file <paths...>` | Attach files/dirs (supports globs and `!` excludes). |
62
+ | `-e, --engine <api|browser>` | Choose API or browser automation. Omitted: API when `OPENAI_API_KEY` is set, otherwise browser. |
63
+ | `-m, --model <name>` | `gpt-5-pro` (default) or `gpt-5.1`. |
64
+ | `--files-report` | Print per-file token usage. |
65
+ | `--preview [summary|json|full]` | Inspect the request without sending. |
66
+ | `--render-markdown` | Print the assembled `[SYSTEM]/[USER]/[FILE]` bundle. |
67
+ | `-v, --verbose` | Extra logging (also surfaces advanced flags with `--help`). |
68
+
69
+ More knobs (`--max-input`, cookie sync controls for browser mode, etc.) live behind `oracle --help --verbose`.
70
+
71
+ ## Sessions & background runs
72
+
73
+ 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.
74
+
75
+ ## Testing
76
+
77
+ ```bash
78
+ pnpm test
79
+ pnpm test:coverage
80
+ ```
81
+
82
+ ---
83
+
84
+ If you’re looking for an even more powerful context-management tool, check out https://repoprompt.com
85
+ Name inspired by: https://ampcode.com/news/oracle
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import { spawn } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Command, Option } from 'commander';
6
+ import { resolveEngine } from '../src/cli/engine.js';
7
+ import { shouldRequirePrompt } from '../src/cli/promptRequirement.js';
8
+ import chalk from 'chalk';
9
+ import { ensureSessionStorage, initializeSession, readSessionMetadata, createSessionLogWriter, deleteSessionsOlderThan, } from '../src/sessionManager.js';
10
+ import { runOracle, renderPromptMarkdown, readFiles } from '../src/oracle.js';
11
+ import { CHATGPT_URL } from '../src/browserMode.js';
12
+ 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 { buildBrowserConfig, resolveBrowserModelLabel } from '../src/cli/browserConfig.js';
15
+ import { performSessionRun } from '../src/cli/sessionRunner.js';
16
+ import { attachSession, showStatus } from '../src/cli/sessionDisplay.js';
17
+ import { handleSessionCommand, formatSessionCleanupMessage } from '../src/cli/sessionCommand.js';
18
+ import { isErrorLogged } from '../src/cli/errorUtils.js';
19
+ import { handleStatusFlag } from '../src/cli/rootAlias.js';
20
+ import { getCliVersion } from '../src/version.js';
21
+ import { runDryRunSummary } from '../src/cli/dryRun.js';
22
+ const VERSION = getCliVersion();
23
+ const CLI_ENTRYPOINT = fileURLToPath(import.meta.url);
24
+ const rawCliArgs = process.argv.slice(2);
25
+ const isTty = process.stdout.isTTY;
26
+ const program = new Command();
27
+ applyHelpStyling(program, VERSION, isTty);
28
+ program.hook('preAction', (thisCommand) => {
29
+ if (thisCommand !== program) {
30
+ return;
31
+ }
32
+ if (rawCliArgs.some((arg) => arg === '--help' || arg === '-h')) {
33
+ return;
34
+ }
35
+ const opts = thisCommand.optsWithGlobals();
36
+ if (shouldRequirePrompt(rawCliArgs, opts)) {
37
+ throw new Error('Prompt is required. Provide it via --prompt "<text>".');
38
+ }
39
+ });
40
+ program
41
+ .name('oracle')
42
+ .description('One-shot GPT-5 Pro / GPT-5.1 tool for hard questions that benefit from large file context and server-side search.')
43
+ .version(VERSION)
44
+ .option('-p, --prompt <text>', 'User prompt to send to the model.')
45
+ .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, [])
46
+ .option('-s, --slug <words>', 'Custom session slug (3-5 words).')
47
+ .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')
48
+ .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']))
49
+ .option('--files-report', 'Show token usage per attached file (also prints automatically when files exceed the token budget).', false)
50
+ .option('-v, --verbose', 'Enable verbose logging for all operations.', false)
51
+ .option('--dry-run', 'Validate inputs and show token estimates without calling the model.', false)
52
+ .addOption(new Option('--preview [mode]', 'Preview the request without calling the API (summary | json | full).')
53
+ .choices(['summary', 'json', 'full'])
54
+ .preset('summary'))
55
+ .addOption(new Option('--exec-session <id>').hideHelp())
56
+ .addOption(new Option('--status', 'Show stored sessions (alias for `oracle status`).').default(false).hideHelp())
57
+ .option('--render-markdown', 'Emit the assembled markdown bundle for prompt + files and exit.', false)
58
+ .addOption(new Option('--search <mode>', 'Set server-side search behavior (on/off).')
59
+ .argParser(parseSearchOption)
60
+ .hideHelp())
61
+ .addOption(new Option('--max-input <tokens>', 'Override the input token budget for the selected model.')
62
+ .argParser(parseIntOption)
63
+ .hideHelp())
64
+ .addOption(new Option('--max-output <tokens>', 'Override the max output tokens for the selected model.')
65
+ .argParser(parseIntOption)
66
+ .hideHelp())
67
+ .addOption(new Option('--browser', '(deprecated) Use --engine browser instead.').default(false).hideHelp())
68
+ .addOption(new Option('--browser-chrome-profile <name>', 'Chrome profile name/path for cookie reuse.').hideHelp())
69
+ .addOption(new Option('--browser-chrome-path <path>', 'Explicit Chrome or Chromium executable path.').hideHelp())
70
+ .addOption(new Option('--browser-url <url>', `Override the ChatGPT URL (default ${CHATGPT_URL}).`).hideHelp())
71
+ .addOption(new Option('--browser-timeout <ms|s|m>', 'Maximum time to wait for an answer (default 900s).').hideHelp())
72
+ .addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 30s).').hideHelp())
73
+ .addOption(new Option('--browser-no-cookie-sync', 'Skip copying cookies from Chrome.').hideHelp())
74
+ .addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
75
+ .addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
76
+ .addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
77
+ .addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
78
+ .addOption(new Option('--browser-inline-files', 'Paste files directly into the ChatGPT composer instead of uploading attachments.').default(false))
79
+ .option('--debug-help', 'Show the advanced/debug option set and exit.', false)
80
+ .option('--heartbeat <seconds>', 'Emit periodic in-progress updates (0 to disable).', parseHeartbeatOption, 30)
81
+ .showHelpAfterError('(use --help for usage)');
82
+ program.addHelpText('after', `
83
+ Examples:
84
+ # Quick API run with two files
85
+ oracle --prompt "Summarize the risk register" --file docs/risk-register.md docs/risk-matrix.md
86
+
87
+ # Browser run (no API key) + globbed TypeScript sources, excluding tests
88
+ oracle --engine browser --prompt "Review the TS data layer" \\
89
+ --file "src/**/*.ts" --file "!src/**/*.test.ts"
90
+ `);
91
+ const sessionCommand = program
92
+ .command('session [id]')
93
+ .description('Attach to a stored session or list recent sessions when no ID is provided.')
94
+ .option('--hours <hours>', 'Look back this many hours when listing sessions (default 24).', parseFloatOption, 24)
95
+ .option('--limit <count>', 'Maximum sessions to show when listing (max 1000).', parseIntOption, 100)
96
+ .option('--all', 'Include all stored sessions regardless of age.', false)
97
+ .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
98
+ .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
99
+ .action(async (sessionId, _options, cmd) => {
100
+ await handleSessionCommand(sessionId, cmd);
101
+ });
102
+ const statusCommand = program
103
+ .command('status [id]')
104
+ .description('List recent sessions (24h window by default) or attach to a session when an ID is provided.')
105
+ .option('--hours <hours>', 'Look back this many hours (default 24).', parseFloatOption, 24)
106
+ .option('--limit <count>', 'Maximum sessions to show (max 1000).', parseIntOption, 100)
107
+ .option('--all', 'Include all stored sessions regardless of age.', false)
108
+ .option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
109
+ .addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
110
+ .action(async (sessionId, _options, command) => {
111
+ const statusOptions = command.opts();
112
+ const clearRequested = Boolean(statusOptions.clear || statusOptions.clean);
113
+ if (clearRequested) {
114
+ if (sessionId) {
115
+ console.error('Cannot combine a session ID with --clear. Remove the ID to delete cached sessions.');
116
+ process.exitCode = 1;
117
+ return;
118
+ }
119
+ const hours = statusOptions.hours;
120
+ const includeAll = statusOptions.all;
121
+ const result = await deleteSessionsOlderThan({ hours, includeAll });
122
+ const scope = includeAll ? 'all stored sessions' : `sessions older than ${hours}h`;
123
+ console.log(formatSessionCleanupMessage(result, scope));
124
+ return;
125
+ }
126
+ if (sessionId === 'clear' || sessionId === 'clean') {
127
+ console.error('Session cleanup now uses --clear. Run "oracle status --clear --hours <n>" instead.');
128
+ process.exitCode = 1;
129
+ return;
130
+ }
131
+ if (sessionId) {
132
+ await attachSession(sessionId);
133
+ return;
134
+ }
135
+ const showExamples = usesDefaultStatusFilters(command);
136
+ await showStatus({
137
+ hours: statusOptions.all ? Infinity : statusOptions.hours,
138
+ includeAll: statusOptions.all,
139
+ limit: statusOptions.limit,
140
+ showExamples,
141
+ });
142
+ });
143
+ function buildRunOptions(options, overrides = {}) {
144
+ if (!options.prompt) {
145
+ throw new Error('Prompt is required.');
146
+ }
147
+ return {
148
+ prompt: options.prompt,
149
+ model: options.model,
150
+ file: overrides.file ?? options.file ?? [],
151
+ slug: overrides.slug ?? options.slug,
152
+ filesReport: overrides.filesReport ?? options.filesReport,
153
+ maxInput: overrides.maxInput ?? options.maxInput,
154
+ maxOutput: overrides.maxOutput ?? options.maxOutput,
155
+ system: overrides.system ?? options.system,
156
+ silent: overrides.silent ?? options.silent,
157
+ search: overrides.search ?? options.search,
158
+ preview: overrides.preview ?? undefined,
159
+ previewMode: overrides.previewMode ?? options.previewMode,
160
+ apiKey: overrides.apiKey ?? options.apiKey,
161
+ sessionId: overrides.sessionId ?? options.sessionId,
162
+ verbose: overrides.verbose ?? options.verbose,
163
+ heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
164
+ browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
165
+ background: overrides.background ?? undefined,
166
+ };
167
+ }
168
+ function resolveHeartbeatIntervalMs(seconds) {
169
+ if (typeof seconds !== 'number' || seconds <= 0) {
170
+ return undefined;
171
+ }
172
+ return Math.round(seconds * 1000);
173
+ }
174
+ function buildRunOptionsFromMetadata(metadata) {
175
+ const stored = metadata.options ?? {};
176
+ return {
177
+ prompt: stored.prompt ?? '',
178
+ model: stored.model ?? 'gpt-5-pro',
179
+ file: stored.file ?? [],
180
+ slug: stored.slug,
181
+ filesReport: stored.filesReport,
182
+ maxInput: stored.maxInput,
183
+ maxOutput: stored.maxOutput,
184
+ system: stored.system,
185
+ silent: stored.silent,
186
+ search: undefined,
187
+ preview: false,
188
+ previewMode: undefined,
189
+ apiKey: undefined,
190
+ sessionId: metadata.id,
191
+ verbose: stored.verbose,
192
+ heartbeatIntervalMs: stored.heartbeatIntervalMs,
193
+ browserInlineFiles: stored.browserInlineFiles,
194
+ background: stored.background,
195
+ };
196
+ }
197
+ function getSessionMode(metadata) {
198
+ return metadata.mode ?? metadata.options?.mode ?? 'api';
199
+ }
200
+ function getBrowserConfigFromMetadata(metadata) {
201
+ return metadata.options?.browserConfig ?? metadata.browser?.config;
202
+ }
203
+ async function runRootCommand(options) {
204
+ const helpRequested = rawCliArgs.some((arg) => arg === '--help' || arg === '-h');
205
+ if (helpRequested) {
206
+ if (options.verbose) {
207
+ console.log('');
208
+ printDebugHelp(program.name());
209
+ console.log('');
210
+ }
211
+ program.help({ error: false });
212
+ return;
213
+ }
214
+ const previewMode = resolvePreviewMode(options.preview);
215
+ if (rawCliArgs.length === 0) {
216
+ console.log(chalk.yellow('No prompt or subcommand supplied. See `oracle --help` for usage.'));
217
+ program.help({ error: false });
218
+ return;
219
+ }
220
+ if (options.debugHelp) {
221
+ printDebugHelp(program.name());
222
+ return;
223
+ }
224
+ if (options.dryRun && previewMode) {
225
+ throw new Error('--dry-run cannot be combined with --preview.');
226
+ }
227
+ if (options.dryRun && options.renderMarkdown) {
228
+ throw new Error('--dry-run cannot be combined with --render-markdown.');
229
+ }
230
+ const engine = resolveEngine({ engine: options.engine, browserFlag: options.browser, env: process.env });
231
+ if (options.browser) {
232
+ console.log(chalk.yellow('`--browser` is deprecated; use `--engine browser` instead.'));
233
+ }
234
+ const cliModelArg = normalizeModelOption(options.model) || 'gpt-5-pro';
235
+ const resolvedModel = engine === 'browser' ? inferModelFromLabel(cliModelArg) : resolveApiModel(cliModelArg);
236
+ const resolvedOptions = { ...options, model: resolvedModel };
237
+ if (await handleStatusFlag(options, { attachSession, showStatus })) {
238
+ return;
239
+ }
240
+ if (options.session) {
241
+ await attachSession(options.session);
242
+ return;
243
+ }
244
+ if (options.execSession) {
245
+ await executeSession(options.execSession);
246
+ return;
247
+ }
248
+ if (options.renderMarkdown) {
249
+ if (!options.prompt) {
250
+ throw new Error('Prompt is required when using --render-markdown.');
251
+ }
252
+ const markdown = await renderPromptMarkdown({ prompt: options.prompt, file: options.file, system: options.system }, { cwd: process.cwd() });
253
+ console.log(markdown);
254
+ return;
255
+ }
256
+ if (previewMode) {
257
+ if (engine === 'browser') {
258
+ throw new Error('--engine browser cannot be combined with --preview.');
259
+ }
260
+ if (!options.prompt) {
261
+ throw new Error('Prompt is required when using --preview.');
262
+ }
263
+ const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode });
264
+ await runOracle(runOptions, { log: console.log, write: (chunk) => process.stdout.write(chunk) });
265
+ return;
266
+ }
267
+ if (!options.prompt) {
268
+ throw new Error('Prompt is required when starting a new session.');
269
+ }
270
+ if (options.dryRun) {
271
+ const baseRunOptions = buildRunOptions(resolvedOptions, { preview: false, previewMode: undefined });
272
+ await runDryRunSummary({
273
+ engine,
274
+ runOptions: baseRunOptions,
275
+ cwd: process.cwd(),
276
+ version: VERSION,
277
+ log: console.log,
278
+ }, {});
279
+ return;
280
+ }
281
+ if (options.file && options.file.length > 0) {
282
+ await readFiles(options.file, { cwd: process.cwd() });
283
+ }
284
+ const sessionMode = engine === 'browser' ? 'browser' : 'api';
285
+ const browserModelLabelOverride = sessionMode === 'browser' ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
286
+ const browserConfig = sessionMode === 'browser'
287
+ ? buildBrowserConfig({
288
+ ...options,
289
+ model: resolvedModel,
290
+ browserModelLabel: browserModelLabelOverride,
291
+ })
292
+ : undefined;
293
+ await ensureSessionStorage();
294
+ const baseRunOptions = buildRunOptions(resolvedOptions, { preview: false, previewMode: undefined });
295
+ const sessionMeta = await initializeSession({
296
+ ...baseRunOptions,
297
+ mode: sessionMode,
298
+ browserConfig,
299
+ }, process.cwd());
300
+ const reattachCommand = `pnpm oracle session ${sessionMeta.id}`;
301
+ console.log(chalk.bold(`Reattach later with: ${chalk.cyan(reattachCommand)}`));
302
+ console.log('');
303
+ const liveRunOptions = { ...baseRunOptions, sessionId: sessionMeta.id };
304
+ const disableDetach = process.env.ORACLE_NO_DETACH === '1';
305
+ const detached = disableDetach
306
+ ? false
307
+ : await launchDetachedSession(sessionMeta.id).catch((error) => {
308
+ const message = error instanceof Error ? error.message : String(error);
309
+ console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
310
+ return false;
311
+ });
312
+ if (detached === false) {
313
+ await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, true);
314
+ console.log(chalk.bold(`Session ${sessionMeta.id} completed`));
315
+ return;
316
+ }
317
+ if (detached) {
318
+ console.log(chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`));
319
+ await attachSession(sessionMeta.id, { suppressMetadata: true });
320
+ console.log(chalk.bold(`Session ${sessionMeta.id} completed`));
321
+ }
322
+ }
323
+ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true) {
324
+ const { logLine, writeChunk, stream } = createSessionLogWriter(sessionMeta.id);
325
+ let headerAugmented = false;
326
+ const combinedLog = (message = '') => {
327
+ if (!headerAugmented && message.startsWith('Oracle (')) {
328
+ headerAugmented = true;
329
+ if (showReattachHint) {
330
+ console.log(`${message}\n${chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`)}`);
331
+ }
332
+ else {
333
+ console.log(message);
334
+ }
335
+ logLine(message);
336
+ return;
337
+ }
338
+ console.log(message);
339
+ logLine(message);
340
+ };
341
+ const combinedWrite = (chunk) => {
342
+ writeChunk(chunk);
343
+ return process.stdout.write(chunk);
344
+ };
345
+ try {
346
+ await performSessionRun({
347
+ sessionMeta,
348
+ runOptions,
349
+ mode,
350
+ browserConfig,
351
+ cwd: process.cwd(),
352
+ log: combinedLog,
353
+ write: combinedWrite,
354
+ version: VERSION,
355
+ });
356
+ }
357
+ catch (error) {
358
+ throw error;
359
+ }
360
+ finally {
361
+ stream.end();
362
+ }
363
+ }
364
+ async function launchDetachedSession(sessionId) {
365
+ return new Promise((resolve, reject) => {
366
+ try {
367
+ const args = ['--', CLI_ENTRYPOINT, '--exec-session', sessionId];
368
+ const child = spawn(process.execPath, args, {
369
+ detached: true,
370
+ stdio: 'ignore',
371
+ env: process.env,
372
+ });
373
+ child.once('error', reject);
374
+ child.once('spawn', () => {
375
+ child.unref();
376
+ resolve(true);
377
+ });
378
+ }
379
+ catch (error) {
380
+ reject(error);
381
+ }
382
+ });
383
+ }
384
+ async function executeSession(sessionId) {
385
+ const metadata = await readSessionMetadata(sessionId);
386
+ if (!metadata) {
387
+ console.error(chalk.red(`No session found with ID ${sessionId}`));
388
+ process.exitCode = 1;
389
+ return;
390
+ }
391
+ const runOptions = buildRunOptionsFromMetadata(metadata);
392
+ const sessionMode = getSessionMode(metadata);
393
+ const browserConfig = getBrowserConfigFromMetadata(metadata);
394
+ const { logLine, writeChunk, stream } = createSessionLogWriter(sessionId);
395
+ try {
396
+ await performSessionRun({
397
+ sessionMeta: metadata,
398
+ runOptions,
399
+ mode: sessionMode,
400
+ browserConfig,
401
+ cwd: metadata.cwd ?? process.cwd(),
402
+ log: logLine,
403
+ write: writeChunk,
404
+ version: VERSION,
405
+ });
406
+ }
407
+ catch {
408
+ // Errors are already logged to the session log; keep quiet to mirror stored-session behavior.
409
+ }
410
+ finally {
411
+ stream.end();
412
+ }
413
+ }
414
+ function printDebugHelp(cliName) {
415
+ console.log(chalk.bold('Advanced Options'));
416
+ printDebugOptionGroup([
417
+ ['--search <on|off>', 'Enable or disable the server-side search tool (default on).'],
418
+ ['--max-input <tokens>', 'Override the input token budget.'],
419
+ ['--max-output <tokens>', 'Override the max output tokens (model default otherwise).'],
420
+ ]);
421
+ console.log('');
422
+ console.log(chalk.bold('Browser Options'));
423
+ printDebugOptionGroup([
424
+ ['--browser-chrome-profile <name>', 'Reuse cookies from a specific Chrome profile.'],
425
+ ['--browser-chrome-path <path>', 'Point to a custom Chrome/Chromium binary.'],
426
+ ['--browser-url <url>', 'Hit an alternate ChatGPT host.'],
427
+ ['--browser-timeout <ms|s|m>', 'Cap total wait time for the assistant response.'],
428
+ ['--browser-input-timeout <ms|s|m>', 'Cap how long we wait for the composer textarea.'],
429
+ ['--browser-no-cookie-sync', 'Skip copying cookies from your main profile.'],
430
+ ['--browser-headless', 'Launch Chrome in headless mode.'],
431
+ ['--browser-hide-window', 'Hide the Chrome window (macOS headful only).'],
432
+ ['--browser-keep-browser', 'Leave Chrome running after completion.'],
433
+ ]);
434
+ console.log('');
435
+ console.log(chalk.dim(`Tip: run \`${cliName} --help\` to see the primary option set.`));
436
+ }
437
+ function printDebugOptionGroup(entries) {
438
+ const flagWidth = Math.max(...entries.map(([flag]) => flag.length));
439
+ entries.forEach(([flag, description]) => {
440
+ const label = chalk.cyan(flag.padEnd(flagWidth + 2));
441
+ console.log(` ${label}${description}`);
442
+ });
443
+ }
444
+ program.action(async function () {
445
+ const options = this.optsWithGlobals();
446
+ await runRootCommand(options);
447
+ });
448
+ await program.parseAsync(process.argv).catch((error) => {
449
+ if (error instanceof Error) {
450
+ if (!isErrorLogged(error)) {
451
+ console.error(chalk.red('✖'), error.message);
452
+ }
453
+ }
454
+ else {
455
+ console.error(chalk.red('✖'), error);
456
+ }
457
+ process.exitCode = 1;
458
+ });