@steipete/oracle 1.0.8 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +32 -4
  2. package/assets-oracle-icon.png +0 -0
  3. package/dist/bin/oracle-cli.js +178 -21
  4. package/dist/bin/oracle-mcp.js +6 -0
  5. package/dist/markdansi/types/index.js +4 -0
  6. package/dist/oracle/bin/oracle-cli.js +472 -0
  7. package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
  8. package/dist/oracle/src/browser/actions/attachments.js +82 -0
  9. package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
  10. package/dist/oracle/src/browser/actions/navigation.js +75 -0
  11. package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
  12. package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
  13. package/dist/oracle/src/browser/config.js +33 -0
  14. package/dist/oracle/src/browser/constants.js +40 -0
  15. package/dist/oracle/src/browser/cookies.js +210 -0
  16. package/dist/oracle/src/browser/domDebug.js +36 -0
  17. package/dist/oracle/src/browser/index.js +331 -0
  18. package/dist/oracle/src/browser/pageActions.js +5 -0
  19. package/dist/oracle/src/browser/prompt.js +88 -0
  20. package/dist/oracle/src/browser/promptSummary.js +20 -0
  21. package/dist/oracle/src/browser/sessionRunner.js +80 -0
  22. package/dist/oracle/src/browser/types.js +1 -0
  23. package/dist/oracle/src/browser/utils.js +62 -0
  24. package/dist/oracle/src/browserMode.js +1 -0
  25. package/dist/oracle/src/cli/browserConfig.js +44 -0
  26. package/dist/oracle/src/cli/dryRun.js +59 -0
  27. package/dist/oracle/src/cli/engine.js +17 -0
  28. package/dist/oracle/src/cli/errorUtils.js +9 -0
  29. package/dist/oracle/src/cli/help.js +70 -0
  30. package/dist/oracle/src/cli/markdownRenderer.js +15 -0
  31. package/dist/oracle/src/cli/options.js +103 -0
  32. package/dist/oracle/src/cli/promptRequirement.js +14 -0
  33. package/dist/oracle/src/cli/rootAlias.js +30 -0
  34. package/dist/oracle/src/cli/sessionCommand.js +77 -0
  35. package/dist/oracle/src/cli/sessionDisplay.js +270 -0
  36. package/dist/oracle/src/cli/sessionRunner.js +94 -0
  37. package/dist/oracle/src/heartbeat.js +43 -0
  38. package/dist/oracle/src/oracle/client.js +48 -0
  39. package/dist/oracle/src/oracle/config.js +29 -0
  40. package/dist/oracle/src/oracle/errors.js +101 -0
  41. package/dist/oracle/src/oracle/files.js +220 -0
  42. package/dist/oracle/src/oracle/format.js +33 -0
  43. package/dist/oracle/src/oracle/fsAdapter.js +7 -0
  44. package/dist/oracle/src/oracle/oscProgress.js +60 -0
  45. package/dist/oracle/src/oracle/request.js +48 -0
  46. package/dist/oracle/src/oracle/run.js +444 -0
  47. package/dist/oracle/src/oracle/tokenStats.js +39 -0
  48. package/dist/oracle/src/oracle/types.js +1 -0
  49. package/dist/oracle/src/oracle.js +9 -0
  50. package/dist/oracle/src/sessionManager.js +205 -0
  51. package/dist/oracle/src/version.js +39 -0
  52. package/dist/src/browser/actions/modelSelection.js +117 -29
  53. package/dist/src/browser/cookies.js +1 -1
  54. package/dist/src/browser/index.js +2 -1
  55. package/dist/src/browser/prompt.js +6 -5
  56. package/dist/src/browser/sessionRunner.js +4 -2
  57. package/dist/src/cli/dryRun.js +41 -5
  58. package/dist/src/cli/engine.js +7 -0
  59. package/dist/src/cli/help.js +1 -1
  60. package/dist/src/cli/hiddenAliases.js +17 -0
  61. package/dist/src/cli/markdownRenderer.js +97 -0
  62. package/dist/src/cli/notifier.js +223 -0
  63. package/dist/src/cli/promptRequirement.js +3 -0
  64. package/dist/src/cli/rootAlias.js +14 -0
  65. package/dist/src/cli/runOptions.js +29 -0
  66. package/dist/src/cli/sessionCommand.js +60 -2
  67. package/dist/src/cli/sessionDisplay.js +222 -10
  68. package/dist/src/cli/sessionRunner.js +21 -2
  69. package/dist/src/cli/tui/index.js +436 -0
  70. package/dist/src/config.js +27 -0
  71. package/dist/src/mcp/server.js +36 -0
  72. package/dist/src/mcp/tools/consult.js +158 -0
  73. package/dist/src/mcp/tools/sessionResources.js +64 -0
  74. package/dist/src/mcp/tools/sessions.js +106 -0
  75. package/dist/src/mcp/types.js +17 -0
  76. package/dist/src/mcp/utils.js +24 -0
  77. package/dist/src/oracle/files.js +143 -6
  78. package/dist/src/oracle/oscProgress.js +60 -0
  79. package/dist/src/oracle/run.js +104 -71
  80. package/dist/src/oracle/tokenEstimate.js +34 -0
  81. package/dist/src/sessionManager.js +65 -3
  82. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  83. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  84. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  85. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  86. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  87. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  88. package/dist/vendor/oracle-notifier/README.md +24 -0
  89. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  90. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  91. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  92. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  93. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  94. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  95. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
  96. package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
  97. package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
  98. package/package.json +27 -9
  99. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  100. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  101. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  102. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  103. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  104. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  105. package/vendor/oracle-notifier/README.md +24 -0
  106. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -47,7 +47,7 @@ function renderHelpFooter(program, colors) {
47
47
  `${colors.bullet('•')} Oracle starts empty—open with a short project briefing (stack, services, build steps), spell out the question and prior attempts, and why it matters; the more explanation and context you provide, the better the response will be.`,
48
48
  `${colors.bullet('•')} Oracle is one-shot: it does not remember prior runs, so start fresh each time with full context.`,
49
49
  `${colors.bullet('•')} Run ${colors.accent('--files-report')} to inspect token spend before hitting the API.`,
50
- `${colors.bullet('•')} Non-preview runs spawn detached sessions so they keep streaming even if your terminal closes — reattach anytime via ${colors.accent('pnpm oracle session <slug>')}.`,
50
+ `${colors.bullet('•')} Non-preview runs spawn detached sessions (especially gpt-5-pro API). If the CLI times out, do not re-run — reattach with ${colors.accent('oracle session <slug>')} to resume/inspect the existing run.`,
51
51
  `${colors.bullet('•')} Set a memorable 3–5 word slug via ${colors.accent('--slug "<words>"')} to keep session IDs tidy.`,
52
52
  `${colors.bullet('•')} Finished sessions auto-hide preamble logs when reattached; raw timestamps remain in the saved log file.`,
53
53
  `${colors.bullet('•')} Need hidden flags? Run ${colors.accent(`${program.name()} --help --verbose`)} to list search/token/browser overrides.`,
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Normalize hidden alias flags so they behave like their primary counterparts.
3
+ *
4
+ * - `--message` maps to `--prompt` when no prompt is provided.
5
+ * - `--include` extends the `--file` list.
6
+ */
7
+ export function applyHiddenAliases(options, setOptionValue) {
8
+ if (options.include && options.include.length > 0) {
9
+ const mergedFiles = [...(options.file ?? []), ...options.include];
10
+ options.file = mergedFiles;
11
+ setOptionValue?.('file', mergedFiles);
12
+ }
13
+ if (!options.prompt && options.message) {
14
+ options.prompt = options.message;
15
+ setOptionValue?.('prompt', options.message);
16
+ }
17
+ }
@@ -0,0 +1,97 @@
1
+ import chalk from 'chalk';
2
+ import { render as renderMarkdown } from 'markdansi';
3
+ import { bundledLanguages, bundledThemes, createHighlighter, } from 'shiki';
4
+ const DEFAULT_THEME = 'github-dark';
5
+ const HIGHLIGHT_LANGS = ['ts', 'tsx', 'js', 'jsx', 'json', 'swift'];
6
+ const SUPPORTED_LANG_ALIASES = {
7
+ ts: 'ts',
8
+ typescript: 'ts',
9
+ tsx: 'tsx',
10
+ js: 'js',
11
+ javascript: 'js',
12
+ jsx: 'jsx',
13
+ json: 'json',
14
+ swift: 'swift',
15
+ };
16
+ const shikiPromise = createHighlighter({
17
+ themes: [bundledThemes[DEFAULT_THEME]],
18
+ langs: HIGHLIGHT_LANGS.map((lang) => bundledLanguages[lang]),
19
+ });
20
+ let shiki = null;
21
+ void shikiPromise
22
+ .then((instance) => {
23
+ shiki = instance;
24
+ })
25
+ .catch(() => {
26
+ shiki = null;
27
+ });
28
+ export async function ensureShikiReady() {
29
+ if (shiki)
30
+ return;
31
+ try {
32
+ shiki = await shikiPromise;
33
+ }
34
+ catch {
35
+ shiki = null;
36
+ }
37
+ }
38
+ function normalizeLanguage(lang) {
39
+ if (!lang)
40
+ return null;
41
+ const key = lang.toLowerCase();
42
+ return SUPPORTED_LANG_ALIASES[key] ?? null;
43
+ }
44
+ function styleToken(text, fontStyle = 0) {
45
+ let styled = text;
46
+ if (fontStyle & 1)
47
+ styled = chalk.italic(styled);
48
+ if (fontStyle & 2)
49
+ styled = chalk.bold(styled);
50
+ if (fontStyle & 4)
51
+ styled = chalk.underline(styled);
52
+ if (fontStyle & 8)
53
+ styled = chalk.strikethrough(styled);
54
+ return styled;
55
+ }
56
+ function shikiHighlighter(code, lang) {
57
+ if (!process.stdout.isTTY || !shiki)
58
+ return code;
59
+ const normalizedLang = normalizeLanguage(lang);
60
+ if (!normalizedLang)
61
+ return code;
62
+ try {
63
+ if (!shiki.getLoadedLanguages().includes(normalizedLang)) {
64
+ return code;
65
+ }
66
+ const { tokens } = shiki.codeToTokens(code, { lang: normalizedLang, theme: DEFAULT_THEME });
67
+ return tokens
68
+ .map((line) => line
69
+ .map((token) => {
70
+ const colored = token.color ? chalk.hex(token.color)(token.content) : token.content;
71
+ return styleToken(colored, token.fontStyle);
72
+ })
73
+ .join(''))
74
+ .join('\n');
75
+ }
76
+ catch {
77
+ return code;
78
+ }
79
+ }
80
+ export function renderMarkdownAnsi(markdown) {
81
+ try {
82
+ const color = Boolean(process.stdout.isTTY);
83
+ const width = process.stdout.columns;
84
+ const hyperlinks = color; // enable OSC 8 only when we have color/TTY
85
+ return renderMarkdown(markdown, {
86
+ color,
87
+ width,
88
+ wrap: true,
89
+ hyperlinks,
90
+ highlighter: color ? shikiHighlighter : undefined,
91
+ });
92
+ }
93
+ catch {
94
+ // Last-resort fallback: return the raw markdown so we never crash.
95
+ return markdown;
96
+ }
97
+ }
@@ -0,0 +1,223 @@
1
+ import notifier from 'toasted-notifier';
2
+ import { spawn } from 'node:child_process';
3
+ import { formatUSD, formatNumber } from '../oracle/format.js';
4
+ import { MODEL_CONFIGS } from '../oracle/config.js';
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import { createRequire } from 'node:module';
8
+ import { fileURLToPath } from 'node:url';
9
+ const ORACLE_EMOJI = '🧿';
10
+ export function resolveNotificationSettings({ cliNotify, cliNotifySound, env, config, }) {
11
+ const defaultEnabled = !(bool(env.CI) || bool(env.SSH_CONNECTION) || muteByConfig(env, config));
12
+ const envNotify = parseToggle(env.ORACLE_NOTIFY);
13
+ const envSound = parseToggle(env.ORACLE_NOTIFY_SOUND);
14
+ const enabled = cliNotify ?? envNotify ?? config?.enabled ?? defaultEnabled;
15
+ const sound = cliNotifySound ?? envSound ?? config?.sound ?? false;
16
+ return { enabled, sound };
17
+ }
18
+ export function deriveNotificationSettingsFromMetadata(metadata, env, config) {
19
+ if (metadata?.notifications) {
20
+ return metadata.notifications;
21
+ }
22
+ return resolveNotificationSettings({ cliNotify: undefined, cliNotifySound: undefined, env, config });
23
+ }
24
+ export async function sendSessionNotification(payload, settings, log, answerPreview) {
25
+ if (!settings.enabled || isTestEnv(process.env)) {
26
+ return;
27
+ }
28
+ const title = `Oracle${ORACLE_EMOJI} finished`;
29
+ const message = buildMessage(payload, sanitizePreview(answerPreview));
30
+ try {
31
+ if (await tryMacNativeNotifier(title, message, settings)) {
32
+ return;
33
+ }
34
+ // Fallback to toasted-notifier (cross-platform). macAppIconOption() is only honored on macOS.
35
+ await notifier.notify({
36
+ title,
37
+ message,
38
+ sound: settings.sound,
39
+ });
40
+ }
41
+ catch (error) {
42
+ if (isMacExecError(error)) {
43
+ const repaired = await repairMacNotifier(log);
44
+ if (repaired) {
45
+ try {
46
+ await notifier.notify({ title, message, sound: settings.sound, ...(macAppIconOption()) });
47
+ return;
48
+ }
49
+ catch (retryError) {
50
+ const reason = retryError instanceof Error ? retryError.message : String(retryError);
51
+ log(`(notify skipped after retry: ${reason})`);
52
+ return;
53
+ }
54
+ }
55
+ }
56
+ const reason = error instanceof Error ? error.message : String(error);
57
+ log(`(notify skipped: ${reason})`);
58
+ }
59
+ }
60
+ function buildMessage(payload, answerPreview) {
61
+ const parts = [];
62
+ const sessionLabel = payload.sessionName || payload.sessionId;
63
+ parts.push(sessionLabel);
64
+ // Show cost only for API runs.
65
+ if (payload.mode === 'api') {
66
+ const cost = payload.costUsd ?? inferCost(payload);
67
+ if (cost !== undefined) {
68
+ // Round to $0.00 for a concise toast.
69
+ parts.push(formatUSD(Number(cost.toFixed(2))));
70
+ }
71
+ }
72
+ if (payload.characters != null) {
73
+ parts.push(`${formatNumber(payload.characters)} chars`);
74
+ }
75
+ if (answerPreview) {
76
+ parts.push(answerPreview);
77
+ }
78
+ return parts.join(' · ');
79
+ }
80
+ function sanitizePreview(preview) {
81
+ if (!preview)
82
+ return undefined;
83
+ let text = preview;
84
+ // Strip code fences and inline code markers.
85
+ text = text.replace(/```[\s\S]*?```/g, ' ');
86
+ text = text.replace(/`([^`]+)`/g, '$1');
87
+ // Convert markdown links and images to their visible text.
88
+ text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1');
89
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
90
+ // Drop bold/italic markers.
91
+ text = text.replace(/(\*\*|__|\*|_)/g, '');
92
+ // Remove headings / list markers / blockquotes.
93
+ text = text.replace(/^\s*#+\s*/gm, '');
94
+ text = text.replace(/^\s*[-*+]\s+/gm, '');
95
+ text = text.replace(/^\s*>\s+/gm, '');
96
+ // Collapse whitespace and trim.
97
+ text = text.replace(/\s+/g, ' ').trim();
98
+ // Limit length to keep notifications short.
99
+ const max = 200;
100
+ if (text.length > max) {
101
+ text = `${text.slice(0, max - 1)}…`;
102
+ }
103
+ return text;
104
+ }
105
+ // Exposed for unit tests only.
106
+ export const testHelpers = { sanitizePreview };
107
+ function inferCost(payload) {
108
+ const model = payload.model;
109
+ const usage = payload.usage;
110
+ if (!model || !usage)
111
+ return undefined;
112
+ const config = MODEL_CONFIGS[model];
113
+ if (!config)
114
+ return undefined;
115
+ return (usage.inputTokens * config.pricing.inputPerToken +
116
+ usage.outputTokens * config.pricing.outputPerToken);
117
+ }
118
+ function parseToggle(value) {
119
+ if (value == null)
120
+ return undefined;
121
+ const normalized = value.trim().toLowerCase();
122
+ if (['1', 'true', 'yes', 'on'].includes(normalized))
123
+ return true;
124
+ if (['0', 'false', 'no', 'off'].includes(normalized))
125
+ return false;
126
+ return undefined;
127
+ }
128
+ function bool(value) {
129
+ return Boolean(value && String(value).length > 0);
130
+ }
131
+ function isMacExecError(error) {
132
+ return Boolean(process.platform === 'darwin' &&
133
+ error &&
134
+ typeof error === 'object' &&
135
+ 'code' in error &&
136
+ error.code === 'EACCES');
137
+ }
138
+ async function repairMacNotifier(log) {
139
+ const binPath = macNotifierPath();
140
+ if (!binPath)
141
+ return false;
142
+ try {
143
+ await fs.chmod(binPath, 0o755);
144
+ return true;
145
+ }
146
+ catch (chmodError) {
147
+ const reason = chmodError instanceof Error ? chmodError.message : String(chmodError);
148
+ log(`(notify repair failed: ${reason} — try: xattr -dr com.apple.quarantine "${path.dirname(binPath)}")`);
149
+ return false;
150
+ }
151
+ }
152
+ function macNotifierPath() {
153
+ if (process.platform !== 'darwin')
154
+ return null;
155
+ try {
156
+ const req = createRequire(import.meta.url);
157
+ const modPath = req.resolve('toasted-notifier');
158
+ const base = path.dirname(modPath);
159
+ return path.join(base, 'vendor', 'mac.noindex', 'terminal-notifier.app', 'Contents', 'MacOS', 'terminal-notifier');
160
+ }
161
+ catch {
162
+ return null;
163
+ }
164
+ }
165
+ function macAppIconOption() {
166
+ if (process.platform !== 'darwin')
167
+ return {};
168
+ const iconPaths = [
169
+ path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../assets-oracle-icon.png'),
170
+ path.resolve(process.cwd(), 'assets-oracle-icon.png'),
171
+ ];
172
+ for (const candidate of iconPaths) {
173
+ if (candidate && fsExistsSync(candidate)) {
174
+ return { appIcon: candidate };
175
+ }
176
+ }
177
+ return {};
178
+ }
179
+ function fsExistsSync(target) {
180
+ try {
181
+ return Boolean(require('node:fs').statSync(target));
182
+ }
183
+ catch {
184
+ return false;
185
+ }
186
+ }
187
+ async function tryMacNativeNotifier(title, message, settings) {
188
+ const binary = macNativeNotifierPath();
189
+ if (!binary)
190
+ return false;
191
+ return new Promise((resolve) => {
192
+ const child = spawn(binary, [title, message, settings.sound ? 'Glass' : ''], {
193
+ stdio: 'ignore',
194
+ });
195
+ child.on('error', () => resolve(false));
196
+ child.on('exit', (code) => resolve(code === 0));
197
+ });
198
+ }
199
+ function macNativeNotifierPath() {
200
+ if (process.platform !== 'darwin')
201
+ return null;
202
+ const candidates = [
203
+ path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier'),
204
+ path.resolve(process.cwd(), 'vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier'),
205
+ ];
206
+ for (const candidate of candidates) {
207
+ if (fsExistsSync(candidate)) {
208
+ return candidate;
209
+ }
210
+ }
211
+ return null;
212
+ }
213
+ function muteByConfig(env, config) {
214
+ if (!config?.muteIn)
215
+ return false;
216
+ return ((config.muteIn.includes('CI') && bool(env.CI)) ||
217
+ (config.muteIn.includes('SSH') && bool(env.SSH_CONNECTION)));
218
+ }
219
+ function isTestEnv(env) {
220
+ return (env.ORACLE_DISABLE_NOTIFICATIONS === '1' ||
221
+ env.NODE_ENV === 'test' ||
222
+ Boolean(env.VITEST || env.VITEST_WORKER_ID || env.JEST_WORKER_ID));
223
+ }
@@ -2,6 +2,9 @@
2
2
  * Determine whether the CLI should enforce a prompt requirement based on raw args and options.
3
3
  */
4
4
  export function shouldRequirePrompt(rawArgs, options) {
5
+ if (rawArgs.length === 0) {
6
+ return !options.prompt;
7
+ }
5
8
  const firstArg = rawArgs[0];
6
9
  const bypassPrompt = Boolean(options.session ||
7
10
  options.execSession ||
@@ -14,3 +14,17 @@ export async function handleStatusFlag(options, deps = defaultDeps) {
14
14
  await deps.showStatus({ hours: 24, includeAll: false, limit: 100, showExamples: true });
15
15
  return true;
16
16
  }
17
+ const defaultSessionDeps = {
18
+ attachSession,
19
+ };
20
+ /**
21
+ * Hidden root-level alias to attach to a stored session (`--session <id>`).
22
+ * Returns true when the alias was handled so callers can short-circuit.
23
+ */
24
+ export async function handleSessionAlias(options, deps = defaultSessionDeps) {
25
+ if (!options.session) {
26
+ return false;
27
+ }
28
+ await deps.attachSession(options.session);
29
+ return true;
30
+ }
@@ -0,0 +1,29 @@
1
+ import { resolveEngine } from './engine.js';
2
+ import { normalizeModelOption, inferModelFromLabel, resolveApiModel } from './options.js';
3
+ export function resolveRunOptionsFromConfig({ prompt, files = [], model, engine, userConfig, env = process.env, }) {
4
+ const resolvedEngine = resolveEngineWithConfig({ engine, configEngine: userConfig?.engine, env });
5
+ const cliModelArg = normalizeModelOption(model ?? userConfig?.model) || 'gpt-5-pro';
6
+ const resolvedModel = resolvedEngine === 'browser' ? inferModelFromLabel(cliModelArg) : resolveApiModel(cliModelArg);
7
+ const promptWithSuffix = userConfig?.promptSuffix && userConfig.promptSuffix.trim().length > 0
8
+ ? `${prompt.trim()}\n${userConfig.promptSuffix}`
9
+ : prompt;
10
+ const search = userConfig?.search !== 'off';
11
+ const heartbeatIntervalMs = userConfig?.heartbeatSeconds !== undefined ? userConfig.heartbeatSeconds * 1000 : 30_000;
12
+ const runOptions = {
13
+ prompt: promptWithSuffix,
14
+ model: resolvedModel,
15
+ file: files ?? [],
16
+ search,
17
+ heartbeatIntervalMs,
18
+ filesReport: userConfig?.filesReport,
19
+ background: userConfig?.background,
20
+ };
21
+ return { runOptions, resolvedEngine };
22
+ }
23
+ function resolveEngineWithConfig({ engine, configEngine, env, }) {
24
+ if (engine)
25
+ return engine;
26
+ if (configEngine)
27
+ return configEngine;
28
+ return resolveEngine({ engine: undefined, env });
29
+ }
@@ -1,14 +1,25 @@
1
+ import chalk from 'chalk';
1
2
  import { usesDefaultStatusFilters } from './options.js';
2
3
  import { attachSession, showStatus } from './sessionDisplay.js';
3
- import { deleteSessionsOlderThan } from '../sessionManager.js';
4
+ import { deleteSessionsOlderThan, getSessionPaths } from '../sessionManager.js';
4
5
  const defaultDependencies = {
5
6
  showStatus,
6
7
  attachSession,
7
8
  usesDefaultStatusFilters,
8
9
  deleteSessionsOlderThan,
10
+ getSessionPaths,
9
11
  };
12
+ const SESSION_OPTION_KEYS = new Set(['hours', 'limit', 'all', 'clear', 'clean', 'render', 'renderMarkdown', 'path']);
10
13
  export async function handleSessionCommand(sessionId, command, deps = defaultDependencies) {
11
14
  const sessionOptions = command.opts();
15
+ if (sessionOptions.verboseRender) {
16
+ process.env.ORACLE_VERBOSE_RENDER = '1';
17
+ }
18
+ const renderSource = command.getOptionValueSource?.('render');
19
+ const renderMarkdownSource = command.getOptionValueSource?.('renderMarkdown');
20
+ const renderExplicit = renderSource === 'cli' || renderMarkdownSource === 'cli';
21
+ const autoRender = !renderExplicit && process.stdout.isTTY;
22
+ const pathRequested = Boolean(sessionOptions.path);
12
23
  const clearRequested = Boolean(sessionOptions.clear || sessionOptions.clean);
13
24
  if (clearRequested) {
14
25
  if (sessionId) {
@@ -28,6 +39,28 @@ export async function handleSessionCommand(sessionId, command, deps = defaultDep
28
39
  process.exitCode = 1;
29
40
  return;
30
41
  }
42
+ if (pathRequested) {
43
+ if (!sessionId) {
44
+ console.error('The --path flag requires a session ID.');
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+ try {
49
+ const paths = await deps.getSessionPaths(sessionId);
50
+ const richTty = Boolean(process.stdout.isTTY && chalk.level > 0);
51
+ const label = (text) => (richTty ? chalk.cyan(text) : text);
52
+ const value = (text) => (richTty ? chalk.dim(text) : text);
53
+ console.log(`${label('Session dir:')} ${value(paths.dir)}`);
54
+ console.log(`${label('Metadata:')} ${value(paths.metadata)}`);
55
+ console.log(`${label('Request:')} ${value(paths.request)}`);
56
+ console.log(`${label('Log:')} ${value(paths.log)}`);
57
+ }
58
+ catch (error) {
59
+ console.error(error instanceof Error ? error.message : String(error));
60
+ process.exitCode = 1;
61
+ }
62
+ return;
63
+ }
31
64
  if (!sessionId) {
32
65
  const showExamples = deps.usesDefaultStatusFilters(command);
33
66
  await deps.showStatus({
@@ -38,7 +71,13 @@ export async function handleSessionCommand(sessionId, command, deps = defaultDep
38
71
  });
39
72
  return;
40
73
  }
41
- await deps.attachSession(sessionId);
74
+ // Surface any root-level flags that were provided but are ignored when attaching to a session.
75
+ const ignoredFlags = listIgnoredFlags(command);
76
+ if (ignoredFlags.length > 0) {
77
+ console.log(`Ignoring flags on session attach: ${ignoredFlags.join(', ')}`);
78
+ }
79
+ const renderMarkdown = Boolean(sessionOptions.render || sessionOptions.renderMarkdown || autoRender);
80
+ await deps.attachSession(sessionId, { renderMarkdown, renderPrompt: !sessionOptions.hidePrompt });
42
81
  }
43
82
  export function formatSessionCleanupMessage(result, scope) {
44
83
  const deletedLabel = `${result.deleted} ${result.deleted === 1 ? 'session' : 'sessions'}`;
@@ -46,3 +85,22 @@ export function formatSessionCleanupMessage(result, scope) {
46
85
  const hint = 'Run "oracle session --clear --all" to delete everything.';
47
86
  return `Deleted ${deletedLabel} (${scope}). ${remainingLabel}.\n${hint}`;
48
87
  }
88
+ function listIgnoredFlags(command) {
89
+ const opts = command.optsWithGlobals();
90
+ const ignored = [];
91
+ for (const key of Object.keys(opts)) {
92
+ if (SESSION_OPTION_KEYS.has(key)) {
93
+ continue;
94
+ }
95
+ const source = command.getOptionValueSource?.(key);
96
+ if (source !== 'cli' && source !== 'env') {
97
+ continue;
98
+ }
99
+ const value = opts[key];
100
+ if (value === undefined || value === false || value === null) {
101
+ continue;
102
+ }
103
+ ignored.push(key);
104
+ }
105
+ return ignored;
106
+ }