@steipete/oracle 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -7
- package/assets-oracle-icon.png +0 -0
- package/dist/.DS_Store +0 -0
- package/dist/bin/oracle-cli.js +315 -47
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/src/browser/actions/modelSelection.js +117 -29
- package/dist/src/browser/config.js +6 -0
- package/dist/src/browser/cookies.js +50 -12
- package/dist/src/browser/index.js +19 -5
- package/dist/src/browser/prompt.js +6 -5
- package/dist/src/browser/sessionRunner.js +14 -3
- package/dist/src/cli/browserConfig.js +109 -2
- package/dist/src/cli/detach.js +12 -0
- package/dist/src/cli/dryRun.js +60 -8
- package/dist/src/cli/engine.js +7 -0
- package/dist/src/cli/help.js +3 -1
- package/dist/src/cli/hiddenAliases.js +17 -0
- package/dist/src/cli/markdownRenderer.js +79 -0
- package/dist/src/cli/notifier.js +223 -0
- package/dist/src/cli/options.js +22 -0
- package/dist/src/cli/promptRequirement.js +3 -0
- package/dist/src/cli/runOptions.js +43 -0
- package/dist/src/cli/sessionCommand.js +1 -1
- package/dist/src/cli/sessionDisplay.js +94 -7
- package/dist/src/cli/sessionRunner.js +32 -2
- package/dist/src/cli/tui/index.js +457 -0
- package/dist/src/config.js +27 -0
- package/dist/src/mcp/server.js +36 -0
- package/dist/src/mcp/tools/consult.js +158 -0
- package/dist/src/mcp/tools/sessionResources.js +64 -0
- package/dist/src/mcp/tools/sessions.js +106 -0
- package/dist/src/mcp/types.js +17 -0
- package/dist/src/mcp/utils.js +24 -0
- package/dist/src/oracle/client.js +24 -6
- package/dist/src/oracle/config.js +10 -0
- package/dist/src/oracle/files.js +151 -8
- package/dist/src/oracle/format.js +2 -7
- package/dist/src/oracle/fsAdapter.js +4 -1
- package/dist/src/oracle/gemini.js +161 -0
- package/dist/src/oracle/logging.js +36 -0
- package/dist/src/oracle/oscProgress.js +7 -1
- package/dist/src/oracle/run.js +148 -64
- package/dist/src/oracle/tokenEstimate.js +34 -0
- package/dist/src/oracle.js +1 -0
- package/dist/src/sessionManager.js +50 -3
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/README.md +24 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
- package/package.json +22 -6
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/vendor/oracle-notifier/build-notifier.sh +93 -0
package/dist/src/cli/dryRun.js
CHANGED
|
@@ -2,9 +2,9 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { MODEL_CONFIGS, TOKENIZER_OPTIONS, DEFAULT_SYSTEM_PROMPT, buildPrompt, readFiles, getFileTokenStats, printFileTokenStats, } from '../oracle.js';
|
|
3
3
|
import { assembleBrowserPrompt } from '../browser/prompt.js';
|
|
4
4
|
import { buildTokenEstimateSuffix, formatAttachmentLabel } from '../browser/promptSummary.js';
|
|
5
|
-
export async function runDryRunSummary({ engine, runOptions, cwd, version, log, }, deps = {}) {
|
|
5
|
+
export async function runDryRunSummary({ engine, runOptions, cwd, version, log, browserConfig, }, deps = {}) {
|
|
6
6
|
if (engine === 'browser') {
|
|
7
|
-
await runBrowserDryRun({ runOptions, cwd, version, log }, deps);
|
|
7
|
+
await runBrowserDryRun({ runOptions, cwd, version, log, browserConfig }, deps);
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
10
|
await runApiDryRun({ runOptions, cwd, version, log }, deps);
|
|
@@ -34,26 +34,78 @@ async function runApiDryRun({ runOptions, cwd, version, log, }, deps) {
|
|
|
34
34
|
});
|
|
35
35
|
printFileTokenStats(stats, { inputTokenBudget: inputBudget, log });
|
|
36
36
|
}
|
|
37
|
-
async function runBrowserDryRun({ runOptions, cwd, version, log, }, deps) {
|
|
37
|
+
async function runBrowserDryRun({ runOptions, cwd, version, log, browserConfig, }, deps) {
|
|
38
38
|
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
|
|
39
39
|
const artifacts = await assemblePromptImpl(runOptions, { cwd });
|
|
40
40
|
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
41
41
|
const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
42
42
|
log(chalk.cyan(headerLine));
|
|
43
|
-
|
|
43
|
+
logBrowserCookieStrategy(browserConfig, log, 'dry-run');
|
|
44
|
+
logBrowserFileSummary(artifacts, log, 'dry-run');
|
|
44
45
|
}
|
|
45
|
-
function
|
|
46
|
+
function logBrowserCookieStrategy(browserConfig, log, label) {
|
|
47
|
+
if (!browserConfig)
|
|
48
|
+
return;
|
|
49
|
+
if (browserConfig.inlineCookies && browserConfig.inlineCookies.length > 0) {
|
|
50
|
+
const source = browserConfig.inlineCookiesSource ?? 'inline';
|
|
51
|
+
log(chalk.bold(`[${label}] Cookies: inline payload (${browserConfig.inlineCookies.length}) via ${source}.`));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (browserConfig.cookieSync === false) {
|
|
55
|
+
log(chalk.bold(`[${label}] Cookies: sync disabled (--browser-no-cookie-sync).`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const allowlist = browserConfig.cookieNames?.length ? browserConfig.cookieNames.join(', ') : 'all from Chrome profile';
|
|
59
|
+
log(chalk.bold(`[${label}] Cookies: copy from Chrome (${allowlist}).`));
|
|
60
|
+
}
|
|
61
|
+
function logBrowserFileSummary(artifacts, log, label) {
|
|
46
62
|
if (artifacts.attachments.length > 0) {
|
|
47
|
-
|
|
63
|
+
const prefix = artifacts.bundled ? `[${label}] Bundled upload:` : `[${label}] Attachments to upload:`;
|
|
64
|
+
log(chalk.bold(prefix));
|
|
48
65
|
artifacts.attachments.forEach((attachment) => {
|
|
49
66
|
log(` • ${formatAttachmentLabel(attachment)}`);
|
|
50
67
|
});
|
|
68
|
+
if (artifacts.bundled) {
|
|
69
|
+
log(chalk.dim(` (bundled ${artifacts.bundled.originalCount} files into ${artifacts.bundled.bundlePath})`));
|
|
70
|
+
}
|
|
51
71
|
return;
|
|
52
72
|
}
|
|
53
73
|
if (artifacts.inlineFileCount > 0) {
|
|
54
|
-
log(chalk.bold(
|
|
74
|
+
log(chalk.bold(`[${label}] Inline file content:`));
|
|
55
75
|
log(` • ${artifacts.inlineFileCount} file${artifacts.inlineFileCount === 1 ? '' : 's'} pasted directly into the composer.`);
|
|
56
76
|
return;
|
|
57
77
|
}
|
|
58
|
-
log(chalk.dim(
|
|
78
|
+
log(chalk.dim(`[${label}] No files attached.`));
|
|
79
|
+
}
|
|
80
|
+
export async function runBrowserPreview({ runOptions, cwd, version, previewMode, log, }, deps = {}) {
|
|
81
|
+
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
|
|
82
|
+
const artifacts = await assemblePromptImpl(runOptions, { cwd });
|
|
83
|
+
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
84
|
+
const headerLine = `[preview] Oracle (${version}) browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
85
|
+
log(chalk.cyan(headerLine));
|
|
86
|
+
logBrowserFileSummary(artifacts, log, 'preview');
|
|
87
|
+
if (previewMode === 'json' || previewMode === 'full') {
|
|
88
|
+
const attachmentSummary = artifacts.attachments.map((attachment) => ({
|
|
89
|
+
path: attachment.path,
|
|
90
|
+
displayPath: attachment.displayPath,
|
|
91
|
+
sizeBytes: attachment.sizeBytes,
|
|
92
|
+
}));
|
|
93
|
+
const previewPayload = {
|
|
94
|
+
model: runOptions.model,
|
|
95
|
+
engine: 'browser',
|
|
96
|
+
composerText: artifacts.composerText,
|
|
97
|
+
attachments: attachmentSummary,
|
|
98
|
+
inlineFileCount: artifacts.inlineFileCount,
|
|
99
|
+
bundled: artifacts.bundled,
|
|
100
|
+
tokenEstimate: artifacts.estimatedInputTokens,
|
|
101
|
+
};
|
|
102
|
+
log('');
|
|
103
|
+
log(chalk.bold('Preview JSON'));
|
|
104
|
+
log(JSON.stringify(previewPayload, null, 2));
|
|
105
|
+
}
|
|
106
|
+
if (previewMode === 'full') {
|
|
107
|
+
log('');
|
|
108
|
+
log(chalk.bold('Composer Text'));
|
|
109
|
+
log(artifacts.composerText || chalk.dim('(empty prompt)'));
|
|
110
|
+
}
|
|
59
111
|
}
|
package/dist/src/cli/engine.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export function defaultWaitPreference(model, engine) {
|
|
2
|
+
// gpt-5-pro (API) can take up to 10 minutes; default to non-blocking
|
|
3
|
+
if (engine === 'api' && model === 'gpt-5-pro') {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
return true; // browser or gpt-5.1 are fast enough to block by default
|
|
7
|
+
}
|
|
1
8
|
/**
|
|
2
9
|
* Determine which engine to use based on CLI flags and the environment.
|
|
3
10
|
*
|
package/dist/src/cli/help.js
CHANGED
|
@@ -43,11 +43,13 @@ function renderHelpBanner(version, colors) {
|
|
|
43
43
|
}
|
|
44
44
|
function renderHelpFooter(program, colors) {
|
|
45
45
|
const tips = [
|
|
46
|
+
`${colors.bullet('•')} Oracle cannot see your project unless you pass ${colors.accent('--file …')} — attach the files/dirs you want it to read.`,
|
|
46
47
|
`${colors.bullet('•')} Attach lots of source (whole directories beat single files) and keep total input under ~196k tokens.`,
|
|
47
48
|
`${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.`,
|
|
49
|
+
`${colors.bullet('•')} Best results: 6–30 sentences plus key source files; very short prompts often yield generic answers.`,
|
|
48
50
|
`${colors.bullet('•')} Oracle is one-shot: it does not remember prior runs, so start fresh each time with full context.`,
|
|
49
51
|
`${colors.bullet('•')} Run ${colors.accent('--files-report')} to inspect token spend before hitting the API.`,
|
|
50
|
-
`${colors.bullet('•')} Non-preview runs spawn detached sessions
|
|
52
|
+
`${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
53
|
`${colors.bullet('•')} Set a memorable 3–5 word slug via ${colors.accent('--slug "<words>"')} to keep session IDs tidy.`,
|
|
52
54
|
`${colors.bullet('•')} Finished sessions auto-hide preamble logs when reattached; raw timestamps remain in the saved log file.`,
|
|
53
55
|
`${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
|
+
}
|
|
@@ -1,4 +1,82 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
1
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
|
+
}
|
|
2
80
|
export function renderMarkdownAnsi(markdown) {
|
|
3
81
|
try {
|
|
4
82
|
const color = Boolean(process.stdout.isTTY);
|
|
@@ -9,6 +87,7 @@ export function renderMarkdownAnsi(markdown) {
|
|
|
9
87
|
width,
|
|
10
88
|
wrap: true,
|
|
11
89
|
hyperlinks,
|
|
90
|
+
highlighter: color ? shikiHighlighter : undefined,
|
|
12
91
|
});
|
|
13
92
|
}
|
|
14
93
|
catch {
|
|
@@ -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
|
+
}
|
package/dist/src/cli/options.js
CHANGED
|
@@ -75,11 +75,30 @@ export function parseSearchOption(value) {
|
|
|
75
75
|
export function normalizeModelOption(value) {
|
|
76
76
|
return (value ?? '').trim();
|
|
77
77
|
}
|
|
78
|
+
export function normalizeBaseUrl(value) {
|
|
79
|
+
const trimmed = value?.trim();
|
|
80
|
+
return trimmed?.length ? trimmed : undefined;
|
|
81
|
+
}
|
|
82
|
+
export function parseTimeoutOption(value) {
|
|
83
|
+
if (value == null)
|
|
84
|
+
return undefined;
|
|
85
|
+
const normalized = value.trim().toLowerCase();
|
|
86
|
+
if (normalized === 'auto')
|
|
87
|
+
return 'auto';
|
|
88
|
+
const parsed = Number.parseFloat(normalized);
|
|
89
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
90
|
+
throw new InvalidArgumentError('Timeout must be a positive number of seconds or "auto".');
|
|
91
|
+
}
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
78
94
|
export function resolveApiModel(modelValue) {
|
|
79
95
|
const normalized = normalizeModelOption(modelValue).toLowerCase();
|
|
80
96
|
if (normalized in MODEL_CONFIGS) {
|
|
81
97
|
return normalized;
|
|
82
98
|
}
|
|
99
|
+
if (normalized.includes('gemini')) {
|
|
100
|
+
return 'gemini-3-pro';
|
|
101
|
+
}
|
|
83
102
|
throw new InvalidArgumentError(`Unsupported model "${modelValue}". Choose one of: ${Object.keys(MODEL_CONFIGS).join(', ')}`);
|
|
84
103
|
}
|
|
85
104
|
export function inferModelFromLabel(modelValue) {
|
|
@@ -90,6 +109,9 @@ export function inferModelFromLabel(modelValue) {
|
|
|
90
109
|
if (normalized in MODEL_CONFIGS) {
|
|
91
110
|
return normalized;
|
|
92
111
|
}
|
|
112
|
+
if (normalized.includes('gemini')) {
|
|
113
|
+
return 'gemini-3-pro';
|
|
114
|
+
}
|
|
93
115
|
if (normalized.includes('pro')) {
|
|
94
116
|
return 'gpt-5-pro';
|
|
95
117
|
}
|
|
@@ -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 ||
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { resolveEngine } from './engine.js';
|
|
2
|
+
import { normalizeModelOption, inferModelFromLabel, resolveApiModel, normalizeBaseUrl } from './options.js';
|
|
3
|
+
import { resolveGeminiModelId } from '../oracle/gemini.js';
|
|
4
|
+
export function resolveRunOptionsFromConfig({ prompt, files = [], model, engine, userConfig, env = process.env, }) {
|
|
5
|
+
const resolvedEngine = resolveEngineWithConfig({ engine, configEngine: userConfig?.engine, env });
|
|
6
|
+
const browserRequested = engine === 'browser';
|
|
7
|
+
const cliModelArg = normalizeModelOption(model ?? userConfig?.model) || 'gpt-5-pro';
|
|
8
|
+
const resolvedModel = resolvedEngine === 'browser' ? inferModelFromLabel(cliModelArg) : resolveApiModel(cliModelArg);
|
|
9
|
+
const isGemini = resolvedModel.startsWith('gemini');
|
|
10
|
+
// Keep the resolved model id alongside the canonical model name so we can log
|
|
11
|
+
// and dispatch the exact identifier (useful for Gemini preview aliases).
|
|
12
|
+
const effectiveModelId = isGemini ? resolveGeminiModelId(resolvedModel) : resolvedModel;
|
|
13
|
+
if (isGemini && browserRequested) {
|
|
14
|
+
throw new Error('Gemini is only supported via API. Use --engine api.');
|
|
15
|
+
}
|
|
16
|
+
// When Gemini is selected, always force API engine (overrides config/env auto browser).
|
|
17
|
+
const fixedEngine = isGemini ? 'api' : resolvedEngine;
|
|
18
|
+
const promptWithSuffix = userConfig?.promptSuffix && userConfig.promptSuffix.trim().length > 0
|
|
19
|
+
? `${prompt.trim()}\n${userConfig.promptSuffix}`
|
|
20
|
+
: prompt;
|
|
21
|
+
const search = userConfig?.search !== 'off';
|
|
22
|
+
const heartbeatIntervalMs = userConfig?.heartbeatSeconds !== undefined ? userConfig.heartbeatSeconds * 1000 : 30_000;
|
|
23
|
+
const baseUrl = normalizeBaseUrl(userConfig?.apiBaseUrl ?? env.OPENAI_BASE_URL);
|
|
24
|
+
const runOptions = {
|
|
25
|
+
prompt: promptWithSuffix,
|
|
26
|
+
model: resolvedModel,
|
|
27
|
+
file: files ?? [],
|
|
28
|
+
search,
|
|
29
|
+
heartbeatIntervalMs,
|
|
30
|
+
filesReport: userConfig?.filesReport,
|
|
31
|
+
background: userConfig?.background,
|
|
32
|
+
baseUrl,
|
|
33
|
+
effectiveModelId,
|
|
34
|
+
};
|
|
35
|
+
return { runOptions, resolvedEngine: fixedEngine };
|
|
36
|
+
}
|
|
37
|
+
function resolveEngineWithConfig({ engine, configEngine, env, }) {
|
|
38
|
+
if (engine)
|
|
39
|
+
return engine;
|
|
40
|
+
if (configEngine)
|
|
41
|
+
return configEngine;
|
|
42
|
+
return resolveEngine({ engine: undefined, env });
|
|
43
|
+
}
|
|
@@ -77,7 +77,7 @@ export async function handleSessionCommand(sessionId, command, deps = defaultDep
|
|
|
77
77
|
console.log(`Ignoring flags on session attach: ${ignoredFlags.join(', ')}`);
|
|
78
78
|
}
|
|
79
79
|
const renderMarkdown = Boolean(sessionOptions.render || sessionOptions.renderMarkdown || autoRender);
|
|
80
|
-
await deps.attachSession(sessionId, { renderMarkdown });
|
|
80
|
+
await deps.attachSession(sessionId, { renderMarkdown, renderPrompt: !sessionOptions.hidePrompt });
|
|
81
81
|
}
|
|
82
82
|
export function formatSessionCleanupMessage(result, scope) {
|
|
83
83
|
const deletedLabel = `${result.deleted} ${result.deleted === 1 ? 'session' : 'sessions'}`;
|