@steipete/oracle 0.4.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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/assets-oracle-icon.png +0 -0
  4. package/dist/bin/oracle-cli.js +954 -0
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/bin/oracle.js +683 -0
  7. package/dist/markdansi/types/index.js +4 -0
  8. package/dist/oracle/bin/oracle-cli.js +472 -0
  9. package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
  10. package/dist/oracle/src/browser/actions/attachments.js +82 -0
  11. package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
  12. package/dist/oracle/src/browser/actions/navigation.js +75 -0
  13. package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
  14. package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
  15. package/dist/oracle/src/browser/config.js +33 -0
  16. package/dist/oracle/src/browser/constants.js +40 -0
  17. package/dist/oracle/src/browser/cookies.js +210 -0
  18. package/dist/oracle/src/browser/domDebug.js +36 -0
  19. package/dist/oracle/src/browser/index.js +331 -0
  20. package/dist/oracle/src/browser/pageActions.js +5 -0
  21. package/dist/oracle/src/browser/prompt.js +88 -0
  22. package/dist/oracle/src/browser/promptSummary.js +20 -0
  23. package/dist/oracle/src/browser/sessionRunner.js +80 -0
  24. package/dist/oracle/src/browser/types.js +1 -0
  25. package/dist/oracle/src/browser/utils.js +62 -0
  26. package/dist/oracle/src/browserMode.js +1 -0
  27. package/dist/oracle/src/cli/browserConfig.js +44 -0
  28. package/dist/oracle/src/cli/dryRun.js +59 -0
  29. package/dist/oracle/src/cli/engine.js +17 -0
  30. package/dist/oracle/src/cli/errorUtils.js +9 -0
  31. package/dist/oracle/src/cli/help.js +70 -0
  32. package/dist/oracle/src/cli/markdownRenderer.js +15 -0
  33. package/dist/oracle/src/cli/options.js +103 -0
  34. package/dist/oracle/src/cli/promptRequirement.js +14 -0
  35. package/dist/oracle/src/cli/rootAlias.js +30 -0
  36. package/dist/oracle/src/cli/sessionCommand.js +77 -0
  37. package/dist/oracle/src/cli/sessionDisplay.js +270 -0
  38. package/dist/oracle/src/cli/sessionRunner.js +94 -0
  39. package/dist/oracle/src/heartbeat.js +43 -0
  40. package/dist/oracle/src/oracle/client.js +48 -0
  41. package/dist/oracle/src/oracle/config.js +29 -0
  42. package/dist/oracle/src/oracle/errors.js +101 -0
  43. package/dist/oracle/src/oracle/files.js +220 -0
  44. package/dist/oracle/src/oracle/format.js +33 -0
  45. package/dist/oracle/src/oracle/fsAdapter.js +7 -0
  46. package/dist/oracle/src/oracle/oscProgress.js +60 -0
  47. package/dist/oracle/src/oracle/request.js +48 -0
  48. package/dist/oracle/src/oracle/run.js +444 -0
  49. package/dist/oracle/src/oracle/tokenStats.js +39 -0
  50. package/dist/oracle/src/oracle/types.js +1 -0
  51. package/dist/oracle/src/oracle.js +9 -0
  52. package/dist/oracle/src/sessionManager.js +205 -0
  53. package/dist/oracle/src/version.js +39 -0
  54. package/dist/scripts/browser-tools.js +536 -0
  55. package/dist/scripts/check.js +21 -0
  56. package/dist/scripts/chrome/browser-tools.js +295 -0
  57. package/dist/scripts/run-cli.js +14 -0
  58. package/dist/src/browser/actions/assistantResponse.js +555 -0
  59. package/dist/src/browser/actions/attachments.js +82 -0
  60. package/dist/src/browser/actions/modelSelection.js +300 -0
  61. package/dist/src/browser/actions/navigation.js +175 -0
  62. package/dist/src/browser/actions/promptComposer.js +167 -0
  63. package/dist/src/browser/actions/remoteFileTransfer.js +154 -0
  64. package/dist/src/browser/chromeCookies.js +274 -0
  65. package/dist/src/browser/chromeLifecycle.js +107 -0
  66. package/dist/src/browser/config.js +49 -0
  67. package/dist/src/browser/constants.js +42 -0
  68. package/dist/src/browser/cookies.js +130 -0
  69. package/dist/src/browser/domDebug.js +36 -0
  70. package/dist/src/browser/index.js +541 -0
  71. package/dist/src/browser/keytarShim.js +56 -0
  72. package/dist/src/browser/pageActions.js +5 -0
  73. package/dist/src/browser/policies.js +43 -0
  74. package/dist/src/browser/prompt.js +82 -0
  75. package/dist/src/browser/promptSummary.js +20 -0
  76. package/dist/src/browser/sessionRunner.js +96 -0
  77. package/dist/src/browser/types.js +1 -0
  78. package/dist/src/browser/utils.js +112 -0
  79. package/dist/src/browser/windowsCookies.js +218 -0
  80. package/dist/src/browserMode.js +1 -0
  81. package/dist/src/cli/browserConfig.js +193 -0
  82. package/dist/src/cli/bundleWarnings.js +9 -0
  83. package/dist/src/cli/clipboard.js +10 -0
  84. package/dist/src/cli/detach.js +11 -0
  85. package/dist/src/cli/dryRun.js +103 -0
  86. package/dist/src/cli/duplicatePromptGuard.js +14 -0
  87. package/dist/src/cli/engine.js +25 -0
  88. package/dist/src/cli/errorUtils.js +9 -0
  89. package/dist/src/cli/format.js +13 -0
  90. package/dist/src/cli/help.js +77 -0
  91. package/dist/src/cli/hiddenAliases.js +22 -0
  92. package/dist/src/cli/markdownBundle.js +17 -0
  93. package/dist/src/cli/markdownRenderer.js +97 -0
  94. package/dist/src/cli/notifier.js +300 -0
  95. package/dist/src/cli/options.js +193 -0
  96. package/dist/src/cli/oscUtils.js +20 -0
  97. package/dist/src/cli/promptRequirement.js +17 -0
  98. package/dist/src/cli/renderFlags.js +9 -0
  99. package/dist/src/cli/renderOutput.js +26 -0
  100. package/dist/src/cli/rootAlias.js +30 -0
  101. package/dist/src/cli/runOptions.js +62 -0
  102. package/dist/src/cli/sessionCommand.js +111 -0
  103. package/dist/src/cli/sessionDisplay.js +540 -0
  104. package/dist/src/cli/sessionRunner.js +419 -0
  105. package/dist/src/cli/tagline.js +258 -0
  106. package/dist/src/cli/tui/index.js +520 -0
  107. package/dist/src/cli/writeOutputPath.js +21 -0
  108. package/dist/src/config.js +27 -0
  109. package/dist/src/heartbeat.js +43 -0
  110. package/dist/src/mcp/server.js +36 -0
  111. package/dist/src/mcp/tools/consult.js +221 -0
  112. package/dist/src/mcp/tools/sessionResources.js +75 -0
  113. package/dist/src/mcp/tools/sessions.js +96 -0
  114. package/dist/src/mcp/types.js +18 -0
  115. package/dist/src/mcp/utils.js +27 -0
  116. package/dist/src/oracle/background.js +134 -0
  117. package/dist/src/oracle/claude.js +95 -0
  118. package/dist/src/oracle/client.js +87 -0
  119. package/dist/src/oracle/config.js +92 -0
  120. package/dist/src/oracle/errors.js +104 -0
  121. package/dist/src/oracle/files.js +371 -0
  122. package/dist/src/oracle/format.js +30 -0
  123. package/dist/src/oracle/fsAdapter.js +10 -0
  124. package/dist/src/oracle/gemini.js +185 -0
  125. package/dist/src/oracle/logging.js +36 -0
  126. package/dist/src/oracle/markdown.js +46 -0
  127. package/dist/src/oracle/multiModelRunner.js +164 -0
  128. package/dist/src/oracle/oscProgress.js +66 -0
  129. package/dist/src/oracle/promptAssembly.js +13 -0
  130. package/dist/src/oracle/request.js +49 -0
  131. package/dist/src/oracle/run.js +492 -0
  132. package/dist/src/oracle/runUtils.js +27 -0
  133. package/dist/src/oracle/tokenEstimate.js +37 -0
  134. package/dist/src/oracle/tokenStats.js +39 -0
  135. package/dist/src/oracle/tokenStringifier.js +24 -0
  136. package/dist/src/oracle/types.js +1 -0
  137. package/dist/src/oracle.js +12 -0
  138. package/dist/src/remote/client.js +128 -0
  139. package/dist/src/remote/server.js +294 -0
  140. package/dist/src/remote/types.js +1 -0
  141. package/dist/src/sessionManager.js +462 -0
  142. package/dist/src/sessionStore.js +56 -0
  143. package/dist/src/version.js +39 -0
  144. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  145. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  146. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  147. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  148. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  149. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  150. package/dist/vendor/oracle-notifier/README.md +24 -0
  151. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  152. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  153. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  154. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  155. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  156. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  157. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
  158. package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
  159. package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
  160. package/package.json +102 -0
  161. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  162. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  163. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  164. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  165. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  166. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  167. package/vendor/oracle-notifier/README.md +24 -0
  168. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,300 @@
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
+ if (!(await shouldSkipToastedNotifier())) {
35
+ // Fallback to toasted-notifier (cross-platform). macAppIconOption() is only honored on macOS.
36
+ await notifier.notify({
37
+ title,
38
+ message,
39
+ sound: settings.sound,
40
+ });
41
+ return;
42
+ }
43
+ }
44
+ catch (error) {
45
+ if (isMacExecError(error)) {
46
+ const repaired = await repairMacNotifier(log);
47
+ if (repaired) {
48
+ try {
49
+ await notifier.notify({ title, message, sound: settings.sound, ...(macAppIconOption()) });
50
+ return;
51
+ }
52
+ catch (retryError) {
53
+ const reason = describeNotifierError(retryError);
54
+ log(`(notify skipped after retry: ${reason})`);
55
+ return;
56
+ }
57
+ }
58
+ }
59
+ if (isMacBadCpuError(error)) {
60
+ const reason = describeNotifierError(error);
61
+ log(`(notify skipped: ${reason})`);
62
+ return;
63
+ }
64
+ const reason = describeNotifierError(error);
65
+ log(`(notify skipped: ${reason})`);
66
+ }
67
+ // Last-resort macOS fallback: AppleScript alert (simple, noisy, but works when helpers are blocked).
68
+ if (process.platform === 'darwin') {
69
+ try {
70
+ await sendOsascriptAlert(title, message, log);
71
+ return;
72
+ }
73
+ catch (scriptError) {
74
+ const reason = describeNotifierError(scriptError);
75
+ log(`(notify skipped: osascript fallback failed: ${reason})`);
76
+ }
77
+ }
78
+ }
79
+ function buildMessage(payload, answerPreview) {
80
+ const parts = [];
81
+ const sessionLabel = payload.sessionName || payload.sessionId;
82
+ parts.push(sessionLabel);
83
+ // Show cost only for API runs.
84
+ if (payload.mode === 'api') {
85
+ const cost = payload.costUsd ?? inferCost(payload);
86
+ if (cost !== undefined) {
87
+ // Round to $0.00 for a concise toast.
88
+ parts.push(formatUSD(Number(cost.toFixed(2))));
89
+ }
90
+ }
91
+ if (payload.characters != null) {
92
+ parts.push(`${formatNumber(payload.characters)} chars`);
93
+ }
94
+ if (answerPreview) {
95
+ parts.push(answerPreview);
96
+ }
97
+ return parts.join(' · ');
98
+ }
99
+ function sanitizePreview(preview) {
100
+ if (!preview)
101
+ return undefined;
102
+ let text = preview;
103
+ // Strip code fences and inline code markers.
104
+ text = text.replace(/```[\s\S]*?```/g, ' ');
105
+ text = text.replace(/`([^`]+)`/g, '$1');
106
+ // Convert markdown links and images to their visible text.
107
+ text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1');
108
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
109
+ // Drop bold/italic markers.
110
+ text = text.replace(/(\*\*|__|\*|_)/g, '');
111
+ // Remove headings / list markers / blockquotes.
112
+ text = text.replace(/^\s*#+\s*/gm, '');
113
+ text = text.replace(/^\s*[-*+]\s+/gm, '');
114
+ text = text.replace(/^\s*>\s+/gm, '');
115
+ // Collapse whitespace and trim.
116
+ text = text.replace(/\s+/g, ' ').trim();
117
+ // Limit length to keep notifications short.
118
+ const max = 200;
119
+ if (text.length > max) {
120
+ text = `${text.slice(0, max - 1)}…`;
121
+ }
122
+ return text;
123
+ }
124
+ // Exposed for unit tests only.
125
+ export const testHelpers = { sanitizePreview };
126
+ function inferCost(payload) {
127
+ const model = payload.model;
128
+ const usage = payload.usage;
129
+ if (!model || !usage)
130
+ return undefined;
131
+ const config = MODEL_CONFIGS[model];
132
+ if (!config?.pricing)
133
+ return undefined;
134
+ return (usage.inputTokens * config.pricing.inputPerToken +
135
+ usage.outputTokens * config.pricing.outputPerToken);
136
+ }
137
+ function parseToggle(value) {
138
+ if (value == null)
139
+ return undefined;
140
+ const normalized = value.trim().toLowerCase();
141
+ if (['1', 'true', 'yes', 'on'].includes(normalized))
142
+ return true;
143
+ if (['0', 'false', 'no', 'off'].includes(normalized))
144
+ return false;
145
+ return undefined;
146
+ }
147
+ function bool(value) {
148
+ return Boolean(value && String(value).length > 0);
149
+ }
150
+ function isMacExecError(error) {
151
+ return Boolean(process.platform === 'darwin' &&
152
+ error &&
153
+ typeof error === 'object' &&
154
+ 'code' in error &&
155
+ error.code === 'EACCES');
156
+ }
157
+ function isMacBadCpuError(error) {
158
+ return Boolean(process.platform === 'darwin' &&
159
+ error &&
160
+ typeof error === 'object' &&
161
+ 'errno' in error &&
162
+ error.errno === -86);
163
+ }
164
+ async function repairMacNotifier(log) {
165
+ const binPath = macNotifierPath();
166
+ if (!binPath)
167
+ return false;
168
+ try {
169
+ await fs.chmod(binPath, 0o755);
170
+ return true;
171
+ }
172
+ catch (chmodError) {
173
+ const reason = chmodError instanceof Error ? chmodError.message : String(chmodError);
174
+ log(`(notify repair failed: ${reason} — try: xattr -dr com.apple.quarantine "${path.dirname(binPath)}")`);
175
+ return false;
176
+ }
177
+ }
178
+ function macNotifierPath() {
179
+ if (process.platform !== 'darwin')
180
+ return null;
181
+ try {
182
+ const req = createRequire(import.meta.url);
183
+ const modPath = req.resolve('toasted-notifier');
184
+ const base = path.dirname(modPath);
185
+ return path.join(base, 'vendor', 'mac.noindex', 'terminal-notifier.app', 'Contents', 'MacOS', 'terminal-notifier');
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ }
191
+ async function shouldSkipToastedNotifier() {
192
+ if (process.platform !== 'darwin')
193
+ return false;
194
+ // On Apple Silicon without Rosetta, prefer the native helper and skip x86-only fallback.
195
+ const arch = process.arch;
196
+ if (arch !== 'arm64')
197
+ return false;
198
+ return !(await hasRosetta());
199
+ }
200
+ async function hasRosetta() {
201
+ return new Promise((resolve) => {
202
+ const child = spawn('pkgutil', ['--files', 'com.apple.pkg.RosettaUpdateAuto'], { stdio: 'ignore' });
203
+ child.on('exit', (code) => resolve(code === 0));
204
+ child.on('error', () => resolve(false));
205
+ });
206
+ }
207
+ async function sendOsascriptAlert(title, message, _log) {
208
+ return new Promise((resolve, reject) => {
209
+ const child = spawn('osascript', ['-e', `display notification "${escapeAppleScript(message)}" with title "${escapeAppleScript(title)}"`], {
210
+ stdio: 'ignore',
211
+ });
212
+ child.on('exit', (code) => {
213
+ if (code === 0) {
214
+ resolve();
215
+ }
216
+ else {
217
+ reject(new Error(`osascript exited with code ${code ?? -1}`));
218
+ }
219
+ });
220
+ child.on('error', reject);
221
+ });
222
+ }
223
+ function escapeAppleScript(value) {
224
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
225
+ }
226
+ function macAppIconOption() {
227
+ if (process.platform !== 'darwin')
228
+ return {};
229
+ const iconPaths = [
230
+ path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../assets-oracle-icon.png'),
231
+ path.resolve(process.cwd(), 'assets-oracle-icon.png'),
232
+ ];
233
+ for (const candidate of iconPaths) {
234
+ if (candidate && fsExistsSync(candidate)) {
235
+ return { appIcon: candidate };
236
+ }
237
+ }
238
+ return {};
239
+ }
240
+ function fsExistsSync(target) {
241
+ try {
242
+ return Boolean(require('node:fs').statSync(target));
243
+ }
244
+ catch {
245
+ return false;
246
+ }
247
+ }
248
+ async function tryMacNativeNotifier(title, message, settings) {
249
+ const binary = macNativeNotifierPath();
250
+ if (!binary)
251
+ return false;
252
+ return new Promise((resolve) => {
253
+ const child = spawn(binary, [title, message, settings.sound ? 'Glass' : ''], {
254
+ stdio: 'ignore',
255
+ });
256
+ child.on('error', () => resolve(false));
257
+ child.on('exit', (code) => resolve(code === 0));
258
+ });
259
+ }
260
+ function macNativeNotifierPath() {
261
+ if (process.platform !== 'darwin')
262
+ return null;
263
+ const candidates = [
264
+ path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier'),
265
+ path.resolve(process.cwd(), 'vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier'),
266
+ ];
267
+ for (const candidate of candidates) {
268
+ if (fsExistsSync(candidate)) {
269
+ return candidate;
270
+ }
271
+ }
272
+ return null;
273
+ }
274
+ function muteByConfig(env, config) {
275
+ if (!config?.muteIn)
276
+ return false;
277
+ return ((config.muteIn.includes('CI') && bool(env.CI)) ||
278
+ (config.muteIn.includes('SSH') && bool(env.SSH_CONNECTION)));
279
+ }
280
+ function isTestEnv(env) {
281
+ return (env.ORACLE_DISABLE_NOTIFICATIONS === '1' ||
282
+ env.NODE_ENV === 'test' ||
283
+ Boolean(env.VITEST || env.VITEST_WORKER_ID || env.JEST_WORKER_ID));
284
+ }
285
+ function describeNotifierError(error) {
286
+ if (error && typeof error === 'object') {
287
+ const err = error;
288
+ if (typeof err.errno === 'number' || typeof err.code === 'string') {
289
+ const errno = typeof err.errno === 'number' ? err.errno : undefined;
290
+ // macOS returns errno -86 for “Bad CPU type in executable” (e.g., wrong arch or quarantined binary).
291
+ if (errno === -86) {
292
+ return 'notifier binary failed to launch (Bad CPU type/quarantine); try xattr -dr com.apple.quarantine vendor/oracle-notifier && ./vendor/oracle-notifier/build-notifier.sh';
293
+ }
294
+ }
295
+ if (typeof err.message === 'string') {
296
+ return err.message;
297
+ }
298
+ }
299
+ return typeof error === 'string' ? error : String(error);
300
+ }
@@ -0,0 +1,193 @@
1
+ import { InvalidArgumentError } from 'commander';
2
+ import { DEFAULT_MODEL, MODEL_CONFIGS } from '../oracle.js';
3
+ export function collectPaths(value, previous = []) {
4
+ if (!value) {
5
+ return previous;
6
+ }
7
+ const nextValues = Array.isArray(value) ? value : [value];
8
+ return previous.concat(nextValues.flatMap((entry) => entry.split(',')).map((entry) => entry.trim()).filter(Boolean));
9
+ }
10
+ /**
11
+ * Merge all path-like CLI inputs (file/include aliases) into a single list, preserving order.
12
+ */
13
+ export function mergePathLikeOptions(file, include, filesAlias, pathAlias, pathsAlias) {
14
+ const withFile = collectPaths(file, []);
15
+ const withInclude = collectPaths(include, withFile);
16
+ const withFilesAlias = collectPaths(filesAlias, withInclude);
17
+ const withPathAlias = collectPaths(pathAlias, withFilesAlias);
18
+ return collectPaths(pathsAlias, withPathAlias);
19
+ }
20
+ export function collectModelList(value, previous = []) {
21
+ if (!value) {
22
+ return previous;
23
+ }
24
+ const entries = value
25
+ .split(',')
26
+ .map((entry) => entry.trim())
27
+ .filter((entry) => entry.length > 0);
28
+ return previous.concat(entries);
29
+ }
30
+ export function parseFloatOption(value) {
31
+ const parsed = Number.parseFloat(value);
32
+ if (Number.isNaN(parsed)) {
33
+ throw new InvalidArgumentError('Value must be a number.');
34
+ }
35
+ return parsed;
36
+ }
37
+ export function parseIntOption(value) {
38
+ if (value == null) {
39
+ return undefined;
40
+ }
41
+ const parsed = Number.parseInt(value, 10);
42
+ if (Number.isNaN(parsed)) {
43
+ throw new InvalidArgumentError('Value must be an integer.');
44
+ }
45
+ return parsed;
46
+ }
47
+ export function parseHeartbeatOption(value) {
48
+ if (value == null) {
49
+ return 30;
50
+ }
51
+ if (typeof value === 'number') {
52
+ if (Number.isNaN(value) || value < 0) {
53
+ throw new InvalidArgumentError('Heartbeat interval must be zero or a positive number.');
54
+ }
55
+ return value;
56
+ }
57
+ const normalized = value.trim().toLowerCase();
58
+ if (normalized.length === 0) {
59
+ return 30;
60
+ }
61
+ if (normalized === 'false' || normalized === 'off') {
62
+ return 0;
63
+ }
64
+ const parsed = Number.parseFloat(normalized);
65
+ if (Number.isNaN(parsed) || parsed < 0) {
66
+ throw new InvalidArgumentError('Heartbeat interval must be zero or a positive number.');
67
+ }
68
+ return parsed;
69
+ }
70
+ export function usesDefaultStatusFilters(cmd) {
71
+ const hoursSource = cmd.getOptionValueSource?.('hours') ?? 'default';
72
+ const limitSource = cmd.getOptionValueSource?.('limit') ?? 'default';
73
+ const allSource = cmd.getOptionValueSource?.('all') ?? 'default';
74
+ return hoursSource === 'default' && limitSource === 'default' && allSource === 'default';
75
+ }
76
+ export function resolvePreviewMode(value) {
77
+ if (typeof value === 'string' && value.length > 0) {
78
+ return value;
79
+ }
80
+ if (value === true) {
81
+ return 'summary';
82
+ }
83
+ return undefined;
84
+ }
85
+ export function parseSearchOption(value) {
86
+ const normalized = value.trim().toLowerCase();
87
+ if (['on', 'true', '1', 'yes'].includes(normalized)) {
88
+ return true;
89
+ }
90
+ if (['off', 'false', '0', 'no'].includes(normalized)) {
91
+ return false;
92
+ }
93
+ throw new InvalidArgumentError('Search mode must be "on" or "off".');
94
+ }
95
+ export function normalizeModelOption(value) {
96
+ return (value ?? '').trim();
97
+ }
98
+ export function normalizeBaseUrl(value) {
99
+ const trimmed = value?.trim();
100
+ return trimmed?.length ? trimmed : undefined;
101
+ }
102
+ export function parseTimeoutOption(value) {
103
+ if (value == null)
104
+ return undefined;
105
+ const normalized = value.trim().toLowerCase();
106
+ if (normalized === 'auto')
107
+ return 'auto';
108
+ const parsed = Number.parseFloat(normalized);
109
+ if (Number.isNaN(parsed) || parsed <= 0) {
110
+ throw new InvalidArgumentError('Timeout must be a positive number of seconds or "auto".');
111
+ }
112
+ return parsed;
113
+ }
114
+ export function resolveApiModel(modelValue) {
115
+ const normalized = normalizeModelOption(modelValue).toLowerCase();
116
+ if (normalized in MODEL_CONFIGS) {
117
+ return normalized;
118
+ }
119
+ if (normalized.includes('claude') && normalized.includes('sonnet')) {
120
+ return 'claude-4.5-sonnet';
121
+ }
122
+ if (normalized.includes('claude') && normalized.includes('opus')) {
123
+ return 'claude-4.1-opus';
124
+ }
125
+ if (normalized === 'claude' || normalized === 'sonnet' || /(^|\b)sonnet(\b|$)/.test(normalized)) {
126
+ return 'claude-4.5-sonnet';
127
+ }
128
+ if (normalized === 'opus' || normalized === 'claude-4.1') {
129
+ return 'claude-4.1-opus';
130
+ }
131
+ if (normalized.includes('5.0') || normalized === 'gpt-5-pro' || normalized === 'gpt-5') {
132
+ return 'gpt-5-pro';
133
+ }
134
+ if (normalized.includes('5-pro') && !normalized.includes('5.1')) {
135
+ return 'gpt-5-pro';
136
+ }
137
+ if (normalized.includes('5.1') && normalized.includes('pro')) {
138
+ return 'gpt-5.1-pro';
139
+ }
140
+ if (normalized.includes('codex')) {
141
+ if (normalized.includes('max')) {
142
+ throw new InvalidArgumentError('gpt-5.1-codex-max is not available yet. OpenAI has not released the API.');
143
+ }
144
+ return 'gpt-5.1-codex';
145
+ }
146
+ if (normalized.includes('gemini')) {
147
+ return 'gemini-3-pro';
148
+ }
149
+ throw new InvalidArgumentError(`Unsupported model "${modelValue}". Choose one of: ${Object.keys(MODEL_CONFIGS).join(', ')}`);
150
+ }
151
+ export function inferModelFromLabel(modelValue) {
152
+ const normalized = normalizeModelOption(modelValue).toLowerCase();
153
+ if (!normalized) {
154
+ return DEFAULT_MODEL;
155
+ }
156
+ if (normalized in MODEL_CONFIGS) {
157
+ return normalized;
158
+ }
159
+ if (normalized.includes('claude') && normalized.includes('sonnet')) {
160
+ return 'claude-4.5-sonnet';
161
+ }
162
+ if (normalized.includes('claude') && normalized.includes('opus')) {
163
+ return 'claude-4.1-opus';
164
+ }
165
+ if (normalized.includes('5.0') || normalized.includes('5-pro')) {
166
+ return 'gpt-5-pro';
167
+ }
168
+ if (normalized.includes('gpt-5') && normalized.includes('pro') && !normalized.includes('5.1')) {
169
+ return 'gpt-5-pro';
170
+ }
171
+ if (normalized.includes('codex')) {
172
+ return 'gpt-5.1-codex';
173
+ }
174
+ if (normalized.includes('gemini')) {
175
+ return 'gemini-3-pro';
176
+ }
177
+ if (normalized.includes('classic')) {
178
+ return 'gpt-5.1-pro';
179
+ }
180
+ if ((normalized.includes('5.1') || normalized.includes('5_1')) && normalized.includes('pro')) {
181
+ return 'gpt-5.1-pro';
182
+ }
183
+ if (normalized.includes('pro')) {
184
+ return 'gpt-5.1-pro';
185
+ }
186
+ if (normalized.includes('5.1') || normalized.includes('5_1')) {
187
+ return 'gpt-5.1';
188
+ }
189
+ if (normalized.includes('instant') || normalized.includes('thinking') || normalized.includes('fast')) {
190
+ return 'gpt-5.1';
191
+ }
192
+ return 'gpt-5.1';
193
+ }
@@ -0,0 +1,20 @@
1
+ // Utilities for handling OSC progress codes embedded in stored logs.
2
+ const OSC_PROGRESS_PREFIX = '\u001b]9;4;';
3
+ const OSC_END = '\u001b\\';
4
+ /**
5
+ * Optionally removes OSC 9;4 progress sequences (used by Ghostty/WezTerm to show progress bars).
6
+ * Keep them when replaying to a real TTY; strip when piping to non-TTY outputs.
7
+ */
8
+ export function sanitizeOscProgress(text, keepOsc) {
9
+ if (keepOsc) {
10
+ return text;
11
+ }
12
+ let current = text;
13
+ while (current.includes(OSC_PROGRESS_PREFIX)) {
14
+ const start = current.indexOf(OSC_PROGRESS_PREFIX);
15
+ const end = current.indexOf(OSC_END, start + OSC_PROGRESS_PREFIX.length);
16
+ const cutEnd = end === -1 ? start + OSC_PROGRESS_PREFIX.length : end + OSC_END.length;
17
+ current = `${current.slice(0, start)}${current.slice(cutEnd)}`;
18
+ }
19
+ return current;
20
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Determine whether the CLI should enforce a prompt requirement based on raw args and options.
3
+ */
4
+ export function shouldRequirePrompt(rawArgs, options) {
5
+ if (rawArgs.length === 0) {
6
+ return !options.prompt;
7
+ }
8
+ const firstArg = rawArgs[0];
9
+ const bypassPrompt = Boolean(options.session ||
10
+ options.execSession ||
11
+ options.status ||
12
+ options.debugHelp ||
13
+ firstArg === 'status' ||
14
+ firstArg === 'session');
15
+ const requiresPrompt = options.renderMarkdown || Boolean(options.preview) || Boolean(options.dryRun) || !bypassPrompt;
16
+ return requiresPrompt && !options.prompt;
17
+ }
@@ -0,0 +1,9 @@
1
+ export function resolveRenderFlag(render, renderMarkdown) {
2
+ return Boolean(renderMarkdown || render);
3
+ }
4
+ export function resolveRenderPlain(renderPlain, render, renderMarkdown) {
5
+ // Explicit plain render wins when any render flag is set; otherwise false.
6
+ if (!renderPlain)
7
+ return false;
8
+ return Boolean(renderMarkdown || render || renderPlain);
9
+ }
@@ -0,0 +1,26 @@
1
+ import { ensureShikiReady, renderMarkdownAnsi } from './markdownRenderer.js';
2
+ export function shouldRenderRich(options = {}) {
3
+ return options.richTty ?? Boolean(process.stdout.isTTY);
4
+ }
5
+ /**
6
+ * Format markdown for CLI output. Uses our ANSI renderer + syntax highlighting
7
+ * when running in a rich TTY; otherwise returns the raw markdown to avoid
8
+ * escape codes in redirected output.
9
+ */
10
+ export async function formatRenderedMarkdown(markdown, options = {}) {
11
+ const richTty = shouldRenderRich(options);
12
+ if (!richTty)
13
+ return markdown;
14
+ try {
15
+ await ensureShikiReady();
16
+ }
17
+ catch {
18
+ // If Shiki fails to init (missing terminals/themes), fall back to plain output gracefully.
19
+ }
20
+ try {
21
+ return renderMarkdownAnsi(markdown);
22
+ }
23
+ catch {
24
+ return markdown;
25
+ }
26
+ }
@@ -0,0 +1,30 @@
1
+ import { attachSession, showStatus } from './sessionDisplay.js';
2
+ const defaultDeps = {
3
+ attachSession,
4
+ showStatus,
5
+ };
6
+ export async function handleStatusFlag(options, deps = defaultDeps) {
7
+ if (!options.status) {
8
+ return false;
9
+ }
10
+ if (options.session) {
11
+ await deps.attachSession(options.session);
12
+ return true;
13
+ }
14
+ await deps.showStatus({ hours: 24, includeAll: false, limit: 100, showExamples: true });
15
+ return true;
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,62 @@
1
+ import { DEFAULT_MODEL, MODEL_CONFIGS } from '../oracle.js';
2
+ import { resolveEngine } from './engine.js';
3
+ import { normalizeModelOption, inferModelFromLabel, resolveApiModel, normalizeBaseUrl } from './options.js';
4
+ import { resolveGeminiModelId } from '../oracle/gemini.js';
5
+ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models, engine, userConfig, env = process.env, }) {
6
+ const resolvedEngine = resolveEngineWithConfig({ engine, configEngine: userConfig?.engine, env });
7
+ const browserRequested = engine === 'browser';
8
+ const requestedModelList = Array.isArray(models) ? models : [];
9
+ const normalizedRequestedModels = requestedModelList.map((entry) => normalizeModelOption(entry)).filter(Boolean);
10
+ const cliModelArg = normalizeModelOption(model ?? userConfig?.model) || DEFAULT_MODEL;
11
+ const resolvedModel = resolvedEngine === 'browser' && normalizedRequestedModels.length === 0
12
+ ? inferModelFromLabel(cliModelArg)
13
+ : resolveApiModel(cliModelArg);
14
+ const isGemini = resolvedModel.startsWith('gemini');
15
+ const isCodex = resolvedModel.startsWith('gpt-5.1-codex');
16
+ const isClaude = resolvedModel.startsWith('claude');
17
+ const engineCoercedToApi = (isGemini || isCodex || isClaude) && browserRequested;
18
+ // When Gemini, Claude, or Codex is selected, always force API engine (overrides config/env auto browser).
19
+ const fixedEngine = isGemini || isCodex || isClaude || normalizedRequestedModels.length > 0 ? 'api' : resolvedEngine;
20
+ const promptWithSuffix = userConfig?.promptSuffix && userConfig.promptSuffix.trim().length > 0
21
+ ? `${prompt.trim()}\n${userConfig.promptSuffix}`
22
+ : prompt;
23
+ const search = userConfig?.search !== 'off';
24
+ const heartbeatIntervalMs = userConfig?.heartbeatSeconds !== undefined ? userConfig.heartbeatSeconds * 1000 : 30_000;
25
+ const baseUrl = normalizeBaseUrl(userConfig?.apiBaseUrl ?? (isClaude ? env.ANTHROPIC_BASE_URL : env.OPENAI_BASE_URL));
26
+ const uniqueMultiModels = normalizedRequestedModels.length > 0
27
+ ? Array.from(new Set(normalizedRequestedModels.map((entry) => resolveApiModel(entry))))
28
+ : [];
29
+ const includesCodexMultiModel = uniqueMultiModels.some((entry) => entry.startsWith('gpt-5.1-codex'));
30
+ if (includesCodexMultiModel && browserRequested) {
31
+ // Silent coerce; multi-model still forces API.
32
+ }
33
+ const chosenModel = uniqueMultiModels[0] ?? resolvedModel;
34
+ const effectiveModelId = resolveEffectiveModelId(chosenModel);
35
+ const runOptions = {
36
+ prompt: promptWithSuffix,
37
+ model: chosenModel,
38
+ models: uniqueMultiModels.length > 0 ? uniqueMultiModels : undefined,
39
+ file: files ?? [],
40
+ search,
41
+ heartbeatIntervalMs,
42
+ filesReport: userConfig?.filesReport,
43
+ background: userConfig?.background,
44
+ baseUrl,
45
+ effectiveModelId,
46
+ };
47
+ return { runOptions, resolvedEngine: fixedEngine, engineCoercedToApi };
48
+ }
49
+ function resolveEngineWithConfig({ engine, configEngine, env, }) {
50
+ if (engine)
51
+ return engine;
52
+ if (configEngine)
53
+ return configEngine;
54
+ return resolveEngine({ engine: undefined, env });
55
+ }
56
+ function resolveEffectiveModelId(model) {
57
+ if (model.startsWith('gemini')) {
58
+ return resolveGeminiModelId(model);
59
+ }
60
+ const config = MODEL_CONFIGS[model];
61
+ return config?.apiModel ?? model;
62
+ }