@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
@@ -0,0 +1,683 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import { Command, InvalidArgumentError, Option } from 'commander';
4
+ import chalk from 'chalk';
5
+ import kleur from 'kleur';
6
+ import { ensureSessionStorage, initializeSession, updateSessionMetadata, readSessionMetadata, listSessionsMetadata, filterSessionsByRange, createSessionLogWriter, readSessionLog, wait, SESSIONS_DIR, deleteSessionsOlderThan, } from '../src/sessionManager.js';
7
+ import { runOracle, MODEL_CONFIGS, parseIntOption, renderPromptMarkdown, readFiles, buildPrompt, createFileSections, DEFAULT_SYSTEM_PROMPT, formatElapsed, TOKENIZER_OPTIONS, OracleResponseError, extractResponseMetadata, } from '../src/oracle.js';
8
+ import { runBrowserMode, CHATGPT_URL, DEFAULT_MODEL_TARGET, parseDuration } from '../src/browserMode.js';
9
+ const VERSION = '1.0.0';
10
+ const rawCliArgs = process.argv.slice(2);
11
+ const isTty = process.stdout.isTTY;
12
+ const colorIfTty = (styler) => (text) => (isTty ? styler(text) : text);
13
+ const helpColors = {
14
+ banner: colorIfTty((text) => kleur.bold().blue(text)),
15
+ subtitle: colorIfTty((text) => kleur.dim(text)),
16
+ section: colorIfTty((text) => kleur.bold().white(text)),
17
+ bullet: colorIfTty((text) => kleur.blue(text)),
18
+ command: colorIfTty((text) => kleur.bold().blue(text)),
19
+ option: colorIfTty((text) => kleur.cyan(text)),
20
+ argument: colorIfTty((text) => kleur.magenta(text)),
21
+ description: colorIfTty((text) => kleur.white(text)),
22
+ muted: colorIfTty((text) => kleur.gray(text)),
23
+ accent: colorIfTty((text) => kleur.cyan(text)),
24
+ };
25
+ const program = new Command();
26
+ program.configureHelp({
27
+ styleTitle(title) {
28
+ return helpColors.section(title);
29
+ },
30
+ styleDescriptionText(text) {
31
+ return helpColors.description(text);
32
+ },
33
+ styleCommandText(text) {
34
+ return helpColors.command(text);
35
+ },
36
+ styleSubcommandText(text) {
37
+ return helpColors.command(text);
38
+ },
39
+ styleOptionText(text) {
40
+ return helpColors.option(text);
41
+ },
42
+ styleArgumentText(text) {
43
+ return helpColors.argument(text);
44
+ },
45
+ });
46
+ program
47
+ .name('oracle')
48
+ .description('One-shot GPT-5 Pro / GPT-5.1 tool for hard questions that benefit from large file context and server-side search.')
49
+ .version(VERSION)
50
+ .option('-p, --prompt <text>', 'User prompt to send to the model.')
51
+ .option('-f, --file <paths...>', 'Paths to files or directories to append to the prompt; repeat, comma-separate, or supply a space-separated list.', collectPaths, [])
52
+ .option('-s, --slug <words>', 'Custom session slug (3-5 words).')
53
+ .option('-m, --model <model>', 'Model to target (gpt-5-pro | gpt-5.1).', validateModel, 'gpt-5-pro')
54
+ .option('--files-report', 'Show token usage per attached file (also prints automatically when files exceed the token budget).', false)
55
+ .option('-v, --verbose', 'Enable verbose logging for all operations.', false)
56
+ .addOption(new Option('--preview [mode]', 'Preview the request without calling the API (summary | json | full).')
57
+ .choices(['summary', 'json', 'full'])
58
+ .preset('summary'))
59
+ .addOption(new Option('--exec-session <id>').hideHelp())
60
+ .option('--render-markdown', 'Emit the assembled markdown bundle for prompt + files and exit.', false)
61
+ .option('--browser', 'Run the prompt via the ChatGPT web UI (Chrome automation).', false)
62
+ .option('--browser-chrome-profile <name>', 'Chrome profile name/path for cookie reuse.')
63
+ .option('--browser-chrome-path <path>', 'Explicit Chrome or Chromium executable path.')
64
+ .option('--browser-url <url>', `Override the ChatGPT URL (default ${CHATGPT_URL}).`)
65
+ .option('--browser-timeout <ms|s|m>', 'Maximum time to wait for an answer (default 900s).')
66
+ .option('--browser-input-timeout <ms|s|m>', 'Maximum time to wait for the prompt textarea (default 30s).')
67
+ .option('--browser-no-cookie-sync', 'Skip copying cookies from Chrome.', false)
68
+ .option('--browser-headless', 'Launch Chrome in headless mode.', false)
69
+ .option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).', false)
70
+ .option('--browser-keep-browser', 'Keep Chrome running after completion.', false)
71
+ .showHelpAfterError('(use --help for usage)');
72
+ program
73
+ .command('session [id]')
74
+ .description('Attach to a stored session or list recent sessions when no ID is provided.')
75
+ .option('--hours <hours>', 'Look back this many hours when listing sessions (default 24).', parseFloatOption, 24)
76
+ .option('--limit <count>', 'Maximum sessions to show when listing (max 1000).', parseIntOption, 100)
77
+ .option('--all', 'Include all stored sessions regardless of age.', false)
78
+ .action(async (sessionId, cmd) => {
79
+ const sessionOptions = cmd.opts();
80
+ if (!sessionId) {
81
+ const showExamples = usesDefaultStatusFilters(cmd);
82
+ await showStatus({
83
+ hours: sessionOptions.all ? Infinity : sessionOptions.hours,
84
+ includeAll: sessionOptions.all,
85
+ limit: sessionOptions.limit,
86
+ showExamples,
87
+ });
88
+ return;
89
+ }
90
+ await attachSession(sessionId);
91
+ });
92
+ const statusCommand = program
93
+ .command('status')
94
+ .description('List recent sessions (24h window by default).')
95
+ .option('--hours <hours>', 'Look back this many hours (default 24).', parseFloatOption, 24)
96
+ .option('--limit <count>', 'Maximum sessions to show (max 1000).', parseIntOption, 100)
97
+ .option('--all', 'Include all stored sessions regardless of age.', false)
98
+ .action(async (_options, command) => {
99
+ const statusOptions = command.opts();
100
+ const showExamples = usesDefaultStatusFilters(command);
101
+ await showStatus({
102
+ hours: statusOptions.all ? Infinity : statusOptions.hours,
103
+ includeAll: statusOptions.all,
104
+ limit: statusOptions.limit,
105
+ showExamples,
106
+ });
107
+ });
108
+ statusCommand
109
+ .command('clear')
110
+ .description('Delete stored sessions older than the provided window (24h default).')
111
+ .option('--hours <hours>', 'Delete sessions older than this many hours (default 24).', parseFloatOption, 24)
112
+ .option('--all', 'Delete all stored sessions.', false)
113
+ .action(async (_options, command) => {
114
+ const clearOptions = command.opts();
115
+ const result = await deleteSessionsOlderThan({ hours: clearOptions.hours, includeAll: clearOptions.all });
116
+ const scope = clearOptions.all ? 'all stored sessions' : `sessions older than ${clearOptions.hours}h`;
117
+ console.log(`Deleted ${result.deleted} ${result.deleted === 1 ? 'session' : 'sessions'} (${scope}).`);
118
+ });
119
+ const bold = (text) => (isTty ? kleur.bold(text) : text);
120
+ const dim = (text) => (isTty ? kleur.dim(text) : text);
121
+ program.addHelpText('beforeAll', renderHelpBanner);
122
+ program.addHelpText('after', renderHelpFooter);
123
+ function renderHelpBanner() {
124
+ const subtitle = 'GPT-5 Pro/GPT-5.1 for tough questions with code/file context.';
125
+ return `${helpColors.banner(`Oracle CLI v${VERSION}`)} ${helpColors.subtitle(`— ${subtitle}`)}\n`;
126
+ }
127
+ function renderHelpFooter() {
128
+ const tips = [
129
+ `${helpColors.bullet('•')} Attach source files for best results, but keep total input under ~196k tokens.`,
130
+ `${helpColors.bullet('•')} The model has no built-in knowledge of your project—open with the architecture, key components, and why you’re asking.`,
131
+ `${helpColors.bullet('•')} Run ${helpColors.accent('--files-report')} to inspect token spend before hitting the API.`,
132
+ `${helpColors.bullet('•')} Non-preview runs spawn detached sessions so they keep streaming even if your terminal closes.`,
133
+ `${helpColors.bullet('•')} Ask the model for a memorable 3–5 word slug and pass it via ${helpColors.accent('--slug "<words>"')} to keep session IDs tidy.`,
134
+ ].join('\n');
135
+ const formatExample = (command, description) => `${helpColors.command(` ${command}`)}\n${helpColors.muted(` ${description}`)}`;
136
+ const examples = [
137
+ formatExample(`${program.name()} --prompt "Summarize risks" --file docs/risk.md --files-report --preview`, 'Inspect tokens + files without calling the API.'),
138
+ formatExample(`${program.name()} --prompt "Explain bug" --file src/,docs/crash.log --files-report`, 'Attach src/ plus docs/crash.log, launch a background session, and capture the Session ID.'),
139
+ formatExample(`${program.name()} status --hours 72 --limit 50`, 'Show sessions from the last 72h (capped at 50 entries).'),
140
+ formatExample(`${program.name()} session <sessionId>`, 'Attach to a running/completed session and stream the saved transcript.'),
141
+ formatExample(`${program.name()} --prompt "Ship review" --slug "release-readiness-audit"`, 'Encourage the model to hand you a 3–5 word slug and pass it along with --slug.'),
142
+ ].join('\n\n');
143
+ return `
144
+ ${helpColors.section('Tips')}
145
+ ${tips}
146
+
147
+ ${helpColors.section('Examples')}
148
+ ${examples}
149
+ `;
150
+ }
151
+ function collectPaths(value, previous = []) {
152
+ if (!value) {
153
+ return previous;
154
+ }
155
+ const nextValues = Array.isArray(value) ? value : [value];
156
+ return previous.concat(nextValues.flatMap((entry) => entry.split(',')).map((entry) => entry.trim()).filter(Boolean));
157
+ }
158
+ function parseFloatOption(value) {
159
+ const parsed = Number.parseFloat(value);
160
+ if (Number.isNaN(parsed)) {
161
+ throw new InvalidArgumentError('Value must be a number.');
162
+ }
163
+ return parsed;
164
+ }
165
+ const DEFAULT_BROWSER_TIMEOUT_MS = 900_000;
166
+ const DEFAULT_BROWSER_INPUT_TIMEOUT_MS = 30_000;
167
+ const BROWSER_MODEL_LABELS = {
168
+ 'gpt-5-pro': 'GPT-5 Pro',
169
+ 'gpt-5.1': 'ChatGPT 5.1',
170
+ };
171
+ function buildBrowserConfig(options) {
172
+ return {
173
+ chromeProfile: options.browserChromeProfile ?? null,
174
+ chromePath: options.browserChromePath ?? null,
175
+ url: options.browserUrl,
176
+ timeoutMs: options.browserTimeout ? parseDuration(options.browserTimeout, DEFAULT_BROWSER_TIMEOUT_MS) : undefined,
177
+ inputTimeoutMs: options.browserInputTimeout
178
+ ? parseDuration(options.browserInputTimeout, DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
179
+ : undefined,
180
+ cookieSync: options.browserNoCookieSync ? false : undefined,
181
+ headless: options.browserHeadless ? true : undefined,
182
+ keepBrowser: options.browserKeepBrowser ? true : undefined,
183
+ hideWindow: options.browserHideWindow ? true : undefined,
184
+ desiredModel: mapModelToBrowserLabel(options.model),
185
+ debug: options.verbose ? true : undefined,
186
+ };
187
+ }
188
+ function mapModelToBrowserLabel(model) {
189
+ return BROWSER_MODEL_LABELS[model] ?? DEFAULT_MODEL_TARGET;
190
+ }
191
+ function validateModel(value) {
192
+ if (!(value in MODEL_CONFIGS)) {
193
+ throw new InvalidArgumentError(`Unsupported model "${value}". Choose one of: ${Object.keys(MODEL_CONFIGS).join(', ')}`);
194
+ }
195
+ return value;
196
+ }
197
+ function usesDefaultStatusFilters(cmd) {
198
+ const hoursSource = cmd.getOptionValueSource?.('hours') ?? 'default';
199
+ const limitSource = cmd.getOptionValueSource?.('limit') ?? 'default';
200
+ const allSource = cmd.getOptionValueSource?.('all') ?? 'default';
201
+ return hoursSource === 'default' && limitSource === 'default' && allSource === 'default';
202
+ }
203
+ function resolvePreviewMode(value) {
204
+ if (typeof value === 'string' && value.length > 0) {
205
+ return value;
206
+ }
207
+ if (value === true) {
208
+ return 'summary';
209
+ }
210
+ return undefined;
211
+ }
212
+ function buildRunOptions(options, overrides = {}) {
213
+ if (!options.prompt) {
214
+ throw new Error('Prompt is required.');
215
+ }
216
+ return {
217
+ prompt: options.prompt,
218
+ model: options.model,
219
+ file: overrides.file ?? options.file ?? [],
220
+ slug: overrides.slug ?? options.slug,
221
+ filesReport: overrides.filesReport ?? options.filesReport,
222
+ maxInput: overrides.maxInput ?? options.maxInput,
223
+ maxOutput: overrides.maxOutput ?? options.maxOutput,
224
+ system: overrides.system ?? options.system,
225
+ silent: overrides.silent ?? options.silent,
226
+ search: overrides.search ?? options.search,
227
+ preview: overrides.preview ?? undefined,
228
+ previewMode: overrides.previewMode ?? options.previewMode,
229
+ apiKey: overrides.apiKey ?? options.apiKey,
230
+ sessionId: overrides.sessionId ?? options.sessionId,
231
+ verbose: overrides.verbose ?? options.verbose,
232
+ };
233
+ }
234
+ function buildRunOptionsFromMetadata(metadata) {
235
+ const stored = metadata.options ?? {};
236
+ return {
237
+ prompt: stored.prompt ?? '',
238
+ model: stored.model ?? 'gpt-5-pro',
239
+ file: stored.file ?? [],
240
+ slug: stored.slug,
241
+ filesReport: stored.filesReport,
242
+ maxInput: stored.maxInput,
243
+ maxOutput: stored.maxOutput,
244
+ system: stored.system,
245
+ silent: stored.silent,
246
+ search: undefined,
247
+ preview: false,
248
+ previewMode: undefined,
249
+ apiKey: undefined,
250
+ sessionId: metadata.id,
251
+ verbose: stored.verbose,
252
+ };
253
+ }
254
+ function getSessionMode(metadata) {
255
+ return metadata.mode ?? metadata.options?.mode ?? 'api';
256
+ }
257
+ function getBrowserConfigFromMetadata(metadata) {
258
+ return metadata.options?.browserConfig ?? metadata.browser?.config;
259
+ }
260
+ async function runRootCommand(options) {
261
+ const helpRequested = rawCliArgs.some((arg) => arg === '--help' || arg === '-h');
262
+ if (helpRequested) {
263
+ program.help({ error: false });
264
+ return;
265
+ }
266
+ const previewMode = resolvePreviewMode(options.preview);
267
+ if (rawCliArgs.length === 0) {
268
+ console.log(chalk.yellow('No prompt or subcommand supplied. See `oracle --help` for usage.'));
269
+ program.help({ error: false });
270
+ return;
271
+ }
272
+ if (options.session) {
273
+ await attachSession(options.session);
274
+ return;
275
+ }
276
+ if (options.execSession) {
277
+ await executeSession(options.execSession);
278
+ return;
279
+ }
280
+ if (options.renderMarkdown) {
281
+ if (!options.prompt) {
282
+ throw new Error('Prompt is required when using --render-markdown.');
283
+ }
284
+ const markdown = await renderPromptMarkdown({ prompt: options.prompt, file: options.file, system: options.system }, { cwd: process.cwd() });
285
+ console.log(markdown);
286
+ return;
287
+ }
288
+ if (previewMode) {
289
+ if (options.browser) {
290
+ throw new Error('--browser cannot be combined with --preview.');
291
+ }
292
+ if (!options.prompt) {
293
+ throw new Error('Prompt is required when using --preview.');
294
+ }
295
+ const runOptions = buildRunOptions(options, { preview: true, previewMode });
296
+ await runOracle(runOptions, { log: console.log, write: (chunk) => process.stdout.write(chunk) });
297
+ return;
298
+ }
299
+ if (!options.prompt) {
300
+ throw new Error('Prompt is required when starting a new session.');
301
+ }
302
+ if (options.file && options.file.length > 0) {
303
+ await readFiles(options.file, { cwd: process.cwd() });
304
+ }
305
+ const sessionMode = options.browser ? 'browser' : 'api';
306
+ const browserConfig = sessionMode === 'browser' ? buildBrowserConfig(options) : undefined;
307
+ await ensureSessionStorage();
308
+ const baseRunOptions = buildRunOptions(options, { preview: false, previewMode: undefined });
309
+ const sessionMeta = await initializeSession({
310
+ ...baseRunOptions,
311
+ mode: sessionMode,
312
+ browserConfig,
313
+ }, process.cwd());
314
+ const liveRunOptions = { ...baseRunOptions, sessionId: sessionMeta.id };
315
+ await runInteractiveSession(sessionMeta, liveRunOptions, sessionMode, browserConfig);
316
+ console.log(chalk.bold(`Session ${sessionMeta.id} completed`));
317
+ }
318
+ async function runInteractiveSession(sessionMeta, runOptions, mode, browserConfig) {
319
+ const { logLine, writeChunk, stream } = createSessionLogWriter(sessionMeta.id);
320
+ let headerAugmented = false;
321
+ const combinedLog = (message = '') => {
322
+ if (!headerAugmented && message.startsWith('Oracle (')) {
323
+ headerAugmented = true;
324
+ console.log(`${message}\n${chalk.blue(`Reattach via: oracle session ${sessionMeta.id}`)}`);
325
+ logLine(message);
326
+ return;
327
+ }
328
+ console.log(message);
329
+ logLine(message);
330
+ };
331
+ const combinedWrite = (chunk) => {
332
+ writeChunk(chunk);
333
+ return process.stdout.write(chunk);
334
+ };
335
+ try {
336
+ await performSessionRun({
337
+ sessionMeta,
338
+ runOptions,
339
+ mode,
340
+ browserConfig,
341
+ cwd: process.cwd(),
342
+ log: combinedLog,
343
+ write: combinedWrite,
344
+ });
345
+ }
346
+ catch (error) {
347
+ throw error;
348
+ }
349
+ finally {
350
+ stream.end();
351
+ }
352
+ }
353
+ async function executeSession(sessionId) {
354
+ const metadata = await readSessionMetadata(sessionId);
355
+ if (!metadata) {
356
+ console.error(chalk.red(`No session found with ID ${sessionId}`));
357
+ process.exitCode = 1;
358
+ return;
359
+ }
360
+ const runOptions = buildRunOptionsFromMetadata(metadata);
361
+ const sessionMode = getSessionMode(metadata);
362
+ const browserConfig = getBrowserConfigFromMetadata(metadata);
363
+ const { logLine, writeChunk, stream } = createSessionLogWriter(sessionId);
364
+ try {
365
+ await performSessionRun({
366
+ sessionMeta: metadata,
367
+ runOptions,
368
+ mode: sessionMode,
369
+ browserConfig,
370
+ cwd: metadata.cwd ?? process.cwd(),
371
+ log: logLine,
372
+ write: writeChunk,
373
+ });
374
+ }
375
+ catch {
376
+ // Errors are already logged to the session log; keep quiet to mirror stored-session behavior.
377
+ }
378
+ finally {
379
+ stream.end();
380
+ }
381
+ }
382
+ async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, }) {
383
+ await updateSessionMetadata(sessionMeta.id, {
384
+ status: 'running',
385
+ startedAt: new Date().toISOString(),
386
+ mode,
387
+ ...(browserConfig ? { browser: { config: browserConfig } } : {}),
388
+ });
389
+ try {
390
+ if (mode === 'browser') {
391
+ if (!browserConfig) {
392
+ throw new Error('Missing browser configuration for session.');
393
+ }
394
+ const result = await runBrowserSessionExecution({
395
+ runOptions,
396
+ browserConfig,
397
+ cwd,
398
+ log,
399
+ });
400
+ await updateSessionMetadata(sessionMeta.id, {
401
+ status: 'completed',
402
+ completedAt: new Date().toISOString(),
403
+ usage: result.usage,
404
+ elapsedMs: result.elapsedMs,
405
+ browser: {
406
+ config: browserConfig,
407
+ runtime: result.runtime,
408
+ },
409
+ response: undefined,
410
+ });
411
+ return;
412
+ }
413
+ const result = await runOracle(runOptions, {
414
+ cwd,
415
+ log,
416
+ write,
417
+ });
418
+ if (result.mode !== 'live') {
419
+ throw new Error('Unexpected preview result while running a session.');
420
+ }
421
+ await updateSessionMetadata(sessionMeta.id, {
422
+ status: 'completed',
423
+ completedAt: new Date().toISOString(),
424
+ usage: result.usage,
425
+ elapsedMs: result.elapsedMs,
426
+ response: extractResponseMetadata(result.response),
427
+ });
428
+ }
429
+ catch (error) {
430
+ const message = formatError(error);
431
+ log(`ERROR: ${message}`);
432
+ const responseMetadata = error instanceof OracleResponseError ? error.metadata : undefined;
433
+ const metadataLine = formatResponseMetadata(responseMetadata);
434
+ if (metadataLine) {
435
+ log(dim(`Response metadata: ${metadataLine}`));
436
+ }
437
+ await updateSessionMetadata(sessionMeta.id, {
438
+ status: 'error',
439
+ completedAt: new Date().toISOString(),
440
+ errorMessage: message,
441
+ mode,
442
+ browser: browserConfig ? { config: browserConfig } : undefined,
443
+ response: responseMetadata,
444
+ });
445
+ throw error;
446
+ }
447
+ }
448
+ async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log, }) {
449
+ const promptArtifacts = await assembleBrowserPrompt(runOptions, cwd);
450
+ if (runOptions.verbose) {
451
+ log(dim(`[verbose] Browser config: ${JSON.stringify({
452
+ ...browserConfig,
453
+ })}`));
454
+ log(dim(`[verbose] Browser prompt length: ${promptArtifacts.markdown.length} chars`));
455
+ }
456
+ const headerLine = `Oracle (${VERSION}) launching browser mode (${runOptions.model}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens`;
457
+ log(headerLine);
458
+ log(dim('Chrome automation does not stream output; this may take a minute...'));
459
+ const browserResult = await runBrowserMode({
460
+ prompt: promptArtifacts.markdown,
461
+ config: browserConfig,
462
+ log,
463
+ });
464
+ if (!runOptions.silent) {
465
+ log(chalk.bold('Answer:'));
466
+ log(browserResult.answerMarkdown || browserResult.answerText || chalk.dim('(no text output)'));
467
+ log('');
468
+ }
469
+ const usage = {
470
+ inputTokens: promptArtifacts.estimatedInputTokens,
471
+ outputTokens: browserResult.answerTokens,
472
+ reasoningTokens: 0,
473
+ totalTokens: promptArtifacts.estimatedInputTokens + browserResult.answerTokens,
474
+ };
475
+ const tokensDisplay = `${usage.inputTokens}/${usage.outputTokens}/${usage.reasoningTokens}/${usage.totalTokens}`;
476
+ const statsParts = [`${runOptions.model}[browser]`, `tok(i/o/r/t)=${tokensDisplay}`];
477
+ if (runOptions.file && runOptions.file.length > 0) {
478
+ statsParts.push(`files=${runOptions.file.length}`);
479
+ }
480
+ log(chalk.blue(`Finished in ${formatElapsed(browserResult.tookMs)} (${statsParts.join(' | ')})`));
481
+ return {
482
+ usage,
483
+ elapsedMs: browserResult.tookMs,
484
+ runtime: {
485
+ chromePid: browserResult.chromePid,
486
+ chromePort: browserResult.chromePort,
487
+ userDataDir: browserResult.userDataDir,
488
+ },
489
+ };
490
+ }
491
+ async function assembleBrowserPrompt(runOptions, cwd) {
492
+ const files = await readFiles(runOptions.file ?? [], { cwd });
493
+ const userPrompt = buildPrompt(runOptions.prompt, files, cwd);
494
+ const systemPrompt = runOptions.system?.trim() || DEFAULT_SYSTEM_PROMPT;
495
+ const sections = createFileSections(files, cwd);
496
+ const lines = ['[SYSTEM]', systemPrompt, '', '[USER]', userPrompt, ''];
497
+ sections.forEach((section) => {
498
+ lines.push(`[FILE: ${section.displayPath}]`, section.content.trimEnd(), '');
499
+ });
500
+ const markdown = lines.join('\n').trimEnd();
501
+ const tokenizer = MODEL_CONFIGS[runOptions.model].tokenizer;
502
+ const estimatedInputTokens = tokenizer([
503
+ { role: 'system', content: systemPrompt },
504
+ { role: 'user', content: userPrompt },
505
+ ], TOKENIZER_OPTIONS);
506
+ return { markdown, estimatedInputTokens };
507
+ }
508
+ async function showStatus({ hours, includeAll, limit, showExamples = false }) {
509
+ const metas = await listSessionsMetadata();
510
+ const { entries, truncated, total } = filterSessionsByRange(metas, { hours, includeAll, limit });
511
+ if (!entries.length) {
512
+ console.log('No sessions found for the requested range.');
513
+ if (showExamples) {
514
+ printStatusExamples();
515
+ }
516
+ return;
517
+ }
518
+ console.log(chalk.bold('Recent Sessions'));
519
+ for (const entry of entries) {
520
+ const status = (entry.status || 'unknown').padEnd(9);
521
+ const model = (entry.model || 'n/a').padEnd(10);
522
+ const created = entry.createdAt.replace('T', ' ').replace('Z', '');
523
+ console.log(`${created} | ${status} | ${model} | ${entry.id}`);
524
+ }
525
+ if (truncated) {
526
+ console.log(chalk.yellow(`Showing ${entries.length} of ${total} sessions from the requested range. Run "oracle status clear" or delete entries in ${SESSIONS_DIR} to free space, or rerun with --status-limit/--status-all.`));
527
+ }
528
+ if (showExamples) {
529
+ printStatusExamples();
530
+ }
531
+ }
532
+ function printStatusExamples() {
533
+ console.log('');
534
+ console.log(chalk.bold('Usage Examples'));
535
+ console.log(`${chalk.bold(' oracle status --hours 72 --limit 50')}`);
536
+ console.log(dim(' Show 72h of history capped at 50 entries.'));
537
+ console.log(`${chalk.bold(' oracle status clear --hours 168')}`);
538
+ console.log(dim(' Delete sessions older than 7 days (use --all to wipe everything).'));
539
+ console.log(`${chalk.bold(' oracle session <session-id>')}`);
540
+ console.log(dim(' Attach to a specific running/completed session to stream its output.'));
541
+ }
542
+ async function attachSession(sessionId) {
543
+ const metadata = await readSessionMetadata(sessionId);
544
+ if (!metadata) {
545
+ console.error(chalk.red(`No session found with ID ${sessionId}`));
546
+ process.exitCode = 1;
547
+ return;
548
+ }
549
+ const reattachLine = buildReattachLine(metadata);
550
+ if (reattachLine) {
551
+ console.log(chalk.blue(reattachLine));
552
+ }
553
+ else {
554
+ console.log(chalk.bold(`Session ${sessionId}`));
555
+ }
556
+ console.log(`Created: ${metadata.createdAt}`);
557
+ console.log(`Status: ${metadata.status}`);
558
+ console.log(`Model: ${metadata.model}`);
559
+ const responseSummary = formatResponseMetadata(metadata.response);
560
+ if (responseSummary) {
561
+ console.log(dim(`Response: ${responseSummary}`));
562
+ }
563
+ let lastLength = 0;
564
+ const printNew = async () => {
565
+ const text = await readSessionLog(sessionId);
566
+ const nextChunk = text.slice(lastLength);
567
+ if (nextChunk.length > 0) {
568
+ process.stdout.write(nextChunk);
569
+ lastLength = text.length;
570
+ }
571
+ };
572
+ await printNew();
573
+ // biome-ignore lint/nursery/noUnnecessaryConditions: deliberate infinite poll
574
+ while (true) {
575
+ const latest = await readSessionMetadata(sessionId);
576
+ if (!latest) {
577
+ break;
578
+ }
579
+ if (latest.status === 'completed' || latest.status === 'error') {
580
+ await printNew();
581
+ if (latest.status === 'error' && latest.errorMessage) {
582
+ console.log(`\nSession failed: ${latest.errorMessage}`);
583
+ }
584
+ if (latest.usage) {
585
+ const usage = latest.usage;
586
+ console.log(`\nFinished (tok i/o/r/t: ${usage.inputTokens}/${usage.outputTokens}/${usage.reasoningTokens}/${usage.totalTokens})`);
587
+ }
588
+ break;
589
+ }
590
+ await wait(1000);
591
+ await printNew();
592
+ }
593
+ }
594
+ function formatError(error) {
595
+ return error instanceof Error ? error.message : String(error);
596
+ }
597
+ function formatResponseMetadata(metadata) {
598
+ if (!metadata) {
599
+ return null;
600
+ }
601
+ const parts = [];
602
+ if (metadata.responseId) {
603
+ parts.push(`response=${metadata.responseId}`);
604
+ }
605
+ if (metadata.requestId) {
606
+ parts.push(`request=${metadata.requestId}`);
607
+ }
608
+ if (metadata.status) {
609
+ parts.push(`status=${metadata.status}`);
610
+ }
611
+ if (metadata.incompleteReason) {
612
+ parts.push(`incomplete=${metadata.incompleteReason}`);
613
+ }
614
+ return parts.length > 0 ? parts.join(' | ') : null;
615
+ }
616
+ function buildReattachLine(metadata) {
617
+ if (!metadata.id) {
618
+ return null;
619
+ }
620
+ const referenceTime = metadata.startedAt ?? metadata.createdAt;
621
+ if (!referenceTime) {
622
+ return null;
623
+ }
624
+ const elapsedLabel = formatRelativeDuration(referenceTime);
625
+ if (!elapsedLabel) {
626
+ return null;
627
+ }
628
+ if (metadata.status === 'running') {
629
+ return `Session ${metadata.id} reattached, request started ${elapsedLabel} ago.`;
630
+ }
631
+ return null;
632
+ }
633
+ function formatRelativeDuration(referenceIso) {
634
+ const timestamp = Date.parse(referenceIso);
635
+ if (Number.isNaN(timestamp)) {
636
+ return null;
637
+ }
638
+ const diffMs = Date.now() - timestamp;
639
+ if (diffMs < 0) {
640
+ return null;
641
+ }
642
+ const seconds = Math.max(1, Math.round(diffMs / 1000));
643
+ if (seconds < 60) {
644
+ return `${seconds}s`;
645
+ }
646
+ const minutes = Math.floor(seconds / 60);
647
+ const remainingSeconds = seconds % 60;
648
+ if (minutes < 60) {
649
+ return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
650
+ }
651
+ const hours = Math.floor(minutes / 60);
652
+ const remainingMinutes = minutes % 60;
653
+ if (hours < 24) {
654
+ const parts = [`${hours}h`];
655
+ if (remainingMinutes > 0) {
656
+ parts.push(`${remainingMinutes}m`);
657
+ }
658
+ return parts.join(' ');
659
+ }
660
+ const days = Math.floor(hours / 24);
661
+ const remainingHours = hours % 24;
662
+ const parts = [`${days}d`];
663
+ if (remainingHours > 0) {
664
+ parts.push(`${remainingHours}h`);
665
+ }
666
+ if (remainingMinutes > 0 && days === 0) {
667
+ parts.push(`${remainingMinutes}m`);
668
+ }
669
+ return parts.join(' ');
670
+ }
671
+ program.action(async function () {
672
+ const options = this.optsWithGlobals();
673
+ await runRootCommand(options);
674
+ });
675
+ await program.parseAsync(process.argv).catch((error) => {
676
+ if (error instanceof Error) {
677
+ console.error(chalk.red('✖'), error.message);
678
+ }
679
+ else {
680
+ console.error(chalk.red('✖'), error);
681
+ }
682
+ process.exitCode = 1;
683
+ });