@steipete/oracle 1.0.8 → 1.1.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.
- package/README.md +3 -0
- package/dist/.DS_Store +0 -0
- package/dist/bin/oracle-cli.js +9 -3
- package/dist/markdansi/types/index.js +4 -0
- package/dist/oracle/bin/oracle-cli.js +472 -0
- package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
- package/dist/oracle/src/browser/actions/attachments.js +82 -0
- package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
- package/dist/oracle/src/browser/actions/navigation.js +75 -0
- package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
- package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
- package/dist/oracle/src/browser/config.js +33 -0
- package/dist/oracle/src/browser/constants.js +40 -0
- package/dist/oracle/src/browser/cookies.js +210 -0
- package/dist/oracle/src/browser/domDebug.js +36 -0
- package/dist/oracle/src/browser/index.js +331 -0
- package/dist/oracle/src/browser/pageActions.js +5 -0
- package/dist/oracle/src/browser/prompt.js +88 -0
- package/dist/oracle/src/browser/promptSummary.js +20 -0
- package/dist/oracle/src/browser/sessionRunner.js +80 -0
- package/dist/oracle/src/browser/types.js +1 -0
- package/dist/oracle/src/browser/utils.js +62 -0
- package/dist/oracle/src/browserMode.js +1 -0
- package/dist/oracle/src/cli/browserConfig.js +44 -0
- package/dist/oracle/src/cli/dryRun.js +59 -0
- package/dist/oracle/src/cli/engine.js +17 -0
- package/dist/oracle/src/cli/errorUtils.js +9 -0
- package/dist/oracle/src/cli/help.js +70 -0
- package/dist/oracle/src/cli/markdownRenderer.js +15 -0
- package/dist/oracle/src/cli/options.js +103 -0
- package/dist/oracle/src/cli/promptRequirement.js +14 -0
- package/dist/oracle/src/cli/rootAlias.js +30 -0
- package/dist/oracle/src/cli/sessionCommand.js +77 -0
- package/dist/oracle/src/cli/sessionDisplay.js +270 -0
- package/dist/oracle/src/cli/sessionRunner.js +94 -0
- package/dist/oracle/src/heartbeat.js +43 -0
- package/dist/oracle/src/oracle/client.js +48 -0
- package/dist/oracle/src/oracle/config.js +29 -0
- package/dist/oracle/src/oracle/errors.js +101 -0
- package/dist/oracle/src/oracle/files.js +220 -0
- package/dist/oracle/src/oracle/format.js +33 -0
- package/dist/oracle/src/oracle/fsAdapter.js +7 -0
- package/dist/oracle/src/oracle/oscProgress.js +60 -0
- package/dist/oracle/src/oracle/request.js +48 -0
- package/dist/oracle/src/oracle/run.js +444 -0
- package/dist/oracle/src/oracle/tokenStats.js +39 -0
- package/dist/oracle/src/oracle/types.js +1 -0
- package/dist/oracle/src/oracle.js +9 -0
- package/dist/oracle/src/sessionManager.js +205 -0
- package/dist/oracle/src/version.js +39 -0
- package/dist/src/cli/markdownRenderer.js +18 -0
- package/dist/src/cli/rootAlias.js +14 -0
- package/dist/src/cli/sessionCommand.js +60 -2
- package/dist/src/cli/sessionDisplay.js +129 -4
- package/dist/src/oracle/oscProgress.js +60 -0
- package/dist/src/oracle/run.js +63 -51
- package/dist/src/sessionManager.js +17 -0
- package/package.json +14 -22
package/README.md
CHANGED
|
@@ -74,6 +74,9 @@ More knobs (`--max-input`, cookie sync controls for browser mode, etc.) live beh
|
|
|
74
74
|
## Sessions & background runs
|
|
75
75
|
|
|
76
76
|
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.
|
|
77
|
+
Add `--render` (alias `--render-markdown`) when attaching to pretty-print the stored markdown if your terminal supports color; falls back to raw text otherwise.
|
|
78
|
+
|
|
79
|
+
**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.
|
|
77
80
|
|
|
78
81
|
## Testing
|
|
79
82
|
|
package/dist/.DS_Store
ADDED
|
Binary file
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { performSessionRun } from '../src/cli/sessionRunner.js';
|
|
|
16
16
|
import { attachSession, showStatus } from '../src/cli/sessionDisplay.js';
|
|
17
17
|
import { handleSessionCommand, formatSessionCleanupMessage } from '../src/cli/sessionCommand.js';
|
|
18
18
|
import { isErrorLogged } from '../src/cli/errorUtils.js';
|
|
19
|
-
import { handleStatusFlag } from '../src/cli/rootAlias.js';
|
|
19
|
+
import { handleSessionAlias, handleStatusFlag } from '../src/cli/rootAlias.js';
|
|
20
20
|
import { getCliVersion } from '../src/version.js';
|
|
21
21
|
import { runDryRunSummary } from '../src/cli/dryRun.js';
|
|
22
22
|
const VERSION = getCliVersion();
|
|
@@ -62,8 +62,10 @@ program
|
|
|
62
62
|
.choices(['summary', 'json', 'full'])
|
|
63
63
|
.preset('summary'))
|
|
64
64
|
.addOption(new Option('--exec-session <id>').hideHelp())
|
|
65
|
+
.addOption(new Option('--session <id>').hideHelp())
|
|
65
66
|
.addOption(new Option('--status', 'Show stored sessions (alias for `oracle status`).').default(false).hideHelp())
|
|
66
67
|
.option('--render-markdown', 'Emit the assembled markdown bundle for prompt + files and exit.', false)
|
|
68
|
+
.option('--verbose-render', 'Show render/TTY diagnostics when replaying sessions.', false)
|
|
67
69
|
.addOption(new Option('--search <mode>', 'Set server-side search behavior (on/off).')
|
|
68
70
|
.argParser(parseSearchOption)
|
|
69
71
|
.hideHelp())
|
|
@@ -104,6 +106,9 @@ const sessionCommand = program
|
|
|
104
106
|
.option('--limit <count>', 'Maximum sessions to show when listing (max 1000).', parseIntOption, 100)
|
|
105
107
|
.option('--all', 'Include all stored sessions regardless of age.', false)
|
|
106
108
|
.option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
|
|
109
|
+
.option('--render', 'Render completed session output as markdown (rich TTY only).', false)
|
|
110
|
+
.option('--render-markdown', 'Alias for --render.', false)
|
|
111
|
+
.option('--path', 'Print the stored session paths instead of attaching.', false)
|
|
107
112
|
.addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
|
|
108
113
|
.action(async (sessionId, _options, cmd) => {
|
|
109
114
|
await handleSessionCommand(sessionId, cmd);
|
|
@@ -115,6 +120,8 @@ const statusCommand = program
|
|
|
115
120
|
.option('--limit <count>', 'Maximum sessions to show (max 1000).', parseIntOption, 100)
|
|
116
121
|
.option('--all', 'Include all stored sessions regardless of age.', false)
|
|
117
122
|
.option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
|
|
123
|
+
.option('--render', 'Render completed session output as markdown (rich TTY only).', false)
|
|
124
|
+
.option('--render-markdown', 'Alias for --render.', false)
|
|
118
125
|
.addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
|
|
119
126
|
.action(async (sessionId, _options, command) => {
|
|
120
127
|
const statusOptions = command.opts();
|
|
@@ -246,8 +253,7 @@ async function runRootCommand(options) {
|
|
|
246
253
|
if (await handleStatusFlag(options, { attachSession, showStatus })) {
|
|
247
254
|
return;
|
|
248
255
|
}
|
|
249
|
-
if (options
|
|
250
|
-
await attachSession(options.session);
|
|
256
|
+
if (await handleSessionAlias(options, { attachSession })) {
|
|
251
257
|
return;
|
|
252
258
|
}
|
|
253
259
|
if (options.execSession) {
|
|
@@ -0,0 +1,472 @@
|
|
|
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 { handleSessionAlias, 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
|
+
const positional = thisCommand.args?.[0];
|
|
37
|
+
if (!opts.prompt && positional) {
|
|
38
|
+
opts.prompt = positional;
|
|
39
|
+
thisCommand.setOptionValue('prompt', positional);
|
|
40
|
+
}
|
|
41
|
+
if (shouldRequirePrompt(rawCliArgs, opts)) {
|
|
42
|
+
console.log(chalk.yellow('Prompt is required. Provide it via --prompt "<text>" or positional [prompt].'));
|
|
43
|
+
thisCommand.help({ error: false });
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
program
|
|
49
|
+
.name('oracle')
|
|
50
|
+
.description('One-shot GPT-5 Pro / GPT-5.1 tool for hard questions that benefit from large file context and server-side search.')
|
|
51
|
+
.version(VERSION)
|
|
52
|
+
.argument('[prompt]', 'Prompt text (shorthand for --prompt).')
|
|
53
|
+
.option('-p, --prompt <text>', 'User prompt to send to the model.')
|
|
54
|
+
.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, [])
|
|
55
|
+
.option('-s, --slug <words>', 'Custom session slug (3-5 words).')
|
|
56
|
+
.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']))
|
|
58
|
+
.option('--files-report', 'Show token usage per attached file (also prints automatically when files exceed the token budget).', false)
|
|
59
|
+
.option('-v, --verbose', 'Enable verbose logging for all operations.', false)
|
|
60
|
+
.option('--dry-run', 'Validate inputs and show token estimates without calling the model.', false)
|
|
61
|
+
.addOption(new Option('--preview [mode]', 'Preview the request without calling the API (summary | json | full).')
|
|
62
|
+
.choices(['summary', 'json', 'full'])
|
|
63
|
+
.preset('summary'))
|
|
64
|
+
.addOption(new Option('--exec-session <id>').hideHelp())
|
|
65
|
+
.addOption(new Option('--session <id>').hideHelp())
|
|
66
|
+
.addOption(new Option('--status', 'Show stored sessions (alias for `oracle status`).').default(false).hideHelp())
|
|
67
|
+
.option('--render-markdown', 'Emit the assembled markdown bundle for prompt + files and exit.', false)
|
|
68
|
+
.option('--verbose-render', 'Show render/TTY diagnostics when replaying sessions.', false)
|
|
69
|
+
.addOption(new Option('--search <mode>', 'Set server-side search behavior (on/off).')
|
|
70
|
+
.argParser(parseSearchOption)
|
|
71
|
+
.hideHelp())
|
|
72
|
+
.addOption(new Option('--max-input <tokens>', 'Override the input token budget for the selected model.')
|
|
73
|
+
.argParser(parseIntOption)
|
|
74
|
+
.hideHelp())
|
|
75
|
+
.addOption(new Option('--max-output <tokens>', 'Override the max output tokens for the selected model.')
|
|
76
|
+
.argParser(parseIntOption)
|
|
77
|
+
.hideHelp())
|
|
78
|
+
.addOption(new Option('--browser', '(deprecated) Use --engine browser instead.').default(false).hideHelp())
|
|
79
|
+
.addOption(new Option('--browser-chrome-profile <name>', 'Chrome profile name/path for cookie reuse.').hideHelp())
|
|
80
|
+
.addOption(new Option('--browser-chrome-path <path>', 'Explicit Chrome or Chromium executable path.').hideHelp())
|
|
81
|
+
.addOption(new Option('--browser-url <url>', `Override the ChatGPT URL (default ${CHATGPT_URL}).`).hideHelp())
|
|
82
|
+
.addOption(new Option('--browser-timeout <ms|s|m>', 'Maximum time to wait for an answer (default 900s).').hideHelp())
|
|
83
|
+
.addOption(new Option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 30s).').hideHelp())
|
|
84
|
+
.addOption(new Option('--browser-no-cookie-sync', 'Skip copying cookies from Chrome.').hideHelp())
|
|
85
|
+
.addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
|
|
86
|
+
.addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
|
|
87
|
+
.addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
|
|
88
|
+
.addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
|
|
89
|
+
.addOption(new Option('--browser-inline-files', 'Paste files directly into the ChatGPT composer instead of uploading attachments.').default(false))
|
|
90
|
+
.option('--debug-help', 'Show the advanced/debug option set and exit.', false)
|
|
91
|
+
.option('--heartbeat <seconds>', 'Emit periodic in-progress updates (0 to disable).', parseHeartbeatOption, 30)
|
|
92
|
+
.showHelpAfterError('(use --help for usage)');
|
|
93
|
+
program.addHelpText('after', `
|
|
94
|
+
Examples:
|
|
95
|
+
# Quick API run with two files
|
|
96
|
+
oracle --prompt "Summarize the risk register" --file docs/risk-register.md docs/risk-matrix.md
|
|
97
|
+
|
|
98
|
+
# Browser run (no API key) + globbed TypeScript sources, excluding tests
|
|
99
|
+
oracle --engine browser --prompt "Review the TS data layer" \\
|
|
100
|
+
--file "src/**/*.ts" --file "!src/**/*.test.ts"
|
|
101
|
+
`);
|
|
102
|
+
const sessionCommand = program
|
|
103
|
+
.command('session [id]')
|
|
104
|
+
.description('Attach to a stored session or list recent sessions when no ID is provided.')
|
|
105
|
+
.option('--hours <hours>', 'Look back this many hours when listing sessions (default 24).', parseFloatOption, 24)
|
|
106
|
+
.option('--limit <count>', 'Maximum sessions to show when listing (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
|
+
.option('--render', 'Render completed session output as markdown (rich TTY only).', false)
|
|
110
|
+
.option('--render-markdown', 'Alias for --render.', false)
|
|
111
|
+
.addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
|
|
112
|
+
.action(async (sessionId, _options, cmd) => {
|
|
113
|
+
await handleSessionCommand(sessionId, cmd);
|
|
114
|
+
});
|
|
115
|
+
const statusCommand = program
|
|
116
|
+
.command('status [id]')
|
|
117
|
+
.description('List recent sessions (24h window by default) or attach to a session when an ID is provided.')
|
|
118
|
+
.option('--hours <hours>', 'Look back this many hours (default 24).', parseFloatOption, 24)
|
|
119
|
+
.option('--limit <count>', 'Maximum sessions to show (max 1000).', parseIntOption, 100)
|
|
120
|
+
.option('--all', 'Include all stored sessions regardless of age.', false)
|
|
121
|
+
.option('--clear', 'Delete stored sessions older than the provided window (24h default).', false)
|
|
122
|
+
.option('--render', 'Render completed session output as markdown (rich TTY only).', false)
|
|
123
|
+
.option('--render-markdown', 'Alias for --render.', false)
|
|
124
|
+
.addOption(new Option('--clean', 'Deprecated alias for --clear.').default(false).hideHelp())
|
|
125
|
+
.action(async (sessionId, _options, command) => {
|
|
126
|
+
const statusOptions = command.opts();
|
|
127
|
+
const clearRequested = Boolean(statusOptions.clear || statusOptions.clean);
|
|
128
|
+
if (clearRequested) {
|
|
129
|
+
if (sessionId) {
|
|
130
|
+
console.error('Cannot combine a session ID with --clear. Remove the ID to delete cached sessions.');
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const hours = statusOptions.hours;
|
|
135
|
+
const includeAll = statusOptions.all;
|
|
136
|
+
const result = await deleteSessionsOlderThan({ hours, includeAll });
|
|
137
|
+
const scope = includeAll ? 'all stored sessions' : `sessions older than ${hours}h`;
|
|
138
|
+
console.log(formatSessionCleanupMessage(result, scope));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (sessionId === 'clear' || sessionId === 'clean') {
|
|
142
|
+
console.error('Session cleanup now uses --clear. Run "oracle status --clear --hours <n>" instead.');
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (sessionId) {
|
|
147
|
+
await attachSession(sessionId);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const showExamples = usesDefaultStatusFilters(command);
|
|
151
|
+
await showStatus({
|
|
152
|
+
hours: statusOptions.all ? Infinity : statusOptions.hours,
|
|
153
|
+
includeAll: statusOptions.all,
|
|
154
|
+
limit: statusOptions.limit,
|
|
155
|
+
showExamples,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
function buildRunOptions(options, overrides = {}) {
|
|
159
|
+
if (!options.prompt) {
|
|
160
|
+
throw new Error('Prompt is required.');
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
prompt: options.prompt,
|
|
164
|
+
model: options.model,
|
|
165
|
+
file: overrides.file ?? options.file ?? [],
|
|
166
|
+
slug: overrides.slug ?? options.slug,
|
|
167
|
+
filesReport: overrides.filesReport ?? options.filesReport,
|
|
168
|
+
maxInput: overrides.maxInput ?? options.maxInput,
|
|
169
|
+
maxOutput: overrides.maxOutput ?? options.maxOutput,
|
|
170
|
+
system: overrides.system ?? options.system,
|
|
171
|
+
silent: overrides.silent ?? options.silent,
|
|
172
|
+
search: overrides.search ?? options.search,
|
|
173
|
+
preview: overrides.preview ?? undefined,
|
|
174
|
+
previewMode: overrides.previewMode ?? options.previewMode,
|
|
175
|
+
apiKey: overrides.apiKey ?? options.apiKey,
|
|
176
|
+
sessionId: overrides.sessionId ?? options.sessionId,
|
|
177
|
+
verbose: overrides.verbose ?? options.verbose,
|
|
178
|
+
heartbeatIntervalMs: overrides.heartbeatIntervalMs ?? resolveHeartbeatIntervalMs(options.heartbeat),
|
|
179
|
+
browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
|
|
180
|
+
background: overrides.background ?? undefined,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function resolveHeartbeatIntervalMs(seconds) {
|
|
184
|
+
if (typeof seconds !== 'number' || seconds <= 0) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
return Math.round(seconds * 1000);
|
|
188
|
+
}
|
|
189
|
+
function buildRunOptionsFromMetadata(metadata) {
|
|
190
|
+
const stored = metadata.options ?? {};
|
|
191
|
+
return {
|
|
192
|
+
prompt: stored.prompt ?? '',
|
|
193
|
+
model: stored.model ?? 'gpt-5-pro',
|
|
194
|
+
file: stored.file ?? [],
|
|
195
|
+
slug: stored.slug,
|
|
196
|
+
filesReport: stored.filesReport,
|
|
197
|
+
maxInput: stored.maxInput,
|
|
198
|
+
maxOutput: stored.maxOutput,
|
|
199
|
+
system: stored.system,
|
|
200
|
+
silent: stored.silent,
|
|
201
|
+
search: undefined,
|
|
202
|
+
preview: false,
|
|
203
|
+
previewMode: undefined,
|
|
204
|
+
apiKey: undefined,
|
|
205
|
+
sessionId: metadata.id,
|
|
206
|
+
verbose: stored.verbose,
|
|
207
|
+
heartbeatIntervalMs: stored.heartbeatIntervalMs,
|
|
208
|
+
browserInlineFiles: stored.browserInlineFiles,
|
|
209
|
+
background: stored.background,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function getSessionMode(metadata) {
|
|
213
|
+
return metadata.mode ?? metadata.options?.mode ?? 'api';
|
|
214
|
+
}
|
|
215
|
+
function getBrowserConfigFromMetadata(metadata) {
|
|
216
|
+
return metadata.options?.browserConfig ?? metadata.browser?.config;
|
|
217
|
+
}
|
|
218
|
+
async function runRootCommand(options) {
|
|
219
|
+
const helpRequested = rawCliArgs.some((arg) => arg === '--help' || arg === '-h');
|
|
220
|
+
if (helpRequested) {
|
|
221
|
+
if (options.verbose) {
|
|
222
|
+
console.log('');
|
|
223
|
+
printDebugHelp(program.name());
|
|
224
|
+
console.log('');
|
|
225
|
+
}
|
|
226
|
+
program.help({ error: false });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const previewMode = resolvePreviewMode(options.preview);
|
|
230
|
+
if (rawCliArgs.length === 0) {
|
|
231
|
+
console.log(chalk.yellow('No prompt or subcommand supplied. See `oracle --help` for usage.'));
|
|
232
|
+
program.help({ error: false });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (options.debugHelp) {
|
|
236
|
+
printDebugHelp(program.name());
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (options.dryRun && previewMode) {
|
|
240
|
+
throw new Error('--dry-run cannot be combined with --preview.');
|
|
241
|
+
}
|
|
242
|
+
if (options.dryRun && options.renderMarkdown) {
|
|
243
|
+
throw new Error('--dry-run cannot be combined with --render-markdown.');
|
|
244
|
+
}
|
|
245
|
+
const engine = resolveEngine({ engine: options.engine, browserFlag: options.browser, env: process.env });
|
|
246
|
+
if (options.browser) {
|
|
247
|
+
console.log(chalk.yellow('`--browser` is deprecated; use `--engine browser` instead.'));
|
|
248
|
+
}
|
|
249
|
+
const cliModelArg = normalizeModelOption(options.model) || 'gpt-5-pro';
|
|
250
|
+
const resolvedModel = engine === 'browser' ? inferModelFromLabel(cliModelArg) : resolveApiModel(cliModelArg);
|
|
251
|
+
const resolvedOptions = { ...options, model: resolvedModel };
|
|
252
|
+
if (await handleStatusFlag(options, { attachSession, showStatus })) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (await handleSessionAlias(options, { attachSession })) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (options.execSession) {
|
|
259
|
+
await executeSession(options.execSession);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (options.renderMarkdown) {
|
|
263
|
+
if (!options.prompt) {
|
|
264
|
+
throw new Error('Prompt is required when using --render-markdown.');
|
|
265
|
+
}
|
|
266
|
+
const markdown = await renderPromptMarkdown({ prompt: options.prompt, file: options.file, system: options.system }, { cwd: process.cwd() });
|
|
267
|
+
console.log(markdown);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (previewMode) {
|
|
271
|
+
if (engine === 'browser') {
|
|
272
|
+
throw new Error('--engine browser cannot be combined with --preview.');
|
|
273
|
+
}
|
|
274
|
+
if (!options.prompt) {
|
|
275
|
+
throw new Error('Prompt is required when using --preview.');
|
|
276
|
+
}
|
|
277
|
+
const runOptions = buildRunOptions(resolvedOptions, { preview: true, previewMode });
|
|
278
|
+
await runOracle(runOptions, { log: console.log, write: (chunk) => process.stdout.write(chunk) });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!options.prompt) {
|
|
282
|
+
throw new Error('Prompt is required when starting a new session.');
|
|
283
|
+
}
|
|
284
|
+
if (options.dryRun) {
|
|
285
|
+
const baseRunOptions = buildRunOptions(resolvedOptions, { preview: false, previewMode: undefined });
|
|
286
|
+
await runDryRunSummary({
|
|
287
|
+
engine,
|
|
288
|
+
runOptions: baseRunOptions,
|
|
289
|
+
cwd: process.cwd(),
|
|
290
|
+
version: VERSION,
|
|
291
|
+
log: console.log,
|
|
292
|
+
}, {});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (options.file && options.file.length > 0) {
|
|
296
|
+
await readFiles(options.file, { cwd: process.cwd() });
|
|
297
|
+
}
|
|
298
|
+
const sessionMode = engine === 'browser' ? 'browser' : 'api';
|
|
299
|
+
const browserModelLabelOverride = sessionMode === 'browser' ? resolveBrowserModelLabel(cliModelArg, resolvedModel) : undefined;
|
|
300
|
+
const browserConfig = sessionMode === 'browser'
|
|
301
|
+
? buildBrowserConfig({
|
|
302
|
+
...options,
|
|
303
|
+
model: resolvedModel,
|
|
304
|
+
browserModelLabel: browserModelLabelOverride,
|
|
305
|
+
})
|
|
306
|
+
: undefined;
|
|
307
|
+
await ensureSessionStorage();
|
|
308
|
+
const baseRunOptions = buildRunOptions(resolvedOptions, { preview: false, previewMode: undefined });
|
|
309
|
+
const sessionMeta = await initializeSession({
|
|
310
|
+
...baseRunOptions,
|
|
311
|
+
mode: sessionMode,
|
|
312
|
+
browserConfig,
|
|
313
|
+
}, process.cwd());
|
|
314
|
+
const reattachCommand = `pnpm oracle session ${sessionMeta.id}`;
|
|
315
|
+
console.log(chalk.bold(`Reattach later with: ${chalk.cyan(reattachCommand)}`));
|
|
316
|
+
console.log('');
|
|
317
|
+
const liveRunOptions = { ...baseRunOptions, sessionId: sessionMeta.id };
|
|
318
|
+
const disableDetach = process.env.ORACLE_NO_DETACH === '1';
|
|
319
|
+
const detached = disableDetach
|
|
320
|
+
? false
|
|
321
|
+
: await launchDetachedSession(sessionMeta.id).catch((error) => {
|
|
322
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
323
|
+
console.log(chalk.yellow(`Unable to detach session runner (${message}). Running inline...`));
|
|
324
|
+
return false;
|
|
325
|
+
});
|
|
326
|
+
if (detached === false) {
|
|
327
|
+
await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig, true);
|
|
328
|
+
console.log(chalk.bold(`Session ${sessionMeta.id} completed`));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (detached) {
|
|
332
|
+
console.log(chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`));
|
|
333
|
+
await attachSession(sessionMeta.id, { suppressMetadata: true });
|
|
334
|
+
console.log(chalk.bold(`Session ${sessionMeta.id} completed`));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig, showReattachHint = true) {
|
|
338
|
+
const { logLine, writeChunk, stream } = createSessionLogWriter(sessionMeta.id);
|
|
339
|
+
let headerAugmented = false;
|
|
340
|
+
const combinedLog = (message = '') => {
|
|
341
|
+
if (!headerAugmented && message.startsWith('Oracle (')) {
|
|
342
|
+
headerAugmented = true;
|
|
343
|
+
if (showReattachHint) {
|
|
344
|
+
console.log(`${message}\n${chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`)}`);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
console.log(message);
|
|
348
|
+
}
|
|
349
|
+
logLine(message);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
console.log(message);
|
|
353
|
+
logLine(message);
|
|
354
|
+
};
|
|
355
|
+
const combinedWrite = (chunk) => {
|
|
356
|
+
writeChunk(chunk);
|
|
357
|
+
return process.stdout.write(chunk);
|
|
358
|
+
};
|
|
359
|
+
try {
|
|
360
|
+
await performSessionRun({
|
|
361
|
+
sessionMeta,
|
|
362
|
+
runOptions,
|
|
363
|
+
mode,
|
|
364
|
+
browserConfig,
|
|
365
|
+
cwd: process.cwd(),
|
|
366
|
+
log: combinedLog,
|
|
367
|
+
write: combinedWrite,
|
|
368
|
+
version: VERSION,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
stream.end();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async function launchDetachedSession(sessionId) {
|
|
379
|
+
return new Promise((resolve, reject) => {
|
|
380
|
+
try {
|
|
381
|
+
const args = ['--', CLI_ENTRYPOINT, '--exec-session', sessionId];
|
|
382
|
+
const child = spawn(process.execPath, args, {
|
|
383
|
+
detached: true,
|
|
384
|
+
stdio: 'ignore',
|
|
385
|
+
env: process.env,
|
|
386
|
+
});
|
|
387
|
+
child.once('error', reject);
|
|
388
|
+
child.once('spawn', () => {
|
|
389
|
+
child.unref();
|
|
390
|
+
resolve(true);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
reject(error);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
async function executeSession(sessionId) {
|
|
399
|
+
const metadata = await readSessionMetadata(sessionId);
|
|
400
|
+
if (!metadata) {
|
|
401
|
+
console.error(chalk.red(`No session found with ID ${sessionId}`));
|
|
402
|
+
process.exitCode = 1;
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const runOptions = buildRunOptionsFromMetadata(metadata);
|
|
406
|
+
const sessionMode = getSessionMode(metadata);
|
|
407
|
+
const browserConfig = getBrowserConfigFromMetadata(metadata);
|
|
408
|
+
const { logLine, writeChunk, stream } = createSessionLogWriter(sessionId);
|
|
409
|
+
try {
|
|
410
|
+
await performSessionRun({
|
|
411
|
+
sessionMeta: metadata,
|
|
412
|
+
runOptions,
|
|
413
|
+
mode: sessionMode,
|
|
414
|
+
browserConfig,
|
|
415
|
+
cwd: metadata.cwd ?? process.cwd(),
|
|
416
|
+
log: logLine,
|
|
417
|
+
write: writeChunk,
|
|
418
|
+
version: VERSION,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// Errors are already logged to the session log; keep quiet to mirror stored-session behavior.
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
stream.end();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function printDebugHelp(cliName) {
|
|
429
|
+
console.log(chalk.bold('Advanced Options'));
|
|
430
|
+
printDebugOptionGroup([
|
|
431
|
+
['--search <on|off>', 'Enable or disable the server-side search tool (default on).'],
|
|
432
|
+
['--max-input <tokens>', 'Override the input token budget.'],
|
|
433
|
+
['--max-output <tokens>', 'Override the max output tokens (model default otherwise).'],
|
|
434
|
+
]);
|
|
435
|
+
console.log('');
|
|
436
|
+
console.log(chalk.bold('Browser Options'));
|
|
437
|
+
printDebugOptionGroup([
|
|
438
|
+
['--browser-chrome-profile <name>', 'Reuse cookies from a specific Chrome profile.'],
|
|
439
|
+
['--browser-chrome-path <path>', 'Point to a custom Chrome/Chromium binary.'],
|
|
440
|
+
['--browser-url <url>', 'Hit an alternate ChatGPT host.'],
|
|
441
|
+
['--browser-timeout <ms|s|m>', 'Cap total wait time for the assistant response.'],
|
|
442
|
+
['--browser-input-timeout <ms|s|m>', 'Cap how long we wait for the composer textarea.'],
|
|
443
|
+
['--browser-no-cookie-sync', 'Skip copying cookies from your main profile.'],
|
|
444
|
+
['--browser-headless', 'Launch Chrome in headless mode.'],
|
|
445
|
+
['--browser-hide-window', 'Hide the Chrome window (macOS headful only).'],
|
|
446
|
+
['--browser-keep-browser', 'Leave Chrome running after completion.'],
|
|
447
|
+
]);
|
|
448
|
+
console.log('');
|
|
449
|
+
console.log(chalk.dim(`Tip: run \`${cliName} --help\` to see the primary option set.`));
|
|
450
|
+
}
|
|
451
|
+
function printDebugOptionGroup(entries) {
|
|
452
|
+
const flagWidth = Math.max(...entries.map(([flag]) => flag.length));
|
|
453
|
+
entries.forEach(([flag, description]) => {
|
|
454
|
+
const label = chalk.cyan(flag.padEnd(flagWidth + 2));
|
|
455
|
+
console.log(` ${label}${description}`);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
program.action(async function () {
|
|
459
|
+
const options = this.optsWithGlobals();
|
|
460
|
+
await runRootCommand(options);
|
|
461
|
+
});
|
|
462
|
+
await program.parseAsync(process.argv).catch((error) => {
|
|
463
|
+
if (error instanceof Error) {
|
|
464
|
+
if (!isErrorLogged(error)) {
|
|
465
|
+
console.error(chalk.red('✖'), error.message);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
console.error(chalk.red('✖'), error);
|
|
470
|
+
}
|
|
471
|
+
process.exitCode = 1;
|
|
472
|
+
});
|