@indykish/oracle 0.9.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 (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/assets-oracle-icon.png +0 -0
  4. package/dist/bin/oracle-cli.js +1252 -0
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/scripts/agent-send.js +147 -0
  7. package/dist/scripts/browser-tools.js +536 -0
  8. package/dist/scripts/check.js +21 -0
  9. package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
  10. package/dist/scripts/docs-list.js +110 -0
  11. package/dist/scripts/git-policy.js +125 -0
  12. package/dist/scripts/run-cli.js +14 -0
  13. package/dist/scripts/runner.js +1378 -0
  14. package/dist/scripts/test-browser.js +103 -0
  15. package/dist/scripts/test-remote-chrome.js +68 -0
  16. package/dist/src/bridge/connection.js +103 -0
  17. package/dist/src/bridge/userConfigFile.js +28 -0
  18. package/dist/src/browser/actions/assistantResponse.js +1067 -0
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
  20. package/dist/src/browser/actions/attachments.js +1910 -0
  21. package/dist/src/browser/actions/domEvents.js +19 -0
  22. package/dist/src/browser/actions/modelSelection.js +485 -0
  23. package/dist/src/browser/actions/navigation.js +445 -0
  24. package/dist/src/browser/actions/promptComposer.js +485 -0
  25. package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
  26. package/dist/src/browser/actions/thinkingTime.js +206 -0
  27. package/dist/src/browser/chromeLifecycle.js +344 -0
  28. package/dist/src/browser/config.js +103 -0
  29. package/dist/src/browser/constants.js +71 -0
  30. package/dist/src/browser/cookies.js +191 -0
  31. package/dist/src/browser/detect.js +164 -0
  32. package/dist/src/browser/domDebug.js +36 -0
  33. package/dist/src/browser/index.js +1741 -0
  34. package/dist/src/browser/modelStrategy.js +13 -0
  35. package/dist/src/browser/pageActions.js +5 -0
  36. package/dist/src/browser/policies.js +43 -0
  37. package/dist/src/browser/profileState.js +280 -0
  38. package/dist/src/browser/prompt.js +152 -0
  39. package/dist/src/browser/promptSummary.js +20 -0
  40. package/dist/src/browser/reattach.js +186 -0
  41. package/dist/src/browser/reattachHelpers.js +382 -0
  42. package/dist/src/browser/sessionRunner.js +119 -0
  43. package/dist/src/browser/types.js +1 -0
  44. package/dist/src/browser/utils.js +122 -0
  45. package/dist/src/browserMode.js +1 -0
  46. package/dist/src/cli/bridge/claudeConfig.js +54 -0
  47. package/dist/src/cli/bridge/client.js +73 -0
  48. package/dist/src/cli/bridge/codexConfig.js +43 -0
  49. package/dist/src/cli/bridge/doctor.js +107 -0
  50. package/dist/src/cli/bridge/host.js +259 -0
  51. package/dist/src/cli/browserConfig.js +278 -0
  52. package/dist/src/cli/browserDefaults.js +81 -0
  53. package/dist/src/cli/bundleWarnings.js +9 -0
  54. package/dist/src/cli/clipboard.js +10 -0
  55. package/dist/src/cli/detach.js +11 -0
  56. package/dist/src/cli/dryRun.js +105 -0
  57. package/dist/src/cli/duplicatePromptGuard.js +14 -0
  58. package/dist/src/cli/engine.js +41 -0
  59. package/dist/src/cli/errorUtils.js +9 -0
  60. package/dist/src/cli/format.js +13 -0
  61. package/dist/src/cli/help.js +77 -0
  62. package/dist/src/cli/hiddenAliases.js +22 -0
  63. package/dist/src/cli/markdownBundle.js +17 -0
  64. package/dist/src/cli/markdownRenderer.js +97 -0
  65. package/dist/src/cli/notifier.js +306 -0
  66. package/dist/src/cli/options.js +281 -0
  67. package/dist/src/cli/oscUtils.js +2 -0
  68. package/dist/src/cli/promptRequirement.js +17 -0
  69. package/dist/src/cli/renderFlags.js +9 -0
  70. package/dist/src/cli/renderOutput.js +26 -0
  71. package/dist/src/cli/rootAlias.js +30 -0
  72. package/dist/src/cli/runOptions.js +78 -0
  73. package/dist/src/cli/sessionCommand.js +111 -0
  74. package/dist/src/cli/sessionDisplay.js +567 -0
  75. package/dist/src/cli/sessionRunner.js +602 -0
  76. package/dist/src/cli/sessionTable.js +92 -0
  77. package/dist/src/cli/tagline.js +258 -0
  78. package/dist/src/cli/tui/index.js +486 -0
  79. package/dist/src/cli/writeOutputPath.js +21 -0
  80. package/dist/src/config.js +26 -0
  81. package/dist/src/gemini-web/client.js +328 -0
  82. package/dist/src/gemini-web/executor.js +285 -0
  83. package/dist/src/gemini-web/index.js +1 -0
  84. package/dist/src/gemini-web/types.js +1 -0
  85. package/dist/src/heartbeat.js +43 -0
  86. package/dist/src/mcp/server.js +40 -0
  87. package/dist/src/mcp/tools/consult.js +290 -0
  88. package/dist/src/mcp/tools/sessionResources.js +75 -0
  89. package/dist/src/mcp/tools/sessions.js +105 -0
  90. package/dist/src/mcp/types.js +22 -0
  91. package/dist/src/mcp/utils.js +37 -0
  92. package/dist/src/oracle/background.js +141 -0
  93. package/dist/src/oracle/claude.js +101 -0
  94. package/dist/src/oracle/client.js +197 -0
  95. package/dist/src/oracle/config.js +227 -0
  96. package/dist/src/oracle/errors.js +132 -0
  97. package/dist/src/oracle/files.js +378 -0
  98. package/dist/src/oracle/finishLine.js +32 -0
  99. package/dist/src/oracle/format.js +30 -0
  100. package/dist/src/oracle/fsAdapter.js +10 -0
  101. package/dist/src/oracle/gemini.js +195 -0
  102. package/dist/src/oracle/logging.js +36 -0
  103. package/dist/src/oracle/markdown.js +46 -0
  104. package/dist/src/oracle/modelResolver.js +183 -0
  105. package/dist/src/oracle/multiModelRunner.js +153 -0
  106. package/dist/src/oracle/oscProgress.js +24 -0
  107. package/dist/src/oracle/promptAssembly.js +13 -0
  108. package/dist/src/oracle/request.js +50 -0
  109. package/dist/src/oracle/run.js +596 -0
  110. package/dist/src/oracle/runUtils.js +31 -0
  111. package/dist/src/oracle/tokenEstimate.js +37 -0
  112. package/dist/src/oracle/tokenStats.js +39 -0
  113. package/dist/src/oracle/tokenStringifier.js +24 -0
  114. package/dist/src/oracle/types.js +1 -0
  115. package/dist/src/oracle.js +12 -0
  116. package/dist/src/oracleHome.js +13 -0
  117. package/dist/src/remote/client.js +129 -0
  118. package/dist/src/remote/health.js +113 -0
  119. package/dist/src/remote/remoteServiceConfig.js +31 -0
  120. package/dist/src/remote/server.js +533 -0
  121. package/dist/src/remote/types.js +1 -0
  122. package/dist/src/sessionManager.js +637 -0
  123. package/dist/src/sessionStore.js +56 -0
  124. package/dist/src/version.js +39 -0
  125. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  126. package/dist/vendor/oracle-notifier/README.md +24 -0
  127. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  128. package/package.json +115 -0
  129. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  130. package/vendor/oracle-notifier/README.md +24 -0
  131. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,486 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import kleur from 'kleur';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import fs from 'node:fs/promises';
7
+ import { DEFAULT_MODEL, MODEL_CONFIGS } from '../../oracle.js';
8
+ import { renderMarkdownAnsi } from '../markdownRenderer.js';
9
+ import { sessionStore, pruneOldSessions } from '../../sessionStore.js';
10
+ import { performSessionRun } from '../sessionRunner.js';
11
+ import { MAX_RENDER_BYTES, trimBeforeFirstAnswer } from '../sessionDisplay.js';
12
+ import { formatSessionTableHeader, formatSessionTableRow } from '../sessionTable.js';
13
+ import { buildBrowserConfig, resolveBrowserModelLabel } from '../browserConfig.js';
14
+ import { resolveNotificationSettings } from '../notifier.js';
15
+ import { loadUserConfig } from '../../config.js';
16
+ import { formatTokenCount } from '../../oracle/runUtils.js';
17
+ const isTty = () => Boolean(process.stdout.isTTY && chalk.level > 0);
18
+ const dim = (text) => (isTty() ? kleur.dim(text) : text);
19
+ const RECENT_WINDOW_HOURS = 24;
20
+ const PAGE_SIZE = 10;
21
+ export async function launchTui({ version, printIntro = true }) {
22
+ const userConfig = (await loadUserConfig()).config;
23
+ const rich = isTty();
24
+ let pagingFailures = 0;
25
+ let exitMessageShown = false;
26
+ if (printIntro) {
27
+ if (rich) {
28
+ console.log(chalk.bold('🧿 oracle'), `${version}`, dim('— Whispering your tokens to the silicon sage'));
29
+ }
30
+ else {
31
+ console.log(`🧿 oracle ${version} — Whispering your tokens to the silicon sage`);
32
+ }
33
+ }
34
+ console.log('');
35
+ let showingOlder = false;
36
+ for (;;) {
37
+ const { recent, older, olderTotal } = await fetchSessionBuckets();
38
+ const choices = [];
39
+ const headerLabel = formatSessionTableHeader(isTty());
40
+ // Start with a selectable row so focus never lands on a separator
41
+ choices.push({ name: chalk.bold.green('ask oracle'), value: '__ask__' });
42
+ if (!showingOlder) {
43
+ if (recent.length > 0) {
44
+ choices.push(new inquirer.Separator(headerLabel));
45
+ choices.push(...recent.map(toSessionChoice));
46
+ }
47
+ else if (older.length > 0) {
48
+ // No recent entries; show first page of older.
49
+ choices.push(new inquirer.Separator(headerLabel));
50
+ choices.push(...older.slice(0, PAGE_SIZE).map(toSessionChoice));
51
+ }
52
+ }
53
+ else if (older.length > 0) {
54
+ choices.push(new inquirer.Separator(headerLabel));
55
+ choices.push(...older.map(toSessionChoice));
56
+ }
57
+ choices.push(new inquirer.Separator(' '));
58
+ choices.push(new inquirer.Separator('Actions'));
59
+ choices.push({ name: chalk.bold.green('ask oracle'), value: '__ask__' });
60
+ if (!showingOlder && olderTotal > 0) {
61
+ choices.push({ name: 'Older page', value: '__older__' });
62
+ }
63
+ else {
64
+ choices.push({ name: 'Newer (recent)', value: '__reset__' });
65
+ }
66
+ choices.push({ name: 'Exit', value: '__exit__' });
67
+ const selection = await new Promise((resolve) => {
68
+ const prompt = inquirer.prompt([
69
+ {
70
+ name: 'selection',
71
+ type: 'select',
72
+ message: 'Select a session or action',
73
+ choices,
74
+ pageSize: 16,
75
+ loop: false,
76
+ },
77
+ ]);
78
+ prompt
79
+ .then(({ selection: answer }) => resolve(answer))
80
+ .catch((error) => {
81
+ pagingFailures += 1;
82
+ const message = error instanceof Error ? error.message : String(error);
83
+ if (message.includes('SIGINT') || message.includes('force closed the prompt')) {
84
+ console.log(chalk.green('🧿 Closing the book. See you next prompt.'));
85
+ exitMessageShown = true;
86
+ resolve('__exit__');
87
+ return;
88
+ }
89
+ console.error(chalk.red('Paging failed; returning to recent list.'), message);
90
+ if (message.includes('setRawMode') || message.includes('EIO') || pagingFailures >= 3) {
91
+ console.error(chalk.red('Terminal input unavailable; exiting TUI.'), dim('Try `stty sane` then rerun oracle, or use `oracle recent`.'));
92
+ resolve('__exit__');
93
+ return;
94
+ }
95
+ resolve('__reset__');
96
+ });
97
+ });
98
+ if (process.env.ORACLE_DEBUG_TUI === '1') {
99
+ console.error(`[tui] selection=${JSON.stringify(selection)}`);
100
+ }
101
+ pagingFailures = 0;
102
+ if (selection === '__exit__') {
103
+ if (!exitMessageShown) {
104
+ console.log(chalk.green('🧿 Closing the book. See you next prompt.'));
105
+ }
106
+ return;
107
+ }
108
+ if (selection === '__ask__') {
109
+ await askOracleFlow(version, userConfig);
110
+ continue;
111
+ }
112
+ if (selection === '__older__') {
113
+ showingOlder = true;
114
+ continue;
115
+ }
116
+ if (selection === '__reset__') {
117
+ showingOlder = false;
118
+ continue;
119
+ }
120
+ await showSessionDetail(selection);
121
+ }
122
+ }
123
+ async function fetchSessionBuckets() {
124
+ const all = await sessionStore.listSessions();
125
+ const cutoff = Date.now() - RECENT_WINDOW_HOURS * 60 * 60 * 1000;
126
+ const recent = all.filter((meta) => new Date(meta.createdAt).getTime() >= cutoff).slice(0, PAGE_SIZE);
127
+ const olderAll = all.filter((meta) => new Date(meta.createdAt).getTime() < cutoff);
128
+ const older = olderAll.slice(0, PAGE_SIZE);
129
+ const hasMoreOlder = olderAll.length > PAGE_SIZE;
130
+ if (recent.length === 0 && older.length === 0 && olderAll.length > 0) {
131
+ // No recent entries; fall back to top 10 overall.
132
+ return { recent: olderAll.slice(0, PAGE_SIZE), older: [], hasMoreOlder: olderAll.length > PAGE_SIZE, olderTotal: olderAll.length };
133
+ }
134
+ return { recent, older, hasMoreOlder, olderTotal: olderAll.length };
135
+ }
136
+ function toSessionChoice(meta) {
137
+ return {
138
+ name: formatSessionTableRow(meta, { rich: isTty() }),
139
+ value: meta.id,
140
+ };
141
+ }
142
+ async function showSessionDetail(sessionId) {
143
+ for (;;) {
144
+ const meta = await readSessionMetadataSafe(sessionId);
145
+ if (!meta) {
146
+ console.log(chalk.red(`No session found with ID ${sessionId}`));
147
+ return;
148
+ }
149
+ console.clear();
150
+ printSessionHeader(meta);
151
+ if (meta.models && meta.models.length > 0) {
152
+ printModelSummaries(meta.models);
153
+ }
154
+ const prompt = await readStoredPrompt(sessionId);
155
+ if (prompt) {
156
+ console.log(chalk.bold('Prompt:'));
157
+ console.log(renderMarkdownAnsi(prompt));
158
+ console.log(dim('---'));
159
+ }
160
+ const logPath = await getSessionLogPath(sessionId);
161
+ if (logPath) {
162
+ console.log(dim(`Log file: ${logPath}`));
163
+ }
164
+ console.log('');
165
+ await renderSessionLog(sessionId);
166
+ const isRunning = meta.status === 'running';
167
+ const modelActions = meta.models?.map((run) => ({
168
+ name: `View ${run.model} log (${run.status})`,
169
+ value: `log:${run.model}`,
170
+ })) ?? [];
171
+ const actions = [
172
+ { name: 'View combined log', value: 'log:__all__' },
173
+ ...modelActions,
174
+ ...(isRunning ? [{ name: 'Refresh', value: 'refresh' }] : []),
175
+ { name: 'Back', value: 'back' },
176
+ ];
177
+ let next;
178
+ try {
179
+ ({ next } = await inquirer.prompt([
180
+ {
181
+ name: 'next',
182
+ type: 'select',
183
+ message: 'Actions',
184
+ choices: actions,
185
+ },
186
+ ]));
187
+ }
188
+ catch (error) {
189
+ const message = error instanceof Error ? error.message : String(error);
190
+ if (message.includes('SIGINT') || message.includes('force closed the prompt')) {
191
+ console.log(chalk.green('🧿 Closing the book. See you next prompt.'));
192
+ return;
193
+ }
194
+ console.error(chalk.red('Paging failed; returning to session list.'), message);
195
+ return;
196
+ }
197
+ if (next === 'back') {
198
+ return;
199
+ }
200
+ if (next === 'refresh') {
201
+ continue;
202
+ }
203
+ if (next.startsWith('log:')) {
204
+ const [, target] = next.split(':');
205
+ await renderSessionLog(sessionId, target === '__all__' ? undefined : target);
206
+ }
207
+ }
208
+ }
209
+ async function renderSessionLog(sessionId, model) {
210
+ const raw = model ? await sessionStore.readModelLog(sessionId, model) : await sessionStore.readLog(sessionId);
211
+ const headerLabel = model ? `Log (${model})` : 'Log';
212
+ console.log(chalk.bold(headerLabel));
213
+ const text = trimBeforeFirstAnswer(raw);
214
+ const size = Buffer.byteLength(text, 'utf8');
215
+ if (size > MAX_RENDER_BYTES) {
216
+ console.log(chalk.yellow(`Log is large (${size.toLocaleString()} bytes). Rendering raw text; open the log file for full context.`));
217
+ process.stdout.write(text);
218
+ console.log('');
219
+ return;
220
+ }
221
+ if (!text.trim()) {
222
+ console.log(dim('(log is empty)'));
223
+ console.log('');
224
+ return;
225
+ }
226
+ process.stdout.write(renderMarkdownAnsi(text));
227
+ console.log('');
228
+ }
229
+ async function getSessionLogPath(sessionId) {
230
+ try {
231
+ const paths = await sessionStore.getPaths(sessionId);
232
+ return paths.log;
233
+ }
234
+ catch {
235
+ return null;
236
+ }
237
+ }
238
+ function printSessionHeader(meta) {
239
+ console.log(chalk.bold(`Session ${chalk.cyan(meta.id)}`));
240
+ console.log(`${chalk.white('Status:')} ${meta.status}`);
241
+ console.log(`${chalk.white('Created:')} ${meta.createdAt}`);
242
+ if (meta.model) {
243
+ console.log(`${chalk.white('Model:')} ${meta.model}`);
244
+ }
245
+ const mode = meta.mode ?? meta.options?.mode;
246
+ if (mode) {
247
+ console.log(`${chalk.white('Mode:')} ${mode}`);
248
+ }
249
+ if (meta.errorMessage) {
250
+ console.log(chalk.red(`Error: ${meta.errorMessage}`));
251
+ }
252
+ }
253
+ function printModelSummaries(models) {
254
+ if (models.length === 0) {
255
+ return;
256
+ }
257
+ console.log(chalk.bold('Models:'));
258
+ for (const run of models) {
259
+ const usage = run.usage
260
+ ? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
261
+ : '';
262
+ console.log(` - ${chalk.cyan(run.model)} — ${run.status}${usage}`);
263
+ }
264
+ console.log('');
265
+ }
266
+ async function askOracleFlow(version, userConfig) {
267
+ const modelChoices = Object.keys(MODEL_CONFIGS);
268
+ const hasApiKey = Boolean(process.env.OPENAI_API_KEY);
269
+ const initialMode = hasApiKey ? 'api' : 'browser';
270
+ const preferredMode = userConfig.engine ?? initialMode;
271
+ const wizardQuestions = [
272
+ {
273
+ name: 'promptInput',
274
+ type: 'input',
275
+ message: 'Paste your prompt text or a path to a file (leave blank to cancel):',
276
+ },
277
+ ...(hasApiKey
278
+ ? [
279
+ {
280
+ name: 'mode',
281
+ type: 'select',
282
+ message: 'Engine',
283
+ default: preferredMode,
284
+ choices: [
285
+ { name: 'API', value: 'api' },
286
+ { name: 'Browser', value: 'browser' },
287
+ ],
288
+ },
289
+ ]
290
+ : [
291
+ {
292
+ name: 'mode',
293
+ type: 'select',
294
+ message: 'Engine',
295
+ default: preferredMode,
296
+ choices: [{ name: 'Browser', value: 'browser' }],
297
+ },
298
+ ]),
299
+ {
300
+ name: 'slug',
301
+ type: 'input',
302
+ message: 'Optional slug (3–5 words, leave blank for auto):',
303
+ },
304
+ {
305
+ name: 'model',
306
+ type: 'select',
307
+ message: 'Model',
308
+ default: DEFAULT_MODEL,
309
+ choices: modelChoices,
310
+ },
311
+ {
312
+ name: 'models',
313
+ type: 'checkbox',
314
+ message: 'Additional API models to fan out to (optional)',
315
+ choices: modelChoices,
316
+ when: (ans) => ans.mode === 'api',
317
+ filter: (values) => Array.isArray(values)
318
+ ? values
319
+ .map((entry) => entry.trim())
320
+ .filter((entry) => modelChoices.includes(entry))
321
+ : [],
322
+ },
323
+ {
324
+ name: 'files',
325
+ type: 'input',
326
+ message: 'Files or globs to attach (comma-separated, optional):',
327
+ filter: (value) => value
328
+ .split(',')
329
+ .map((entry) => entry.trim())
330
+ .filter(Boolean),
331
+ },
332
+ {
333
+ name: 'chromeProfile',
334
+ type: 'input',
335
+ message: 'Chrome profile to reuse cookies from:',
336
+ default: 'Default',
337
+ when: (ans) => ans.mode === 'browser',
338
+ },
339
+ {
340
+ name: 'chromeCookiePath',
341
+ type: 'input',
342
+ message: 'Cookie DB path (Chromium/Edge, optional):',
343
+ when: (ans) => ans.mode === 'browser',
344
+ },
345
+ {
346
+ name: 'hideWindow',
347
+ type: 'confirm',
348
+ message: 'Hide Chrome window (macOS headful only)?',
349
+ default: false,
350
+ when: (ans) => ans.mode === 'browser',
351
+ },
352
+ {
353
+ name: 'keepBrowser',
354
+ type: 'confirm',
355
+ message: 'Keep browser open after completion?',
356
+ default: false,
357
+ when: (ans) => ans.mode === 'browser',
358
+ },
359
+ ];
360
+ const answers = await inquirer.prompt(wizardQuestions);
361
+ const mode = (answers.mode ?? initialMode);
362
+ const prompt = await resolvePromptInput(answers.promptInput);
363
+ if (!prompt.trim()) {
364
+ console.log(chalk.yellow('Cancelled.'));
365
+ return;
366
+ }
367
+ const promptWithSuffix = userConfig.promptSuffix ? `${prompt.trim()}\n${userConfig.promptSuffix}` : prompt;
368
+ await sessionStore.ensureStorage();
369
+ await pruneOldSessions(userConfig.sessionRetentionHours, (message) => console.log(chalk.dim(message)));
370
+ const normalizedMultiModels = Array.isArray(answers.models) && answers.models.length > 0
371
+ ? Array.from(new Set([answers.model, ...answers.models].filter((entry) => modelChoices.includes(entry))))
372
+ : [answers.model];
373
+ const runOptions = {
374
+ prompt: promptWithSuffix,
375
+ model: answers.model,
376
+ file: answers.files,
377
+ models: normalizedMultiModels.length > 1 ? normalizedMultiModels : undefined,
378
+ slug: answers.slug,
379
+ filesReport: false,
380
+ maxInput: undefined,
381
+ maxOutput: undefined,
382
+ system: undefined,
383
+ silent: false,
384
+ search: undefined,
385
+ preview: false,
386
+ previewMode: undefined,
387
+ apiKey: undefined,
388
+ sessionId: undefined,
389
+ verbose: false,
390
+ heartbeatIntervalMs: undefined,
391
+ browserAttachments: 'auto',
392
+ browserInlineFiles: false,
393
+ browserBundleFiles: false,
394
+ background: undefined,
395
+ };
396
+ const browserConfig = mode === 'browser'
397
+ ? await buildBrowserConfig({
398
+ browserChromeProfile: answers.chromeProfile,
399
+ browserCookiePath: answers.chromeCookiePath,
400
+ browserHideWindow: answers.hideWindow,
401
+ browserKeepBrowser: answers.keepBrowser,
402
+ browserModelLabel: resolveBrowserModelLabel(undefined, answers.model),
403
+ model: answers.model,
404
+ })
405
+ : undefined;
406
+ const notifications = resolveNotificationSettings({
407
+ cliNotify: undefined,
408
+ cliNotifySound: undefined,
409
+ env: process.env,
410
+ config: userConfig.notify,
411
+ });
412
+ const sessionMeta = await sessionStore.createSession({
413
+ ...runOptions,
414
+ mode,
415
+ browserConfig,
416
+ waitPreference: true,
417
+ }, process.cwd(), notifications);
418
+ const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionMeta.id);
419
+ const combinedLog = (message) => {
420
+ if (message) {
421
+ console.log(message);
422
+ logLine(message);
423
+ }
424
+ };
425
+ // Write streamed chunks to the session log; stdout handling is owned by runOracle.
426
+ const combinedWrite = (chunk) => {
427
+ writeChunk(chunk);
428
+ return true;
429
+ };
430
+ console.log(chalk.bold(`Session ${sessionMeta.id} starting...`));
431
+ console.log(dim(`Log path: ${path.join(os.homedir(), '.oracle', 'sessions', sessionMeta.id, 'output.log')}`));
432
+ try {
433
+ await performSessionRun({
434
+ sessionMeta,
435
+ runOptions: { ...runOptions, sessionId: sessionMeta.id },
436
+ mode,
437
+ browserConfig,
438
+ cwd: process.cwd(),
439
+ log: combinedLog,
440
+ write: combinedWrite,
441
+ version,
442
+ notifications,
443
+ });
444
+ console.log(chalk.green(`Session ${sessionMeta.id} completed.`));
445
+ }
446
+ catch (error) {
447
+ const message = error instanceof Error ? error.message : String(error);
448
+ console.log(chalk.red(`Session ${sessionMeta.id} failed: ${message}`));
449
+ }
450
+ finally {
451
+ stream.end();
452
+ }
453
+ }
454
+ const readSessionMetadataSafe = (sessionId) => sessionStore.readSession(sessionId);
455
+ async function resolvePromptInput(rawInput) {
456
+ const trimmed = rawInput.trim();
457
+ if (!trimmed) {
458
+ return trimmed;
459
+ }
460
+ const asPath = path.resolve(process.cwd(), trimmed);
461
+ try {
462
+ const stats = await fs.stat(asPath);
463
+ if (stats.isFile()) {
464
+ const contents = await fs.readFile(asPath, 'utf8');
465
+ return contents;
466
+ }
467
+ }
468
+ catch {
469
+ // not a file; fall through
470
+ }
471
+ return trimmed;
472
+ }
473
+ async function readStoredPrompt(sessionId) {
474
+ const request = await sessionStore.readRequest(sessionId);
475
+ if (request?.prompt && request.prompt.trim().length > 0) {
476
+ return request.prompt;
477
+ }
478
+ const meta = await sessionStore.readSession(sessionId);
479
+ if (meta?.options?.prompt && meta.options.prompt.trim().length > 0) {
480
+ return meta.options.prompt;
481
+ }
482
+ return null;
483
+ }
484
+ // Exported for testing
485
+ export { askOracleFlow, showSessionDetail };
486
+ export { resolveSessionCost as resolveCost } from '../sessionTable.js';
@@ -0,0 +1,21 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { sessionStore } from '../sessionStore.js';
4
+ export function resolveOutputPath(input, cwd) {
5
+ if (!input || input.trim().length === 0) {
6
+ return undefined;
7
+ }
8
+ const expanded = input.startsWith('~/') ? path.join(os.homedir(), input.slice(2)) : input;
9
+ if (expanded === '-' || expanded === '/dev/stdout') {
10
+ return expanded;
11
+ }
12
+ const absolute = path.isAbsolute(expanded) ? expanded : path.resolve(cwd, expanded);
13
+ const sessionsDir = sessionStore.sessionsDir();
14
+ const normalizedSessionsDir = path.resolve(sessionsDir);
15
+ const normalizedTarget = path.resolve(absolute);
16
+ if (normalizedTarget === normalizedSessionsDir ||
17
+ normalizedTarget.startsWith(`${normalizedSessionsDir}${path.sep}`)) {
18
+ throw new Error(`Refusing to write output inside session storage (${normalizedSessionsDir}). Choose another path.`);
19
+ }
20
+ return absolute;
21
+ }
@@ -0,0 +1,26 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import JSON5 from 'json5';
4
+ import { getOracleHomeDir } from './oracleHome.js';
5
+ function resolveConfigPath() {
6
+ return path.join(getOracleHomeDir(), 'config.json');
7
+ }
8
+ export async function loadUserConfig() {
9
+ const CONFIG_PATH = resolveConfigPath();
10
+ try {
11
+ const raw = await fs.readFile(CONFIG_PATH, 'utf8');
12
+ const parsed = JSON5.parse(raw);
13
+ return { config: parsed ?? {}, path: CONFIG_PATH, loaded: true };
14
+ }
15
+ catch (error) {
16
+ const code = error.code;
17
+ if (code === 'ENOENT') {
18
+ return { config: {}, path: CONFIG_PATH, loaded: false };
19
+ }
20
+ console.warn(`Failed to read ${CONFIG_PATH}: ${error instanceof Error ? error.message : String(error)}`);
21
+ return { config: {}, path: CONFIG_PATH, loaded: false };
22
+ }
23
+ }
24
+ export function configPath() {
25
+ return resolveConfigPath();
26
+ }