@steipete/oracle 0.4.4 → 0.5.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 +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 +29 -18
- package/dist/src/browser/actions/remoteFileTransfer.js +36 -4
- package/dist/src/browser/chromeCookies.js +37 -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 +301 -21
- 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 +98 -5
- package/dist/src/cli/sessionRunner.js +39 -6
- package/dist/src/cli/tui/index.js +15 -18
- package/dist/src/heartbeat.js +2 -2
- package/dist/src/oracle/background.js +10 -2
- package/dist/src/oracle/client.js +17 -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 +82 -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/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +47 -46
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/vendor/oracle-notifier/README.md +0 -24
|
@@ -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];
|
|
@@ -4,10 +4,24 @@ import { renderMarkdownAnsi } from './markdownRenderer.js';
|
|
|
4
4
|
import { formatElapsed, formatUSD } from '../oracle/format.js';
|
|
5
5
|
import { MODEL_CONFIGS } from '../oracle.js';
|
|
6
6
|
import { sessionStore, wait } from '../sessionStore.js';
|
|
7
|
+
import { formatTokenCount, formatTokenValue } from '../oracle/runUtils.js';
|
|
8
|
+
import { resumeBrowserSession } from '../browser/reattach.js';
|
|
9
|
+
import { estimateTokenCount } from '../browser/utils.js';
|
|
7
10
|
const isTty = () => Boolean(process.stdout.isTTY);
|
|
8
11
|
const dim = (text) => (isTty() ? kleur.dim(text) : text);
|
|
9
12
|
export const MAX_RENDER_BYTES = 200_000;
|
|
10
13
|
const MODEL_COLUMN_WIDTH = 18;
|
|
14
|
+
function isProcessAlive(pid) {
|
|
15
|
+
if (!pid)
|
|
16
|
+
return false;
|
|
17
|
+
try {
|
|
18
|
+
process.kill(pid, 0);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return !(error instanceof Error && error.code === 'ESRCH');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
11
25
|
const CLEANUP_TIP = 'Tip: Run "oracle session --clear --hours 24" to prune cached runs (add --all to wipe everything).';
|
|
12
26
|
export async function showStatus({ hours, includeAll, limit, showExamples = false, modelFilter, }) {
|
|
13
27
|
const metas = await sessionStore.listSessions();
|
|
@@ -55,7 +69,7 @@ function colorStatus(status, padded) {
|
|
|
55
69
|
}
|
|
56
70
|
}
|
|
57
71
|
export async function attachSession(sessionId, options) {
|
|
58
|
-
|
|
72
|
+
let metadata = await sessionStore.readSession(sessionId);
|
|
59
73
|
if (!metadata) {
|
|
60
74
|
console.error(chalk.red(`No session found with ID ${sessionId}`));
|
|
61
75
|
process.exitCode = 1;
|
|
@@ -74,6 +88,65 @@ export async function attachSession(sessionId, options) {
|
|
|
74
88
|
const initialStatus = metadata.status;
|
|
75
89
|
const wantsRender = Boolean(options?.renderMarkdown);
|
|
76
90
|
const isVerbose = Boolean(process.env.ORACLE_VERBOSE_RENDER);
|
|
91
|
+
const runtime = metadata.browser?.runtime;
|
|
92
|
+
const controllerAlive = isProcessAlive(runtime?.controllerPid);
|
|
93
|
+
const canReattach = metadata.status === 'running' &&
|
|
94
|
+
metadata.mode === 'browser' &&
|
|
95
|
+
runtime?.chromePort &&
|
|
96
|
+
(metadata.response?.incompleteReason === 'chrome-disconnected' || (runtime.controllerPid && !controllerAlive));
|
|
97
|
+
if (canReattach) {
|
|
98
|
+
const portInfo = runtime?.chromePort ? `port ${runtime.chromePort}` : 'unknown port';
|
|
99
|
+
const urlInfo = runtime?.tabUrl ? `url=${runtime.tabUrl}` : 'url=unknown';
|
|
100
|
+
console.log(chalk.yellow(`Attempting to reattach to the existing Chrome session (${portInfo}, ${urlInfo})...`));
|
|
101
|
+
try {
|
|
102
|
+
const result = await resumeBrowserSession(runtime, metadata.browser?.config, Object.assign(((message) => {
|
|
103
|
+
if (message) {
|
|
104
|
+
console.log(dim(message));
|
|
105
|
+
}
|
|
106
|
+
}), { verbose: true }));
|
|
107
|
+
const outputTokens = estimateTokenCount(result.answerMarkdown);
|
|
108
|
+
const logWriter = sessionStore.createLogWriter(sessionId);
|
|
109
|
+
logWriter.logLine('[reattach] captured assistant response from existing Chrome tab');
|
|
110
|
+
logWriter.logLine('Answer:');
|
|
111
|
+
logWriter.logLine(result.answerMarkdown || result.answerText);
|
|
112
|
+
logWriter.stream.end();
|
|
113
|
+
if (metadata.model) {
|
|
114
|
+
await sessionStore.updateModelRun(metadata.id, metadata.model, {
|
|
115
|
+
status: 'completed',
|
|
116
|
+
usage: {
|
|
117
|
+
inputTokens: 0,
|
|
118
|
+
outputTokens,
|
|
119
|
+
reasoningTokens: 0,
|
|
120
|
+
totalTokens: outputTokens,
|
|
121
|
+
},
|
|
122
|
+
completedAt: new Date().toISOString(),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
await sessionStore.updateSession(sessionId, {
|
|
126
|
+
status: 'completed',
|
|
127
|
+
completedAt: new Date().toISOString(),
|
|
128
|
+
usage: {
|
|
129
|
+
inputTokens: 0,
|
|
130
|
+
outputTokens,
|
|
131
|
+
reasoningTokens: 0,
|
|
132
|
+
totalTokens: outputTokens,
|
|
133
|
+
},
|
|
134
|
+
browser: {
|
|
135
|
+
config: metadata.browser?.config,
|
|
136
|
+
runtime,
|
|
137
|
+
},
|
|
138
|
+
response: { status: 'completed' },
|
|
139
|
+
error: undefined,
|
|
140
|
+
transport: undefined,
|
|
141
|
+
});
|
|
142
|
+
console.log(chalk.green('Reattach succeeded; session marked completed.'));
|
|
143
|
+
metadata = (await sessionStore.readSession(sessionId)) ?? metadata;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
147
|
+
console.log(chalk.red(`Reattach failed: ${message}`));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
77
150
|
if (!options?.suppressMetadata) {
|
|
78
151
|
const reattachLine = buildReattachLine(metadata);
|
|
79
152
|
if (reattachLine) {
|
|
@@ -85,7 +158,7 @@ export async function attachSession(sessionId, options) {
|
|
|
85
158
|
console.log('Models:');
|
|
86
159
|
for (const run of metadata.models) {
|
|
87
160
|
const usage = run.usage
|
|
88
|
-
? ` tok=${run.usage.outputTokens
|
|
161
|
+
? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
|
|
89
162
|
: '';
|
|
90
163
|
console.log(`- ${chalk.cyan(run.model)} — ${run.status}${usage}`);
|
|
91
164
|
}
|
|
@@ -424,17 +497,25 @@ async function buildSessionLogForDisplay(sessionId, fallbackMeta, modelFilter) {
|
|
|
424
497
|
}
|
|
425
498
|
return await sessionStore.readLog(sessionId);
|
|
426
499
|
}
|
|
427
|
-
const candidates = normalizedFilter
|
|
500
|
+
const candidates = normalizedFilter
|
|
428
501
|
? models.filter((model) => model.model.toLowerCase() === normalizedFilter)
|
|
429
502
|
: models;
|
|
430
503
|
if (candidates.length === 0) {
|
|
431
504
|
return '';
|
|
432
505
|
}
|
|
433
506
|
const sections = [];
|
|
507
|
+
let hasContent = false;
|
|
434
508
|
for (const model of candidates) {
|
|
435
|
-
const body = await sessionStore.readModelLog(sessionId, model.model);
|
|
509
|
+
const body = (await sessionStore.readModelLog(sessionId, model.model)) ?? '';
|
|
510
|
+
if (body.trim().length > 0) {
|
|
511
|
+
hasContent = true;
|
|
512
|
+
}
|
|
436
513
|
sections.push(`=== ${model.model} ===\n${body}`.trimEnd());
|
|
437
514
|
}
|
|
515
|
+
if (!hasContent) {
|
|
516
|
+
// Fallback for runs that recorded output only in the session log (e.g., browser runs without per-model logs).
|
|
517
|
+
return await sessionStore.readLog(sessionId);
|
|
518
|
+
}
|
|
438
519
|
return sections.join('\n\n');
|
|
439
520
|
}
|
|
440
521
|
function extractRenderableChunks(text, state) {
|
|
@@ -499,7 +580,19 @@ export function formatCompletionSummary(metadata, options = {}) {
|
|
|
499
580
|
const usage = metadata.usage;
|
|
500
581
|
const cost = metadata.mode === 'browser' ? null : resolveCost(metadata);
|
|
501
582
|
const costPart = cost != null ? ` | ${formatUSD(cost)}` : '';
|
|
502
|
-
const tokensDisplay =
|
|
583
|
+
const tokensDisplay = [
|
|
584
|
+
usage.inputTokens ?? 0,
|
|
585
|
+
usage.outputTokens ?? 0,
|
|
586
|
+
usage.reasoningTokens ?? 0,
|
|
587
|
+
usage.totalTokens ?? 0,
|
|
588
|
+
]
|
|
589
|
+
.map((value, index) => formatTokenValue(value, {
|
|
590
|
+
input_tokens: usage.inputTokens,
|
|
591
|
+
output_tokens: usage.outputTokens,
|
|
592
|
+
reasoning_tokens: usage.reasoningTokens,
|
|
593
|
+
total_tokens: usage.totalTokens,
|
|
594
|
+
}, index))
|
|
595
|
+
.join('/');
|
|
503
596
|
const filesCount = metadata.options?.file?.length ?? 0;
|
|
504
597
|
const filesPart = filesCount > 0 ? ` | files=${filesCount}` : '';
|
|
505
598
|
const slugPart = options.includeSlug ? ` | slug=${metadata.id}` : '';
|
|
@@ -10,6 +10,8 @@ import { sendSessionNotification, deriveNotificationSettingsFromMetadata, } from
|
|
|
10
10
|
import { sessionStore } from '../sessionStore.js';
|
|
11
11
|
import { runMultiModelApiSession } from '../oracle/multiModelRunner.js';
|
|
12
12
|
import { MODEL_CONFIGS, DEFAULT_SYSTEM_PROMPT } from '../oracle/config.js';
|
|
13
|
+
import { isKnownModel } from '../oracle/modelResolver.js';
|
|
14
|
+
import { resolveModelConfig } from '../oracle/modelResolver.js';
|
|
13
15
|
import { buildPrompt, buildRequestBody } from '../oracle/request.js';
|
|
14
16
|
import { estimateRequestTokens } from '../oracle/tokenEstimate.js';
|
|
15
17
|
import { formatTokenEstimate, formatTokenValue } from '../oracle/runUtils.js';
|
|
@@ -49,7 +51,16 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
49
51
|
startedAt: new Date().toISOString(),
|
|
50
52
|
});
|
|
51
53
|
}
|
|
52
|
-
const
|
|
54
|
+
const runnerDeps = {
|
|
55
|
+
...browserDeps,
|
|
56
|
+
persistRuntimeHint: async (runtime) => {
|
|
57
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
58
|
+
status: 'running',
|
|
59
|
+
browser: { config: browserConfig, runtime },
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
const result = await runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, runnerDeps);
|
|
53
64
|
if (modelForStatus) {
|
|
54
65
|
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
55
66
|
status: 'completed',
|
|
@@ -87,10 +98,10 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
87
98
|
if (!primaryModel) {
|
|
88
99
|
throw new Error('Missing model name for multi-model run.');
|
|
89
100
|
}
|
|
90
|
-
const modelConfig =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
101
|
+
const modelConfig = await resolveModelConfig(primaryModel, {
|
|
102
|
+
baseUrl: runOptions.baseUrl,
|
|
103
|
+
openRouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
104
|
+
});
|
|
94
105
|
const files = await readFiles(runOptions.file ?? [], { cwd });
|
|
95
106
|
const promptWithFiles = buildPrompt(runOptions.prompt, files, cwd);
|
|
96
107
|
const requestBody = buildRequestBody({
|
|
@@ -119,7 +130,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
119
130
|
log(dim(tip));
|
|
120
131
|
}
|
|
121
132
|
// Surface long-running model expectations up front so users know why a response might lag.
|
|
122
|
-
const longRunningModels = multiModels.filter((model) => MODEL_CONFIGS[model]?.reasoning?.effort === 'high');
|
|
133
|
+
const longRunningModels = multiModels.filter((model) => isKnownModel(model) && MODEL_CONFIGS[model]?.reasoning?.effort === 'high');
|
|
123
134
|
if (longRunningModels.length > 0) {
|
|
124
135
|
for (const model of longRunningModels) {
|
|
125
136
|
log('');
|
|
@@ -299,6 +310,28 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
299
310
|
log(`ERROR: ${message}`);
|
|
300
311
|
markErrorLogged(error);
|
|
301
312
|
const userError = asOracleUserError(error);
|
|
313
|
+
const connectionLost = userError?.category === 'browser-automation' && userError.details?.stage === 'connection-lost';
|
|
314
|
+
if (connectionLost && mode === 'browser') {
|
|
315
|
+
const runtime = userError.details?.runtime;
|
|
316
|
+
log(dim('Chrome disconnected before completion; keeping session running for reattach.'));
|
|
317
|
+
if (modelForStatus) {
|
|
318
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
319
|
+
status: 'running',
|
|
320
|
+
completedAt: undefined,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
324
|
+
status: 'running',
|
|
325
|
+
errorMessage: message,
|
|
326
|
+
mode,
|
|
327
|
+
browser: {
|
|
328
|
+
config: browserConfig,
|
|
329
|
+
runtime: runtime ?? sessionMeta.browser?.runtime,
|
|
330
|
+
},
|
|
331
|
+
response: { status: 'running', incompleteReason: 'chrome-disconnected' },
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
302
335
|
if (userError) {
|
|
303
336
|
log(dim(`User error (${userError.category}): ${userError.message}`));
|
|
304
337
|
}
|
|
@@ -12,13 +12,9 @@ import { MAX_RENDER_BYTES, trimBeforeFirstAnswer } from '../sessionDisplay.js';
|
|
|
12
12
|
import { buildBrowserConfig, resolveBrowserModelLabel } from '../browserConfig.js';
|
|
13
13
|
import { resolveNotificationSettings } from '../notifier.js';
|
|
14
14
|
import { loadUserConfig } from '../../config.js';
|
|
15
|
+
import { formatTokenCount } from '../../oracle/runUtils.js';
|
|
15
16
|
const isTty = () => Boolean(process.stdout.isTTY && chalk.level > 0);
|
|
16
17
|
const dim = (text) => (isTty() ? kleur.dim(text) : text);
|
|
17
|
-
const disabledChoice = (label) => ({
|
|
18
|
-
name: label,
|
|
19
|
-
value: '__disabled__',
|
|
20
|
-
disabled: true,
|
|
21
|
-
});
|
|
22
18
|
const RECENT_WINDOW_HOURS = 24;
|
|
23
19
|
const PAGE_SIZE = 10;
|
|
24
20
|
const STATUS_PAD = 9;
|
|
@@ -27,14 +23,16 @@ const MODE_PAD = 7;
|
|
|
27
23
|
const TIMESTAMP_PAD = 19;
|
|
28
24
|
const CHARS_PAD = 5;
|
|
29
25
|
const COST_PAD = 7;
|
|
30
|
-
export async function launchTui({ version }) {
|
|
26
|
+
export async function launchTui({ version, printIntro = true }) {
|
|
31
27
|
const userConfig = (await loadUserConfig()).config;
|
|
32
28
|
const rich = isTty();
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
if (printIntro) {
|
|
30
|
+
if (rich) {
|
|
31
|
+
console.log(chalk.bold('🧿 oracle'), `${version}`, dim('— Whispering your tokens to the silicon sage'));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log(`🧿 oracle ${version} — Whispering your tokens to the silicon sage`);
|
|
35
|
+
}
|
|
38
36
|
}
|
|
39
37
|
console.log('');
|
|
40
38
|
let showingOlder = false;
|
|
@@ -44,24 +42,23 @@ export async function launchTui({ version }) {
|
|
|
44
42
|
const headerLabel = dim(`${'Status'.padEnd(STATUS_PAD)} ${'Model'.padEnd(MODEL_PAD)} ${'Mode'.padEnd(MODE_PAD)} ${'Timestamp'.padEnd(TIMESTAMP_PAD)} ${'Chars'.padStart(CHARS_PAD)} ${'Cost'.padStart(COST_PAD)} Slug`);
|
|
45
43
|
// Start with a selectable row so focus never lands on a separator
|
|
46
44
|
choices.push({ name: chalk.bold.green('ask oracle'), value: '__ask__' });
|
|
47
|
-
choices.push(disabledChoice(''));
|
|
48
45
|
if (!showingOlder) {
|
|
49
46
|
if (recent.length > 0) {
|
|
50
|
-
choices.push(
|
|
47
|
+
choices.push(new inquirer.Separator(headerLabel));
|
|
51
48
|
choices.push(...recent.map(toSessionChoice));
|
|
52
49
|
}
|
|
53
50
|
else if (older.length > 0) {
|
|
54
51
|
// No recent entries; show first page of older.
|
|
55
|
-
choices.push(
|
|
52
|
+
choices.push(new inquirer.Separator(headerLabel));
|
|
56
53
|
choices.push(...older.slice(0, PAGE_SIZE).map(toSessionChoice));
|
|
57
54
|
}
|
|
58
55
|
}
|
|
59
56
|
else if (older.length > 0) {
|
|
60
|
-
choices.push(
|
|
57
|
+
choices.push(new inquirer.Separator(headerLabel));
|
|
61
58
|
choices.push(...older.map(toSessionChoice));
|
|
62
59
|
}
|
|
63
|
-
choices.push(
|
|
64
|
-
choices.push(
|
|
60
|
+
choices.push(new inquirer.Separator(' '));
|
|
61
|
+
choices.push(new inquirer.Separator('Actions'));
|
|
65
62
|
choices.push({ name: chalk.bold.green('ask oracle'), value: '__ask__' });
|
|
66
63
|
if (!showingOlder && olderTotal > 0) {
|
|
67
64
|
choices.push({ name: 'Older page', value: '__older__' });
|
|
@@ -294,7 +291,7 @@ function printModelSummaries(models) {
|
|
|
294
291
|
console.log(chalk.bold('Models:'));
|
|
295
292
|
for (const run of models) {
|
|
296
293
|
const usage = run.usage
|
|
297
|
-
? ` tok=${run.usage.outputTokens
|
|
294
|
+
? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
|
|
298
295
|
: '';
|
|
299
296
|
console.log(` - ${chalk.cyan(run.model)} — ${run.status}${usage}`);
|
|
300
297
|
}
|
package/dist/src/heartbeat.js
CHANGED
|
@@ -7,7 +7,7 @@ export function startHeartbeat(config) {
|
|
|
7
7
|
let pending = false;
|
|
8
8
|
const start = Date.now();
|
|
9
9
|
const timer = setInterval(async () => {
|
|
10
|
-
//
|
|
10
|
+
// stop flag flips asynchronously
|
|
11
11
|
if (stopped || pending) {
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
@@ -32,7 +32,7 @@ export function startHeartbeat(config) {
|
|
|
32
32
|
}, intervalMs);
|
|
33
33
|
timer.unref?.();
|
|
34
34
|
const stop = () => {
|
|
35
|
-
//
|
|
35
|
+
// multiple callers may race to stop
|
|
36
36
|
if (stopped) {
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
@@ -9,7 +9,15 @@ const BACKGROUND_RETRY_BASE_MS = 3000;
|
|
|
9
9
|
const BACKGROUND_RETRY_MAX_MS = 15000;
|
|
10
10
|
export async function executeBackgroundResponse(params) {
|
|
11
11
|
const { client, requestBody, log, wait, heartbeatIntervalMs, now, maxWaitMs } = params;
|
|
12
|
-
|
|
12
|
+
let initialResponse;
|
|
13
|
+
try {
|
|
14
|
+
initialResponse = await client.responses.create(requestBody);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const transportError = toTransportError(error, requestBody.model);
|
|
18
|
+
log(chalk.yellow(describeTransportError(transportError, maxWaitMs)));
|
|
19
|
+
throw transportError;
|
|
20
|
+
}
|
|
13
21
|
if (!initialResponse || !initialResponse.id) {
|
|
14
22
|
throw new OracleResponseError('API did not return a response ID for the background run.', initialResponse);
|
|
15
23
|
}
|
|
@@ -60,7 +68,7 @@ async function pollBackgroundResponse(params) {
|
|
|
60
68
|
// biome-ignore lint/nursery/noUnnecessaryConditions: intentional polling loop.
|
|
61
69
|
while (true) {
|
|
62
70
|
const status = response.status ?? 'completed';
|
|
63
|
-
//
|
|
71
|
+
// firstCycle toggles immediately; keep for clarity in logs.
|
|
64
72
|
if (firstCycle) {
|
|
65
73
|
firstCycle = false;
|
|
66
74
|
log(chalk.dim(`API background response status=${status}. We'll keep retrying automatically.`));
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { createGeminiClient } from './gemini.js';
|
|
5
5
|
import { createClaudeClient } from './claude.js';
|
|
6
|
+
import { isOpenRouterBaseUrl } from './modelResolver.js';
|
|
6
7
|
export function createDefaultClientFactory() {
|
|
7
8
|
const customFactory = loadCustomClientFactory();
|
|
8
9
|
if (customFactory)
|
|
@@ -16,6 +17,9 @@ export function createDefaultClientFactory() {
|
|
|
16
17
|
return createClaudeClient(key, options.model, options.resolvedModelId, options.baseUrl);
|
|
17
18
|
}
|
|
18
19
|
let instance;
|
|
20
|
+
const defaultHeaders = isOpenRouterBaseUrl(options?.baseUrl)
|
|
21
|
+
? buildOpenRouterHeaders()
|
|
22
|
+
: undefined;
|
|
19
23
|
if (options?.azure?.endpoint) {
|
|
20
24
|
instance = new AzureOpenAI({
|
|
21
25
|
apiKey: key,
|
|
@@ -30,6 +34,7 @@ export function createDefaultClientFactory() {
|
|
|
30
34
|
apiKey: key,
|
|
31
35
|
timeout: 20 * 60 * 1000,
|
|
32
36
|
baseURL: options?.baseUrl,
|
|
37
|
+
defaultHeaders,
|
|
33
38
|
});
|
|
34
39
|
}
|
|
35
40
|
return {
|
|
@@ -41,6 +46,18 @@ export function createDefaultClientFactory() {
|
|
|
41
46
|
};
|
|
42
47
|
};
|
|
43
48
|
}
|
|
49
|
+
function buildOpenRouterHeaders() {
|
|
50
|
+
const headers = {};
|
|
51
|
+
const referer = process.env.OPENROUTER_REFERER ?? process.env.OPENROUTER_HTTP_REFERER ?? 'https://github.com/steipete/oracle';
|
|
52
|
+
const title = process.env.OPENROUTER_TITLE ?? 'Oracle CLI';
|
|
53
|
+
if (referer) {
|
|
54
|
+
headers['HTTP-Referer'] = referer;
|
|
55
|
+
}
|
|
56
|
+
if (title) {
|
|
57
|
+
headers['X-Title'] = title;
|
|
58
|
+
}
|
|
59
|
+
return headers;
|
|
60
|
+
}
|
|
44
61
|
function loadCustomClientFactory() {
|
|
45
62
|
const override = process.env.ORACLE_CLIENT_FACTORY;
|
|
46
63
|
if (!override) {
|
|
@@ -3,11 +3,12 @@ import { countTokens as countTokensGpt5Pro } from 'gpt-tokenizer/model/gpt-5-pro
|
|
|
3
3
|
import { countTokens as countTokensAnthropicRaw } from '@anthropic-ai/tokenizer';
|
|
4
4
|
import { stringifyTokenizerInput } from './tokenStringifier.js';
|
|
5
5
|
export const DEFAULT_MODEL = 'gpt-5.1-pro';
|
|
6
|
-
export const PRO_MODELS = new Set(['gpt-5.1-pro', 'gpt-5-pro', 'claude-4.1-opus']);
|
|
6
|
+
export const PRO_MODELS = new Set(['gpt-5.1-pro', 'gpt-5-pro', 'claude-4.5-sonnet', 'claude-4.1-opus']);
|
|
7
7
|
const countTokensAnthropic = (input) => countTokensAnthropicRaw(stringifyTokenizerInput(input));
|
|
8
8
|
export const MODEL_CONFIGS = {
|
|
9
9
|
'gpt-5.1-pro': {
|
|
10
10
|
model: 'gpt-5.1-pro',
|
|
11
|
+
provider: 'openai',
|
|
11
12
|
tokenizer: countTokensGpt5Pro,
|
|
12
13
|
inputLimit: 196000,
|
|
13
14
|
pricing: {
|
|
@@ -18,6 +19,7 @@ export const MODEL_CONFIGS = {
|
|
|
18
19
|
},
|
|
19
20
|
'gpt-5-pro': {
|
|
20
21
|
model: 'gpt-5-pro',
|
|
22
|
+
provider: 'openai',
|
|
21
23
|
tokenizer: countTokensGpt5Pro,
|
|
22
24
|
inputLimit: 196000,
|
|
23
25
|
pricing: {
|
|
@@ -28,6 +30,7 @@ export const MODEL_CONFIGS = {
|
|
|
28
30
|
},
|
|
29
31
|
'gpt-5.1': {
|
|
30
32
|
model: 'gpt-5.1',
|
|
33
|
+
provider: 'openai',
|
|
31
34
|
tokenizer: countTokensGpt5,
|
|
32
35
|
inputLimit: 196000,
|
|
33
36
|
pricing: {
|
|
@@ -38,6 +41,7 @@ export const MODEL_CONFIGS = {
|
|
|
38
41
|
},
|
|
39
42
|
'gpt-5.1-codex': {
|
|
40
43
|
model: 'gpt-5.1-codex',
|
|
44
|
+
provider: 'openai',
|
|
41
45
|
tokenizer: countTokensGpt5,
|
|
42
46
|
inputLimit: 196000,
|
|
43
47
|
pricing: {
|
|
@@ -48,6 +52,7 @@ export const MODEL_CONFIGS = {
|
|
|
48
52
|
},
|
|
49
53
|
'gemini-3-pro': {
|
|
50
54
|
model: 'gemini-3-pro',
|
|
55
|
+
provider: 'google',
|
|
51
56
|
tokenizer: countTokensGpt5Pro,
|
|
52
57
|
inputLimit: 200000,
|
|
53
58
|
pricing: {
|
|
@@ -61,6 +66,7 @@ export const MODEL_CONFIGS = {
|
|
|
61
66
|
'claude-4.5-sonnet': {
|
|
62
67
|
model: 'claude-4.5-sonnet',
|
|
63
68
|
apiModel: 'claude-sonnet-4-5',
|
|
69
|
+
provider: 'anthropic',
|
|
64
70
|
tokenizer: countTokensAnthropic,
|
|
65
71
|
inputLimit: 200000,
|
|
66
72
|
pricing: {
|
|
@@ -74,6 +80,7 @@ export const MODEL_CONFIGS = {
|
|
|
74
80
|
'claude-4.1-opus': {
|
|
75
81
|
model: 'claude-4.1-opus',
|
|
76
82
|
apiModel: 'claude-opus-4-1',
|
|
83
|
+
provider: 'anthropic',
|
|
77
84
|
tokenizer: countTokensAnthropic,
|
|
78
85
|
inputLimit: 200000,
|
|
79
86
|
pricing: {
|
|
@@ -87,6 +94,7 @@ export const MODEL_CONFIGS = {
|
|
|
87
94
|
'grok-4.1': {
|
|
88
95
|
model: 'grok-4.1',
|
|
89
96
|
apiModel: 'grok-4-1-fast-reasoning',
|
|
97
|
+
provider: 'xai',
|
|
90
98
|
tokenizer: countTokensGpt5Pro,
|
|
91
99
|
inputLimit: 2_000_000,
|
|
92
100
|
pricing: {
|
|
@@ -101,6 +109,6 @@ export const MODEL_CONFIGS = {
|
|
|
101
109
|
};
|
|
102
110
|
export const DEFAULT_SYSTEM_PROMPT = [
|
|
103
111
|
'You are Oracle, a focused one-shot problem solver.',
|
|
104
|
-
'Emphasize direct answers
|
|
112
|
+
'Emphasize direct answers and cite any files referenced.',
|
|
105
113
|
].join(' ');
|
|
106
114
|
export const TOKENIZER_OPTIONS = { allowedSpecial: 'all' };
|