@steipete/oracle 0.4.5 → 0.5.1
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 +11 -9
- package/dist/.DS_Store +0 -0
- package/dist/bin/oracle-cli.js +16 -48
- package/dist/scripts/agent-send.js +147 -0
- package/dist/scripts/docs-list.js +110 -0
- package/dist/scripts/git-policy.js +125 -0
- package/dist/scripts/runner.js +1378 -0
- package/dist/scripts/test-browser.js +103 -0
- package/dist/scripts/test-remote-chrome.js +68 -0
- package/dist/src/browser/actions/attachments.js +47 -16
- package/dist/src/browser/actions/promptComposer.js +67 -18
- package/dist/src/browser/actions/remoteFileTransfer.js +36 -4
- package/dist/src/browser/chromeCookies.js +44 -6
- package/dist/src/browser/chromeLifecycle.js +166 -25
- package/dist/src/browser/config.js +25 -1
- package/dist/src/browser/constants.js +22 -3
- package/dist/src/browser/index.js +384 -22
- package/dist/src/browser/profileSync.js +141 -0
- package/dist/src/browser/prompt.js +3 -1
- package/dist/src/browser/reattach.js +59 -0
- package/dist/src/browser/sessionRunner.js +15 -1
- package/dist/src/browser/windowsCookies.js +2 -1
- package/dist/src/cli/browserConfig.js +11 -0
- package/dist/src/cli/browserDefaults.js +41 -0
- package/dist/src/cli/detach.js +2 -2
- package/dist/src/cli/dryRun.js +4 -2
- package/dist/src/cli/engine.js +2 -2
- package/dist/src/cli/help.js +2 -2
- package/dist/src/cli/options.js +2 -1
- package/dist/src/cli/runOptions.js +1 -1
- package/dist/src/cli/sessionDisplay.js +102 -104
- package/dist/src/cli/sessionRunner.js +39 -6
- package/dist/src/cli/sessionTable.js +88 -0
- package/dist/src/cli/tui/index.js +19 -89
- package/dist/src/heartbeat.js +2 -2
- package/dist/src/oracle/background.js +10 -2
- package/dist/src/oracle/client.js +107 -0
- package/dist/src/oracle/config.js +10 -2
- package/dist/src/oracle/errors.js +24 -4
- package/dist/src/oracle/modelResolver.js +144 -0
- package/dist/src/oracle/oscProgress.js +1 -1
- package/dist/src/oracle/run.js +83 -34
- package/dist/src/oracle/runUtils.js +12 -8
- package/dist/src/remote/server.js +214 -23
- package/dist/src/sessionManager.js +5 -2
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +14 -14
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { mkdir, rm, cp as copyDir } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { defaultProfileRoot, expandPath, looksLikePath } from './chromeCookies.js';
|
|
6
|
+
const DIR_EXCLUDES = [
|
|
7
|
+
'Cache',
|
|
8
|
+
'Code Cache',
|
|
9
|
+
'GPUCache',
|
|
10
|
+
'Service Worker',
|
|
11
|
+
'Crashpad',
|
|
12
|
+
'BrowserMetrics*',
|
|
13
|
+
'GrShaderCache',
|
|
14
|
+
'ShaderCache',
|
|
15
|
+
'OptimizationGuide',
|
|
16
|
+
];
|
|
17
|
+
const FILE_EXCLUDES = [
|
|
18
|
+
'SingletonLock',
|
|
19
|
+
'SingletonSocket',
|
|
20
|
+
'SingletonCookie',
|
|
21
|
+
'*.lock',
|
|
22
|
+
'lockfile',
|
|
23
|
+
'Lock',
|
|
24
|
+
'*.tmp',
|
|
25
|
+
'DevToolsActivePort',
|
|
26
|
+
path.join('Default', 'DevToolsActivePort'),
|
|
27
|
+
path.join('Sessions', '*'),
|
|
28
|
+
'Current Session',
|
|
29
|
+
'Current Tabs',
|
|
30
|
+
'Last Session',
|
|
31
|
+
'Last Tabs',
|
|
32
|
+
];
|
|
33
|
+
export async function syncChromeProfile(options) {
|
|
34
|
+
const { targetDir } = options;
|
|
35
|
+
await mkdir(targetDir, { recursive: true });
|
|
36
|
+
const { sourceDir, profileName } = await resolveProfileSource(options.profile, options.explicitPath);
|
|
37
|
+
const logger = options.logger;
|
|
38
|
+
if (!existsSync(sourceDir)) {
|
|
39
|
+
throw new Error(`Chrome profile not found at ${sourceDir}. Log in once in Chrome, then retry.`);
|
|
40
|
+
}
|
|
41
|
+
// Clean any stale DevTools ports/locks in the target before copying.
|
|
42
|
+
await rm(targetDir, { recursive: true, force: true }).catch(() => undefined);
|
|
43
|
+
await mkdir(targetDir, { recursive: true });
|
|
44
|
+
const result = process.platform === 'win32'
|
|
45
|
+
? await copyWithRobocopy(sourceDir, targetDir, logger)
|
|
46
|
+
: await copyWithRsync(sourceDir, targetDir, logger);
|
|
47
|
+
// Remove lock files in the copied profile to avoid "already running" errors.
|
|
48
|
+
await removeLocks(targetDir);
|
|
49
|
+
return {
|
|
50
|
+
source: sourceDir,
|
|
51
|
+
profileName,
|
|
52
|
+
method: result.method,
|
|
53
|
+
status: result.copied ? 'copied' : 'skipped',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function copyWithRsync(sourceDir, targetDir, logger) {
|
|
57
|
+
const rsyncArgs = [
|
|
58
|
+
'-a',
|
|
59
|
+
'--delete',
|
|
60
|
+
...DIR_EXCLUDES.flatMap((entry) => ['--exclude', entry]),
|
|
61
|
+
...FILE_EXCLUDES.flatMap((entry) => ['--exclude', entry]),
|
|
62
|
+
`${sourceDir}/`,
|
|
63
|
+
`${targetDir}/`,
|
|
64
|
+
];
|
|
65
|
+
const attempt = spawnSync('rsync', rsyncArgs, { stdio: 'pipe' });
|
|
66
|
+
if (!attempt.error && (attempt.status ?? 0) === 0) {
|
|
67
|
+
return { copied: true, method: 'rsync' };
|
|
68
|
+
}
|
|
69
|
+
logger?.('rsync unavailable or failed; falling back to Node copy');
|
|
70
|
+
await copyDirWithFilter(sourceDir, targetDir);
|
|
71
|
+
return copyWithNodeFs();
|
|
72
|
+
}
|
|
73
|
+
async function copyWithRobocopy(sourceDir, targetDir, logger) {
|
|
74
|
+
const args = [sourceDir, targetDir, '/MIR', '/NFL', '/NDL', '/NJH', '/NJS', '/NP', '/Z'];
|
|
75
|
+
if (DIR_EXCLUDES.length) {
|
|
76
|
+
args.push('/XD', ...DIR_EXCLUDES);
|
|
77
|
+
}
|
|
78
|
+
if (FILE_EXCLUDES.length) {
|
|
79
|
+
args.push('/XF', ...FILE_EXCLUDES);
|
|
80
|
+
}
|
|
81
|
+
const attempt = spawnSync('robocopy', args, { stdio: 'pipe' });
|
|
82
|
+
const exitCode = attempt.status ?? 0;
|
|
83
|
+
// Robocopy treats 0-7 as success/partial success; >=8 is failure.
|
|
84
|
+
if (!attempt.error && exitCode < 8) {
|
|
85
|
+
return { copied: true, method: 'robocopy' };
|
|
86
|
+
}
|
|
87
|
+
logger?.('robocopy failed; falling back to Node copy');
|
|
88
|
+
await copyDirWithFilter(sourceDir, targetDir);
|
|
89
|
+
return copyWithNodeFs();
|
|
90
|
+
}
|
|
91
|
+
function copyWithNodeFs() {
|
|
92
|
+
return { copied: true, method: 'node' };
|
|
93
|
+
}
|
|
94
|
+
function shouldExclude(relativePath) {
|
|
95
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
96
|
+
return DIR_EXCLUDES.some((entry) => normalized === entry || normalized.startsWith(`${entry}/`)) ||
|
|
97
|
+
FILE_EXCLUDES.some((entry) => {
|
|
98
|
+
if (entry.endsWith('*')) {
|
|
99
|
+
return normalized.startsWith(entry.slice(0, -1));
|
|
100
|
+
}
|
|
101
|
+
if (entry.includes('*')) {
|
|
102
|
+
// simple glob support for BrowserMetrics*
|
|
103
|
+
const prefix = entry.replace('*', '');
|
|
104
|
+
return normalized.startsWith(prefix);
|
|
105
|
+
}
|
|
106
|
+
return path.basename(normalized) === entry || normalized.endsWith(`/${entry}`);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function removeLocks(targetDir) {
|
|
110
|
+
const lockNames = ['SingletonLock', 'SingletonSocket', 'SingletonCookie', 'DevToolsActivePort'];
|
|
111
|
+
for (const lock of lockNames) {
|
|
112
|
+
await rm(path.join(targetDir, lock), { force: true }).catch(() => undefined);
|
|
113
|
+
await rm(path.join(targetDir, 'Default', lock), { force: true }).catch(() => undefined);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function resolveProfileSource(profile, explicitPath) {
|
|
117
|
+
const profileName = profile?.trim() ? profile.trim() : 'Default';
|
|
118
|
+
if (explicitPath?.trim()) {
|
|
119
|
+
const resolved = expandPath(explicitPath.trim());
|
|
120
|
+
if (resolved.toLowerCase().endsWith('cookies')) {
|
|
121
|
+
return { sourceDir: path.dirname(resolved), profileName };
|
|
122
|
+
}
|
|
123
|
+
return { sourceDir: resolved, profileName };
|
|
124
|
+
}
|
|
125
|
+
if (looksLikePath(profileName)) {
|
|
126
|
+
return { sourceDir: expandPath(profileName), profileName };
|
|
127
|
+
}
|
|
128
|
+
const baseRoot = await defaultProfileRoot();
|
|
129
|
+
return { sourceDir: path.join(baseRoot, profileName), profileName };
|
|
130
|
+
}
|
|
131
|
+
async function copyDirWithFilter(sourceDir, targetDir) {
|
|
132
|
+
await copyDir(sourceDir, targetDir, {
|
|
133
|
+
recursive: true,
|
|
134
|
+
filter: async (source) => {
|
|
135
|
+
const rel = path.relative(sourceDir, source);
|
|
136
|
+
if (!rel)
|
|
137
|
+
return true;
|
|
138
|
+
return !shouldExclude(rel);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS, formatFileSection, } from '../oracle.js';
|
|
5
|
+
import { isKnownModel } from '../oracle/modelResolver.js';
|
|
5
6
|
import { buildPromptMarkdown } from '../oracle/promptAssembly.js';
|
|
6
7
|
import { buildAttachmentPlan } from './policies.js';
|
|
7
8
|
export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
@@ -48,7 +49,8 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
48
49
|
});
|
|
49
50
|
}
|
|
50
51
|
const inlineFileCount = attachmentPlan.inlineFileCount;
|
|
51
|
-
const
|
|
52
|
+
const modelConfig = isKnownModel(runOptions.model) ? MODEL_CONFIGS[runOptions.model] : MODEL_CONFIGS['gpt-5.1'];
|
|
53
|
+
const tokenizer = modelConfig.tokenizer;
|
|
52
54
|
const tokenizerUserContent = inlineFileCount > 0 && attachmentPlan.inlineBlock
|
|
53
55
|
? [userPrompt, attachmentPlan.inlineBlock].filter((value) => Boolean(value?.trim())).join('\n\n').trim()
|
|
54
56
|
: userPrompt;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import CDP from 'chrome-remote-interface';
|
|
2
|
+
import { waitForAssistantResponse, captureAssistantMarkdown } from './pageActions.js';
|
|
3
|
+
function pickTarget(targets, runtime) {
|
|
4
|
+
if (!Array.isArray(targets) || targets.length === 0) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
if (runtime.chromeTargetId) {
|
|
8
|
+
const byId = targets.find((t) => t.targetId === runtime.chromeTargetId);
|
|
9
|
+
if (byId)
|
|
10
|
+
return byId;
|
|
11
|
+
}
|
|
12
|
+
if (runtime.tabUrl) {
|
|
13
|
+
const byUrl = targets.find((t) => t.url?.startsWith(runtime.tabUrl)) ||
|
|
14
|
+
targets.find((t) => runtime.tabUrl.startsWith(t.url || ''));
|
|
15
|
+
if (byUrl)
|
|
16
|
+
return byUrl;
|
|
17
|
+
}
|
|
18
|
+
return targets.find((t) => t.type === 'page') ?? targets[0];
|
|
19
|
+
}
|
|
20
|
+
export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
21
|
+
if (!runtime.chromePort) {
|
|
22
|
+
throw new Error('Missing chromePort; cannot reattach.');
|
|
23
|
+
}
|
|
24
|
+
const host = runtime.chromeHost ?? '127.0.0.1';
|
|
25
|
+
const listTargets = deps.listTargets ??
|
|
26
|
+
(async () => {
|
|
27
|
+
const targets = await CDP.List({ host, port: runtime.chromePort });
|
|
28
|
+
return targets;
|
|
29
|
+
});
|
|
30
|
+
const connect = deps.connect ?? ((options) => CDP(options));
|
|
31
|
+
const targetList = (await listTargets());
|
|
32
|
+
const target = pickTarget(targetList, runtime);
|
|
33
|
+
const client = (await connect({
|
|
34
|
+
host,
|
|
35
|
+
port: runtime.chromePort,
|
|
36
|
+
target: target?.targetId,
|
|
37
|
+
}));
|
|
38
|
+
const { Runtime, DOM } = client;
|
|
39
|
+
if (Runtime?.enable) {
|
|
40
|
+
await Runtime.enable();
|
|
41
|
+
}
|
|
42
|
+
if (DOM && typeof DOM.enable === 'function') {
|
|
43
|
+
await DOM.enable();
|
|
44
|
+
}
|
|
45
|
+
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
46
|
+
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
47
|
+
const timeoutMs = config?.timeoutMs ?? 120_000;
|
|
48
|
+
const answer = await waitForResponse(Runtime, timeoutMs, logger);
|
|
49
|
+
const markdown = (await captureMarkdown(Runtime, answer.meta, logger)) ?? answer.text;
|
|
50
|
+
if (client && typeof client.close === 'function') {
|
|
51
|
+
try {
|
|
52
|
+
await client.close();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { answerText: answer.text, answerMarkdown: markdown };
|
|
59
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { formatElapsed } from '../oracle.js';
|
|
3
|
+
import { formatTokenCount } from '../oracle/runUtils.js';
|
|
3
4
|
import { runBrowserMode } from '../browserMode.js';
|
|
4
5
|
import { assembleBrowserPrompt } from './prompt.js';
|
|
5
6
|
import { BrowserAutomationError } from '../oracle/errors.js';
|
|
@@ -46,6 +47,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
46
47
|
if (runOptions.verbose) {
|
|
47
48
|
log(chalk.dim('Chrome automation does not stream output; this may take a minute...'));
|
|
48
49
|
}
|
|
50
|
+
const persistRuntimeHint = deps.persistRuntimeHint ?? (() => { });
|
|
49
51
|
let browserResult;
|
|
50
52
|
try {
|
|
51
53
|
browserResult = await executeBrowser({
|
|
@@ -55,6 +57,9 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
55
57
|
log: automationLogger,
|
|
56
58
|
heartbeatIntervalMs: runOptions.heartbeatIntervalMs,
|
|
57
59
|
verbose: runOptions.verbose,
|
|
60
|
+
runtimeHintCb: async (runtime) => {
|
|
61
|
+
await persistRuntimeHint({ ...runtime, controllerPid: runtime.controllerPid ?? process.pid });
|
|
62
|
+
},
|
|
58
63
|
});
|
|
59
64
|
}
|
|
60
65
|
catch (error) {
|
|
@@ -76,7 +81,14 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
76
81
|
reasoningTokens: 0,
|
|
77
82
|
totalTokens: promptArtifacts.estimatedInputTokens + browserResult.answerTokens,
|
|
78
83
|
};
|
|
79
|
-
const tokensDisplay =
|
|
84
|
+
const tokensDisplay = [
|
|
85
|
+
usage.inputTokens,
|
|
86
|
+
usage.outputTokens,
|
|
87
|
+
usage.reasoningTokens,
|
|
88
|
+
usage.totalTokens,
|
|
89
|
+
]
|
|
90
|
+
.map((value) => formatTokenCount(value))
|
|
91
|
+
.join('/');
|
|
80
92
|
const tokensLabel = runOptions.verbose ? 'tokens (input/output/reasoning/total)' : 'tok(i/o/r/t)';
|
|
81
93
|
const statsParts = [`${runOptions.model}[browser]`, `${tokensLabel}=${tokensDisplay}`];
|
|
82
94
|
if (runOptions.file && runOptions.file.length > 0) {
|
|
@@ -89,7 +101,9 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
89
101
|
runtime: {
|
|
90
102
|
chromePid: browserResult.chromePid,
|
|
91
103
|
chromePort: browserResult.chromePort,
|
|
104
|
+
chromeHost: browserResult.chromeHost,
|
|
92
105
|
userDataDir: browserResult.userDataDir,
|
|
106
|
+
controllerPid: browserResult.controllerPid ?? process.pid,
|
|
93
107
|
},
|
|
94
108
|
answerText,
|
|
95
109
|
};
|
|
@@ -65,7 +65,8 @@ export async function loadWindowsCookies(dbPath, filterNames) {
|
|
|
65
65
|
}
|
|
66
66
|
function decryptCookie(value, aesKey) {
|
|
67
67
|
const prefix = value.slice(0, 3).toString();
|
|
68
|
-
|
|
68
|
+
// Chrome prefixes AES-GCM encrypted cookies with version markers like v10/v11/v20; treat all v** the same.
|
|
69
|
+
if (/^v\d{2}$/u.test(prefix)) {
|
|
69
70
|
const iv = value.slice(3, 15);
|
|
70
71
|
const tag = value.slice(value.length - 16);
|
|
71
72
|
const data = value.slice(15, value.length - 16);
|
|
@@ -35,6 +35,7 @@ export async function buildBrowserConfig(options) {
|
|
|
35
35
|
chromePath: options.browserChromePath ?? null,
|
|
36
36
|
chromeCookiePath: options.browserCookiePath ?? null,
|
|
37
37
|
url,
|
|
38
|
+
debugPort: selectBrowserPort(options),
|
|
38
39
|
timeoutMs: options.browserTimeout ? parseDuration(options.browserTimeout, DEFAULT_BROWSER_TIMEOUT_MS) : undefined,
|
|
39
40
|
inputTimeoutMs: options.browserInputTimeout
|
|
40
41
|
? parseDuration(options.browserInputTimeout, DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
|
|
@@ -45,6 +46,7 @@ export async function buildBrowserConfig(options) {
|
|
|
45
46
|
inlineCookiesSource: inline?.source ?? null,
|
|
46
47
|
headless: undefined, // disable headless; Cloudflare blocks it
|
|
47
48
|
keepBrowser: options.browserKeepBrowser ? true : undefined,
|
|
49
|
+
manualLogin: options.browserManualLogin ? true : undefined,
|
|
48
50
|
hideWindow: options.browserHideWindow ? true : undefined,
|
|
49
51
|
desiredModel: shouldUseOverride ? desiredModelOverride : mapModelToBrowserLabel(options.model),
|
|
50
52
|
debug: options.verbose ? true : undefined,
|
|
@@ -53,6 +55,15 @@ export async function buildBrowserConfig(options) {
|
|
|
53
55
|
remoteChrome,
|
|
54
56
|
};
|
|
55
57
|
}
|
|
58
|
+
function selectBrowserPort(options) {
|
|
59
|
+
const candidate = options.browserPort ?? options.browserDebugPort;
|
|
60
|
+
if (candidate === undefined || candidate === null)
|
|
61
|
+
return null;
|
|
62
|
+
if (!Number.isFinite(candidate) || candidate <= 0 || candidate > 65_535) {
|
|
63
|
+
throw new Error(`Invalid browser port: ${candidate}. Expected a number between 1 and 65535.`);
|
|
64
|
+
}
|
|
65
|
+
return candidate;
|
|
66
|
+
}
|
|
56
67
|
export function mapModelToBrowserLabel(model) {
|
|
57
68
|
return BROWSER_MODEL_LABELS[model] ?? DEFAULT_MODEL_TARGET;
|
|
58
69
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { normalizeChatgptUrl, CHATGPT_URL } from '../browserMode.js';
|
|
2
|
+
export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
3
|
+
const browser = config.browser;
|
|
4
|
+
if (!browser)
|
|
5
|
+
return;
|
|
6
|
+
const configuredChatgptUrl = browser.chatgptUrl ?? browser.url;
|
|
7
|
+
const cliChatgptSet = options.chatgptUrl !== undefined || options.browserUrl !== undefined;
|
|
8
|
+
if ((getSource('chatgptUrl') === 'default' || getSource('chatgptUrl') === undefined) && !cliChatgptSet && configuredChatgptUrl !== undefined) {
|
|
9
|
+
options.chatgptUrl = normalizeChatgptUrl(configuredChatgptUrl ?? '', CHATGPT_URL);
|
|
10
|
+
}
|
|
11
|
+
if (getSource('browserChromeProfile') === 'default' && browser.chromeProfile !== undefined) {
|
|
12
|
+
options.browserChromeProfile = browser.chromeProfile ?? undefined;
|
|
13
|
+
}
|
|
14
|
+
if (getSource('browserChromePath') === 'default' && browser.chromePath !== undefined) {
|
|
15
|
+
options.browserChromePath = browser.chromePath ?? undefined;
|
|
16
|
+
}
|
|
17
|
+
if (getSource('browserCookiePath') === 'default' && browser.chromeCookiePath !== undefined) {
|
|
18
|
+
options.browserCookiePath = browser.chromeCookiePath ?? undefined;
|
|
19
|
+
}
|
|
20
|
+
if ((getSource('browserUrl') === 'default' || getSource('browserUrl') === undefined) && options.browserUrl === undefined && browser.url !== undefined) {
|
|
21
|
+
options.browserUrl = browser.url;
|
|
22
|
+
}
|
|
23
|
+
if (getSource('browserTimeout') === 'default' && typeof browser.timeoutMs === 'number') {
|
|
24
|
+
options.browserTimeout = String(browser.timeoutMs);
|
|
25
|
+
}
|
|
26
|
+
if (getSource('browserPort') === 'default' && typeof browser.debugPort === 'number') {
|
|
27
|
+
options.browserPort = browser.debugPort;
|
|
28
|
+
}
|
|
29
|
+
if (getSource('browserInputTimeout') === 'default' && typeof browser.inputTimeoutMs === 'number') {
|
|
30
|
+
options.browserInputTimeout = String(browser.inputTimeoutMs);
|
|
31
|
+
}
|
|
32
|
+
if (getSource('browserHeadless') === 'default' && browser.headless !== undefined) {
|
|
33
|
+
options.browserHeadless = browser.headless;
|
|
34
|
+
}
|
|
35
|
+
if (getSource('browserHideWindow') === 'default' && browser.hideWindow !== undefined) {
|
|
36
|
+
options.browserHideWindow = browser.hideWindow;
|
|
37
|
+
}
|
|
38
|
+
if (getSource('browserKeepBrowser') === 'default' && browser.keepBrowser !== undefined) {
|
|
39
|
+
options.browserKeepBrowser = browser.keepBrowser;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/src/cli/detach.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isProModel } from '../oracle/modelResolver.js';
|
|
2
2
|
export function shouldDetachSession({
|
|
3
3
|
// Params kept for future policy tweaks; currently only model/disableDetachEnv matter.
|
|
4
4
|
engine, model, waitPreference: _waitPreference, disableDetachEnv, }) {
|
|
5
5
|
if (disableDetachEnv)
|
|
6
6
|
return false;
|
|
7
7
|
// Only Pro-tier API runs should start detached by default; browser runs stay inline so failures surface.
|
|
8
|
-
if (
|
|
8
|
+
if (isProModel(model) && engine === 'api')
|
|
9
9
|
return true;
|
|
10
10
|
return false;
|
|
11
11
|
}
|
package/dist/src/cli/dryRun.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { MODEL_CONFIGS, TOKENIZER_OPTIONS, DEFAULT_SYSTEM_PROMPT, buildPrompt, readFiles, getFileTokenStats, printFileTokenStats, } from '../oracle.js';
|
|
3
|
+
import { isKnownModel } from '../oracle/modelResolver.js';
|
|
3
4
|
import { assembleBrowserPrompt } from '../browser/prompt.js';
|
|
4
5
|
import { buildTokenEstimateSuffix, formatAttachmentLabel } from '../browser/promptSummary.js';
|
|
5
6
|
import { buildCookiePlan } from '../browser/policies.js';
|
|
@@ -15,7 +16,8 @@ async function runApiDryRun({ runOptions, cwd, version, log, }, deps) {
|
|
|
15
16
|
const files = await readFilesImpl(runOptions.file ?? [], { cwd });
|
|
16
17
|
const systemPrompt = runOptions.system?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
17
18
|
const combinedPrompt = buildPrompt(runOptions.prompt ?? '', files, cwd);
|
|
18
|
-
const
|
|
19
|
+
const modelConfig = isKnownModel(runOptions.model) ? MODEL_CONFIGS[runOptions.model] : MODEL_CONFIGS['gpt-5.1'];
|
|
20
|
+
const tokenizer = modelConfig.tokenizer;
|
|
19
21
|
const estimatedInputTokens = tokenizer([
|
|
20
22
|
{ role: 'system', content: systemPrompt },
|
|
21
23
|
{ role: 'user', content: combinedPrompt },
|
|
@@ -26,7 +28,7 @@ async function runApiDryRun({ runOptions, cwd, version, log, }, deps) {
|
|
|
26
28
|
log(chalk.dim('[dry-run] No files matched the provided --file patterns.'));
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
|
-
const inputBudget = runOptions.maxInput ??
|
|
31
|
+
const inputBudget = runOptions.maxInput ?? modelConfig.inputLimit;
|
|
30
32
|
const stats = getFileTokenStats(files, {
|
|
31
33
|
cwd,
|
|
32
34
|
tokenizer,
|
package/dist/src/cli/engine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isProModel } from '../oracle/modelResolver.js';
|
|
2
2
|
export function defaultWaitPreference(model, engine) {
|
|
3
3
|
// Pro-class API runs can take a long time; prefer non-blocking unless explicitly overridden.
|
|
4
|
-
if (engine === 'api' &&
|
|
4
|
+
if (engine === 'api' && isProModel(model)) {
|
|
5
5
|
return false;
|
|
6
6
|
}
|
|
7
7
|
return true; // browser or non-pro models are fast enough to block by default
|
package/dist/src/cli/help.js
CHANGED
|
@@ -38,12 +38,12 @@ export function applyHelpStyling(program, version, isTty) {
|
|
|
38
38
|
program.addHelpText('after', () => renderHelpFooter(program, colors));
|
|
39
39
|
}
|
|
40
40
|
function renderHelpBanner(version, colors) {
|
|
41
|
-
const subtitle = 'GPT-5.1 Pro/GPT-5.1 for tough questions with code/file context.';
|
|
41
|
+
const subtitle = 'Prompt + files required — GPT-5.1 Pro/GPT-5.1 for tough questions with code/file context.';
|
|
42
42
|
return `${colors.banner(`Oracle CLI v${version}`)} ${colors.subtitle(`— ${subtitle}`)}\n`;
|
|
43
43
|
}
|
|
44
44
|
function renderHelpFooter(program, colors) {
|
|
45
45
|
const tips = [
|
|
46
|
-
`${colors.bullet('•')}
|
|
46
|
+
`${colors.bullet('•')} Required: always pass a prompt AND ${colors.accent('--file …')} (directories/globs are fine); Oracle cannot see your project otherwise.`,
|
|
47
47
|
`${colors.bullet('•')} Attach lots of source (whole directories beat single files) and keep total input under ~196k tokens.`,
|
|
48
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
49
|
`${colors.bullet('•')} Spell out the project + platform + version requirements (repo name, target OS/toolchain versions, API dependencies) so Oracle doesn’t guess defaults.`,
|
package/dist/src/cli/options.js
CHANGED
|
@@ -149,7 +149,8 @@ export function resolveApiModel(modelValue) {
|
|
|
149
149
|
if (normalized.includes('gemini')) {
|
|
150
150
|
return 'gemini-3-pro';
|
|
151
151
|
}
|
|
152
|
-
|
|
152
|
+
// Passthrough for custom/OpenRouter model IDs.
|
|
153
|
+
return normalized;
|
|
153
154
|
}
|
|
154
155
|
export function inferModelFromLabel(modelValue) {
|
|
155
156
|
const normalized = normalizeModelOption(modelValue).toLowerCase();
|
|
@@ -64,7 +64,7 @@ function resolveEngineWithConfig({ engine, configEngine, env, }) {
|
|
|
64
64
|
return resolveEngine({ engine: undefined, env });
|
|
65
65
|
}
|
|
66
66
|
function resolveEffectiveModelId(model) {
|
|
67
|
-
if (model.startsWith('gemini')) {
|
|
67
|
+
if (typeof model === 'string' && model.startsWith('gemini')) {
|
|
68
68
|
return resolveGeminiModelId(model);
|
|
69
69
|
}
|
|
70
70
|
const config = MODEL_CONFIGS[model];
|