@steipete/oracle 0.7.2 → 0.7.4
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 +6 -0
- package/dist/bin/oracle-cli.js +3 -1
- package/dist/src/browser/actions/attachments.js +7 -3
- package/dist/src/browser/actions/modelSelection.js +51 -10
- package/dist/src/browser/actions/thinkingTime.js +40 -31
- package/dist/src/browser/chromeLifecycle.js +2 -0
- package/dist/src/browser/config.js +1 -1
- package/dist/src/browser/index.js +57 -9
- package/dist/src/browser/reattach.js +192 -17
- package/dist/src/cli/browserConfig.js +26 -16
- package/dist/src/cli/notifier.js +8 -2
- package/dist/src/cli/options.js +13 -3
- package/dist/src/cli/oscUtils.js +1 -19
- package/dist/src/cli/sessionDisplay.js +6 -3
- package/dist/src/cli/sessionTable.js +5 -1
- package/dist/src/oracle/files.js +8 -1
- package/dist/src/oracle/modelResolver.js +11 -4
- package/dist/src/oracle/multiModelRunner.js +3 -14
- package/dist/src/oracle/oscProgress.js +12 -61
- package/dist/src/oracle/run.js +62 -34
- package/dist/src/sessionManager.js +91 -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/package.json +43 -26
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/README.md +24 -0
- package/vendor/oracle-notifier/build-notifier.sh +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import CDP from 'chrome-remote-interface';
|
|
2
|
-
import
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { mkdtemp, mkdir, rm } from 'node:fs/promises';
|
|
5
|
+
import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, } from './pageActions.js';
|
|
6
|
+
import { launchChrome, connectToChrome, hideChromeWindow } from './chromeLifecycle.js';
|
|
7
|
+
import { resolveBrowserConfig } from './config.js';
|
|
8
|
+
import { syncCookies } from './cookies.js';
|
|
9
|
+
import { CHATGPT_URL } from './constants.js';
|
|
10
|
+
import { delay } from './utils.js';
|
|
3
11
|
function pickTarget(targets, runtime) {
|
|
4
12
|
if (!Array.isArray(targets) || targets.length === 0) {
|
|
5
13
|
return undefined;
|
|
@@ -18,33 +26,114 @@ function pickTarget(targets, runtime) {
|
|
|
18
26
|
return targets.find((t) => t.type === 'page') ?? targets[0];
|
|
19
27
|
}
|
|
20
28
|
export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
29
|
+
const recoverSession = deps.recoverSession ??
|
|
30
|
+
(async (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps));
|
|
21
31
|
if (!runtime.chromePort) {
|
|
22
|
-
|
|
32
|
+
logger('No running Chrome detected; reopening browser to locate the session.');
|
|
33
|
+
return recoverSession(runtime, config);
|
|
23
34
|
}
|
|
24
35
|
const host = runtime.chromeHost ?? '127.0.0.1';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
try {
|
|
37
|
+
const listTargets = deps.listTargets ??
|
|
38
|
+
(async () => {
|
|
39
|
+
const targets = await CDP.List({ host, port: runtime.chromePort });
|
|
40
|
+
return targets;
|
|
41
|
+
});
|
|
42
|
+
const connect = deps.connect ?? ((options) => CDP(options));
|
|
43
|
+
const targetList = (await listTargets());
|
|
44
|
+
const target = pickTarget(targetList, runtime);
|
|
45
|
+
const client = (await connect({
|
|
46
|
+
host,
|
|
47
|
+
port: runtime.chromePort,
|
|
48
|
+
target: target?.targetId,
|
|
49
|
+
}));
|
|
50
|
+
const { Runtime, DOM } = client;
|
|
51
|
+
if (Runtime?.enable) {
|
|
52
|
+
await Runtime.enable();
|
|
53
|
+
}
|
|
54
|
+
if (DOM && typeof DOM.enable === 'function') {
|
|
55
|
+
await DOM.enable();
|
|
56
|
+
}
|
|
57
|
+
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
58
|
+
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
59
|
+
const timeoutMs = config?.timeoutMs ?? 120_000;
|
|
60
|
+
const answer = await waitForResponse(Runtime, timeoutMs, logger);
|
|
61
|
+
const markdown = (await captureMarkdown(Runtime, answer.meta, logger)) ?? answer.text;
|
|
62
|
+
if (client && typeof client.close === 'function') {
|
|
63
|
+
try {
|
|
64
|
+
await client.close();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// ignore
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { answerText: answer.text, answerMarkdown: markdown };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
logger(`Existing Chrome reattach failed (${message}); reopening browser to locate the session.`);
|
|
75
|
+
return recoverSession(runtime, config);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
79
|
+
const resolved = resolveBrowserConfig(config ?? {});
|
|
80
|
+
const manualLogin = Boolean(resolved.manualLogin);
|
|
81
|
+
const userDataDir = manualLogin
|
|
82
|
+
? resolved.manualLoginProfileDir ?? path.join(os.homedir(), '.oracle', 'browser-profile')
|
|
83
|
+
: await mkdtemp(path.join(os.tmpdir(), 'oracle-reattach-'));
|
|
84
|
+
if (manualLogin) {
|
|
85
|
+
await mkdir(userDataDir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
const chrome = await launchChrome(resolved, userDataDir, logger);
|
|
88
|
+
const chromeHost = chrome.host ?? '127.0.0.1';
|
|
89
|
+
const client = await connectToChrome(chrome.port, logger, chromeHost);
|
|
90
|
+
const { Network, Page, Runtime, DOM } = client;
|
|
39
91
|
if (Runtime?.enable) {
|
|
40
92
|
await Runtime.enable();
|
|
41
93
|
}
|
|
42
94
|
if (DOM && typeof DOM.enable === 'function') {
|
|
43
95
|
await DOM.enable();
|
|
44
96
|
}
|
|
97
|
+
if (!resolved.headless && resolved.hideWindow) {
|
|
98
|
+
await hideChromeWindow(chrome, logger);
|
|
99
|
+
}
|
|
100
|
+
let appliedCookies = 0;
|
|
101
|
+
if (!manualLogin && resolved.cookieSync) {
|
|
102
|
+
appliedCookies = await syncCookies(Network, resolved.url, resolved.chromeProfile, logger, {
|
|
103
|
+
allowErrors: resolved.allowCookieErrors,
|
|
104
|
+
filterNames: resolved.cookieNames ?? undefined,
|
|
105
|
+
inlineCookies: resolved.inlineCookies ?? undefined,
|
|
106
|
+
cookiePath: resolved.chromeCookiePath ?? undefined,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
await navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger);
|
|
110
|
+
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
111
|
+
await ensureLoggedIn(Runtime, logger, { appliedCookies });
|
|
112
|
+
if (resolved.url !== CHATGPT_URL) {
|
|
113
|
+
await navigateToChatGPT(Page, Runtime, resolved.url, logger);
|
|
114
|
+
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
115
|
+
}
|
|
116
|
+
await ensurePromptReady(Runtime, resolved.inputTimeoutMs, logger);
|
|
117
|
+
const conversationUrl = buildConversationUrl(runtime, resolved.url);
|
|
118
|
+
if (conversationUrl) {
|
|
119
|
+
logger(`Reopening conversation at ${conversationUrl}`);
|
|
120
|
+
await navigateToChatGPT(Page, Runtime, conversationUrl, logger);
|
|
121
|
+
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
122
|
+
await ensurePromptReady(Runtime, resolved.inputTimeoutMs, logger);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const opened = await openConversationFromSidebar(Runtime, {
|
|
126
|
+
conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ''),
|
|
127
|
+
preferProjects: resolved.url !== CHATGPT_URL,
|
|
128
|
+
});
|
|
129
|
+
if (!opened) {
|
|
130
|
+
throw new Error('Unable to locate prior ChatGPT conversation in sidebar.');
|
|
131
|
+
}
|
|
132
|
+
await waitForLocationChange(Runtime, 15_000);
|
|
133
|
+
}
|
|
45
134
|
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
46
135
|
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
47
|
-
const timeoutMs =
|
|
136
|
+
const timeoutMs = resolved.timeoutMs ?? 120_000;
|
|
48
137
|
const answer = await waitForResponse(Runtime, timeoutMs, logger);
|
|
49
138
|
const markdown = (await captureMarkdown(Runtime, answer.meta, logger)) ?? answer.text;
|
|
50
139
|
if (client && typeof client.close === 'function') {
|
|
@@ -55,5 +144,91 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
55
144
|
// ignore
|
|
56
145
|
}
|
|
57
146
|
}
|
|
147
|
+
if (!resolved.keepBrowser && !manualLogin) {
|
|
148
|
+
try {
|
|
149
|
+
await chrome.kill();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// ignore
|
|
153
|
+
}
|
|
154
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
155
|
+
}
|
|
58
156
|
return { answerText: answer.text, answerMarkdown: markdown };
|
|
59
157
|
}
|
|
158
|
+
function extractConversationIdFromUrl(url) {
|
|
159
|
+
if (!url)
|
|
160
|
+
return undefined;
|
|
161
|
+
const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
|
|
162
|
+
return match?.[1];
|
|
163
|
+
}
|
|
164
|
+
function buildConversationUrl(runtime, baseUrl) {
|
|
165
|
+
if (runtime.tabUrl) {
|
|
166
|
+
return runtime.tabUrl;
|
|
167
|
+
}
|
|
168
|
+
const conversationId = runtime.conversationId;
|
|
169
|
+
if (!conversationId) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const base = new URL(baseUrl);
|
|
174
|
+
return `${base.origin}/c/${conversationId}`;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function openConversationFromSidebar(Runtime, options) {
|
|
181
|
+
const response = await Runtime.evaluate({
|
|
182
|
+
expression: `(() => {
|
|
183
|
+
const conversationId = ${JSON.stringify(options.conversationId ?? null)};
|
|
184
|
+
const preferProjects = ${JSON.stringify(Boolean(options.preferProjects))};
|
|
185
|
+
const nav = document.querySelector('nav') || document.querySelector('aside') || document.body;
|
|
186
|
+
if (preferProjects) {
|
|
187
|
+
const projectLink = Array.from(nav.querySelectorAll('a,button'))
|
|
188
|
+
.find((el) => (el.textContent || '').trim().toLowerCase() === 'projects');
|
|
189
|
+
if (projectLink) {
|
|
190
|
+
projectLink.click();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const links = Array.from(nav.querySelectorAll('a[href]'))
|
|
194
|
+
.filter((el) => el instanceof HTMLAnchorElement)
|
|
195
|
+
.map((el) => el);
|
|
196
|
+
const convoLinks = links.filter((el) => el.href.includes('/c/'));
|
|
197
|
+
let target = null;
|
|
198
|
+
if (conversationId) {
|
|
199
|
+
target = convoLinks.find((el) => el.href.includes('/c/' + conversationId));
|
|
200
|
+
}
|
|
201
|
+
if (!target && convoLinks.length > 0) {
|
|
202
|
+
target = convoLinks[0];
|
|
203
|
+
}
|
|
204
|
+
if (target) {
|
|
205
|
+
target.scrollIntoView({ block: 'center' });
|
|
206
|
+
target.click();
|
|
207
|
+
return { ok: true, href: target.href, count: convoLinks.length };
|
|
208
|
+
}
|
|
209
|
+
return { ok: false, count: convoLinks.length };
|
|
210
|
+
})()`,
|
|
211
|
+
returnByValue: true,
|
|
212
|
+
});
|
|
213
|
+
return Boolean(response.result?.value?.ok);
|
|
214
|
+
}
|
|
215
|
+
async function waitForLocationChange(Runtime, timeoutMs) {
|
|
216
|
+
const start = Date.now();
|
|
217
|
+
let lastHref = '';
|
|
218
|
+
while (Date.now() - start < timeoutMs) {
|
|
219
|
+
const { result } = await Runtime.evaluate({ expression: 'location.href', returnByValue: true });
|
|
220
|
+
const href = typeof result?.value === 'string' ? result.value : '';
|
|
221
|
+
if (lastHref && href !== lastHref) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
lastHref = href;
|
|
225
|
+
await delay(200);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
229
|
+
export const __test__ = {
|
|
230
|
+
pickTarget,
|
|
231
|
+
extractConversationIdFromUrl,
|
|
232
|
+
buildConversationUrl,
|
|
233
|
+
openConversationFromSidebar,
|
|
234
|
+
};
|
|
@@ -5,17 +5,20 @@ import { getOracleHomeDir } from '../oracleHome.js';
|
|
|
5
5
|
const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
|
|
6
6
|
const DEFAULT_BROWSER_INPUT_TIMEOUT_MS = 30_000;
|
|
7
7
|
const DEFAULT_CHROME_PROFILE = 'Default';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
'gpt-5.
|
|
13
|
-
'gpt-5.2'
|
|
14
|
-
|
|
15
|
-
'gpt-5.
|
|
16
|
-
'gpt-5
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
// Ordered array: most specific models first to ensure correct selection.
|
|
9
|
+
// The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
|
|
10
|
+
const BROWSER_MODEL_LABELS = [
|
|
11
|
+
// Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
|
|
12
|
+
['gpt-5.2-thinking', 'GPT-5.2 Thinking'],
|
|
13
|
+
['gpt-5.2-instant', 'GPT-5.2 Instant'],
|
|
14
|
+
['gpt-5.2-pro', 'GPT-5.2 Pro'],
|
|
15
|
+
['gpt-5.1-pro', 'GPT-5.2 Pro'],
|
|
16
|
+
['gpt-5-pro', 'GPT-5.2 Pro'],
|
|
17
|
+
// Base models last (least specific)
|
|
18
|
+
['gpt-5.2', 'GPT-5.2'], // Selects "Auto" in ChatGPT UI
|
|
19
|
+
['gpt-5.1', 'GPT-5.2'], // Legacy alias → Auto
|
|
20
|
+
['gemini-3-pro', 'Gemini 3 Pro'],
|
|
21
|
+
];
|
|
19
22
|
export function normalizeChatGptModelForBrowser(model) {
|
|
20
23
|
const normalized = model.toLowerCase();
|
|
21
24
|
if (!normalized.startsWith('gpt-') || normalized.includes('codex')) {
|
|
@@ -25,10 +28,11 @@ export function normalizeChatGptModelForBrowser(model) {
|
|
|
25
28
|
if (normalized === 'gpt-5-pro' || normalized === 'gpt-5.1-pro' || normalized.endsWith('-pro')) {
|
|
26
29
|
return 'gpt-5.2-pro';
|
|
27
30
|
}
|
|
28
|
-
//
|
|
29
|
-
if (normalized === 'gpt-5.2-instant') {
|
|
30
|
-
return
|
|
31
|
+
// Explicit model variants: keep as-is (they have their own browser labels)
|
|
32
|
+
if (normalized === 'gpt-5.2-thinking' || normalized === 'gpt-5.2-instant') {
|
|
33
|
+
return normalized;
|
|
31
34
|
}
|
|
35
|
+
// Legacy aliases: map to base GPT-5.2 (Auto)
|
|
32
36
|
if (normalized === 'gpt-5.1') {
|
|
33
37
|
return 'gpt-5.2';
|
|
34
38
|
}
|
|
@@ -86,7 +90,7 @@ export async function buildBrowserConfig(options) {
|
|
|
86
90
|
// Allow cookie failures by default so runs can continue without Chrome/Keychain secrets.
|
|
87
91
|
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
88
92
|
remoteChrome,
|
|
89
|
-
|
|
93
|
+
thinkingTime: options.browserThinkingTime,
|
|
90
94
|
};
|
|
91
95
|
}
|
|
92
96
|
function selectBrowserPort(options) {
|
|
@@ -100,7 +104,13 @@ function selectBrowserPort(options) {
|
|
|
100
104
|
}
|
|
101
105
|
export function mapModelToBrowserLabel(model) {
|
|
102
106
|
const normalized = normalizeChatGptModelForBrowser(model);
|
|
103
|
-
|
|
107
|
+
// Iterate ordered array to find first match (most specific first)
|
|
108
|
+
for (const [key, label] of BROWSER_MODEL_LABELS) {
|
|
109
|
+
if (key === normalized) {
|
|
110
|
+
return label;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return DEFAULT_MODEL_TARGET;
|
|
104
114
|
}
|
|
105
115
|
export function resolveBrowserModelLabel(input, model) {
|
|
106
116
|
const trimmed = input?.trim?.() ?? '';
|
package/dist/src/cli/notifier.js
CHANGED
|
@@ -2,6 +2,7 @@ import notifier from 'toasted-notifier';
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { formatUSD, formatNumber } from '../oracle/format.js';
|
|
4
4
|
import { MODEL_CONFIGS } from '../oracle/config.js';
|
|
5
|
+
import { estimateUsdCost } from 'tokentally';
|
|
5
6
|
import fs from 'node:fs/promises';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { createRequire } from 'node:module';
|
|
@@ -131,8 +132,13 @@ function inferCost(payload) {
|
|
|
131
132
|
const config = MODEL_CONFIGS[model];
|
|
132
133
|
if (!config?.pricing)
|
|
133
134
|
return undefined;
|
|
134
|
-
return (
|
|
135
|
-
usage.outputTokens
|
|
135
|
+
return (estimateUsdCost({
|
|
136
|
+
usage: { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens },
|
|
137
|
+
pricing: {
|
|
138
|
+
inputUsdPerToken: config.pricing.inputPerToken,
|
|
139
|
+
outputUsdPerToken: config.pricing.outputPerToken,
|
|
140
|
+
},
|
|
141
|
+
})?.totalUsd ?? undefined);
|
|
136
142
|
}
|
|
137
143
|
function parseToggle(value) {
|
|
138
144
|
if (value == null)
|
package/dist/src/cli/options.js
CHANGED
|
@@ -187,6 +187,13 @@ export function inferModelFromLabel(modelValue) {
|
|
|
187
187
|
if ((normalized.includes('5.2') || normalized.includes('5_2')) && normalized.includes('pro')) {
|
|
188
188
|
return 'gpt-5.2-pro';
|
|
189
189
|
}
|
|
190
|
+
// Browser-only: pass through 5.2 thinking/instant variants for browser label mapping
|
|
191
|
+
if ((normalized.includes('5.2') || normalized.includes('5_2')) && normalized.includes('thinking')) {
|
|
192
|
+
return 'gpt-5.2-thinking';
|
|
193
|
+
}
|
|
194
|
+
if ((normalized.includes('5.2') || normalized.includes('5_2')) && normalized.includes('instant')) {
|
|
195
|
+
return 'gpt-5.2-instant';
|
|
196
|
+
}
|
|
190
197
|
if (normalized.includes('5.0') || normalized.includes('5-pro')) {
|
|
191
198
|
return 'gpt-5-pro';
|
|
192
199
|
}
|
|
@@ -205,8 +212,11 @@ export function inferModelFromLabel(modelValue) {
|
|
|
205
212
|
if (normalized.includes('5.1') || normalized.includes('5_1')) {
|
|
206
213
|
return 'gpt-5.1';
|
|
207
214
|
}
|
|
208
|
-
if (normalized.includes('
|
|
209
|
-
return 'gpt-5.
|
|
215
|
+
if (normalized.includes('thinking')) {
|
|
216
|
+
return 'gpt-5.2-thinking';
|
|
217
|
+
}
|
|
218
|
+
if (normalized.includes('instant') || normalized.includes('fast')) {
|
|
219
|
+
return 'gpt-5.2-instant';
|
|
210
220
|
}
|
|
211
|
-
return 'gpt-5.
|
|
221
|
+
return 'gpt-5.2';
|
|
212
222
|
}
|
package/dist/src/cli/oscUtils.js
CHANGED
|
@@ -1,20 +1,2 @@
|
|
|
1
1
|
// Utilities for handling OSC progress codes embedded in stored logs.
|
|
2
|
-
|
|
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
|
-
}
|
|
2
|
+
export { sanitizeOscProgress } from 'osc-progress';
|
|
@@ -76,10 +76,13 @@ export async function attachSession(sessionId, options) {
|
|
|
76
76
|
const isVerbose = Boolean(process.env.ORACLE_VERBOSE_RENDER);
|
|
77
77
|
const runtime = metadata.browser?.runtime;
|
|
78
78
|
const controllerAlive = isProcessAlive(runtime?.controllerPid);
|
|
79
|
-
const
|
|
79
|
+
const hasChromeDisconnect = metadata.response?.incompleteReason === 'chrome-disconnected';
|
|
80
|
+
const statusAllowsReattach = metadata.status === 'running' || (metadata.status === 'error' && hasChromeDisconnect);
|
|
81
|
+
const hasFallbackSessionInfo = Boolean(runtime?.chromePort || runtime?.tabUrl || runtime?.conversationId);
|
|
82
|
+
const canReattach = statusAllowsReattach &&
|
|
80
83
|
metadata.mode === 'browser' &&
|
|
81
|
-
|
|
82
|
-
(
|
|
84
|
+
hasFallbackSessionInfo &&
|
|
85
|
+
(hasChromeDisconnect || (runtime?.controllerPid && !controllerAlive));
|
|
83
86
|
if (canReattach) {
|
|
84
87
|
const portInfo = runtime?.chromePort ? `port ${runtime.chromePort}` : 'unknown port';
|
|
85
88
|
const urlInfo = runtime?.tabUrl ? `url=${runtime.tabUrl}` : 'url=unknown';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import kleur from 'kleur';
|
|
3
3
|
import { MODEL_CONFIGS } from '../oracle.js';
|
|
4
|
+
import { estimateUsdCost } from 'tokentally';
|
|
4
5
|
const isRich = (rich) => rich ?? Boolean(process.stdout.isTTY && chalk.level > 0);
|
|
5
6
|
const dim = (text, rich) => (rich ? kleur.dim(text) : text);
|
|
6
7
|
export const STATUS_PAD = 9;
|
|
@@ -48,7 +49,10 @@ export function resolveSessionCost(meta) {
|
|
|
48
49
|
}
|
|
49
50
|
const input = meta.usage.inputTokens ?? 0;
|
|
50
51
|
const output = meta.usage.outputTokens ?? 0;
|
|
51
|
-
const cost =
|
|
52
|
+
const cost = estimateUsdCost({
|
|
53
|
+
usage: { inputTokens: input, outputTokens: output },
|
|
54
|
+
pricing: { inputUsdPerToken: pricing.inputPerToken, outputUsdPerToken: pricing.outputPerToken },
|
|
55
|
+
})?.totalUsd ?? 0;
|
|
52
56
|
return cost > 0 ? cost : null;
|
|
53
57
|
}
|
|
54
58
|
export function formatTimestampAligned(iso) {
|
package/dist/src/oracle/files.js
CHANGED
|
@@ -13,7 +13,14 @@ export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEF
|
|
|
13
13
|
const useNativeFilesystem = fsModule === DEFAULT_FS || isNativeFsModule(fsModule);
|
|
14
14
|
let candidatePaths = [];
|
|
15
15
|
if (useNativeFilesystem) {
|
|
16
|
-
|
|
16
|
+
if (partitioned.globPatterns.length === 0 &&
|
|
17
|
+
partitioned.excludePatterns.length === 0 &&
|
|
18
|
+
partitioned.literalDirectories.length === 0) {
|
|
19
|
+
candidatePaths = Array.from(new Set(partitioned.literalFiles));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
candidatePaths = await expandWithNativeGlob(partitioned, cwd);
|
|
23
|
+
}
|
|
17
24
|
}
|
|
18
25
|
else {
|
|
19
26
|
if (partitioned.globPatterns.length > 0 || partitioned.excludePatterns.length > 0) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MODEL_CONFIGS, PRO_MODELS } from './config.js';
|
|
2
2
|
import { countTokens as countTokensGpt5Pro } from 'gpt-tokenizer/model/gpt-5-pro';
|
|
3
|
+
import { pricingFromUsdPerMillion } from 'tokentally';
|
|
3
4
|
const OPENROUTER_DEFAULT_BASE = 'https://openrouter.ai/api/v1';
|
|
4
5
|
const OPENROUTER_MODELS_ENDPOINT = 'https://openrouter.ai/api/v1/models';
|
|
5
6
|
export function isKnownModel(model) {
|
|
@@ -96,10 +97,16 @@ export async function resolveModelConfig(model, options = {}) {
|
|
|
96
97
|
provider: known?.provider ?? 'other',
|
|
97
98
|
inputLimit: info.context_length ?? known?.inputLimit ?? 200_000,
|
|
98
99
|
pricing: info.pricing && info.pricing.prompt != null && info.pricing.completion != null
|
|
99
|
-
? {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
? (() => {
|
|
101
|
+
const pricing = pricingFromUsdPerMillion({
|
|
102
|
+
inputUsdPerMillion: info.pricing.prompt,
|
|
103
|
+
outputUsdPerMillion: info.pricing.completion,
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
inputPerToken: pricing.inputUsdPerToken,
|
|
107
|
+
outputPerToken: pricing.outputUsdPerToken,
|
|
108
|
+
};
|
|
109
|
+
})()
|
|
103
110
|
: known?.pricing ?? null,
|
|
104
111
|
supportsBackground: known?.supportsBackground ?? true,
|
|
105
112
|
supportsSearch: known?.supportsSearch ?? true,
|
|
@@ -2,24 +2,13 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, } from '../oracle.js';
|
|
4
4
|
import { sessionStore } from '../sessionStore.js';
|
|
5
|
-
|
|
6
|
-
const OSC_PROGRESS_END = '\u001b\\';
|
|
5
|
+
import { findOscProgressSequences, OSC_PROGRESS_PREFIX } from 'osc-progress';
|
|
7
6
|
function forwardOscProgress(chunk, shouldForward) {
|
|
8
7
|
if (!shouldForward || !chunk.includes(OSC_PROGRESS_PREFIX)) {
|
|
9
8
|
return;
|
|
10
9
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const start = chunk.indexOf(OSC_PROGRESS_PREFIX, searchFrom);
|
|
14
|
-
if (start === -1) {
|
|
15
|
-
break;
|
|
16
|
-
}
|
|
17
|
-
const end = chunk.indexOf(OSC_PROGRESS_END, start + OSC_PROGRESS_PREFIX.length);
|
|
18
|
-
if (end === -1) {
|
|
19
|
-
break;
|
|
20
|
-
}
|
|
21
|
-
process.stdout.write(chunk.slice(start, end + OSC_PROGRESS_END.length));
|
|
22
|
-
searchFrom = end + OSC_PROGRESS_END.length;
|
|
10
|
+
for (const seq of findOscProgressSequences(chunk)) {
|
|
11
|
+
process.stdout.write(seq.raw);
|
|
23
12
|
}
|
|
24
13
|
}
|
|
25
14
|
const defaultDeps = {
|
|
@@ -1,66 +1,17 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
-
|
|
3
|
-
const ST = '\u001b\\';
|
|
4
|
-
function sanitizeLabel(label) {
|
|
5
|
-
const withoutEscape = label.split('\u001b').join('');
|
|
6
|
-
const withoutBellAndSt = withoutEscape.replaceAll('\u0007', '').replaceAll('\u009c', '');
|
|
7
|
-
return withoutBellAndSt.replaceAll(']', '').trim();
|
|
8
|
-
}
|
|
2
|
+
import { startOscProgress as startOscProgressShared, supportsOscProgress as supportsOscProgressShared, } from 'osc-progress';
|
|
9
3
|
export function supportsOscProgress(env = process.env, isTty = process.stdout.isTTY) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
if (env.ORACLE_FORCE_OSC_PROGRESS === '1') {
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
|
-
const termProgram = (env.TERM_PROGRAM ?? '').toLowerCase();
|
|
20
|
-
if (termProgram.includes('ghostty')) {
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
if (termProgram.includes('wezterm')) {
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
if (env.WT_SESSION) {
|
|
27
|
-
return true; // Windows Terminal exposes this
|
|
28
|
-
}
|
|
29
|
-
return false;
|
|
4
|
+
return supportsOscProgressShared(env, isTty, {
|
|
5
|
+
disableEnvVar: 'ORACLE_NO_OSC_PROGRESS',
|
|
6
|
+
forceEnvVar: 'ORACLE_FORCE_OSC_PROGRESS',
|
|
7
|
+
});
|
|
30
8
|
}
|
|
31
9
|
export function startOscProgress(options = {}) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return () => {
|
|
40
|
-
write(`${OSC}0;0;${cleanLabel}${ST}`);
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
const target = Math.max(targetMs, 1_000);
|
|
44
|
-
const send = (state, percent) => {
|
|
45
|
-
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
46
|
-
write(`${OSC}${state};${clamped};${cleanLabel}${ST}`);
|
|
47
|
-
};
|
|
48
|
-
const startedAt = Date.now();
|
|
49
|
-
send(1, 0); // activate progress bar
|
|
50
|
-
const timer = setInterval(() => {
|
|
51
|
-
const elapsed = Date.now() - startedAt;
|
|
52
|
-
const percent = Math.min(99, (elapsed / target) * 100);
|
|
53
|
-
send(1, percent);
|
|
54
|
-
}, 900);
|
|
55
|
-
timer.unref?.();
|
|
56
|
-
let stopped = false;
|
|
57
|
-
return () => {
|
|
58
|
-
// multiple callers may try to stop
|
|
59
|
-
if (stopped) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
stopped = true;
|
|
63
|
-
clearInterval(timer);
|
|
64
|
-
send(0, 0); // clear the progress bar
|
|
65
|
-
};
|
|
10
|
+
return startOscProgressShared({
|
|
11
|
+
...options,
|
|
12
|
+
// Preserve Oracle's previous default: progress emits to stdout.
|
|
13
|
+
write: options.write ?? ((text) => process.stdout.write(text)),
|
|
14
|
+
disableEnvVar: 'ORACLE_NO_OSC_PROGRESS',
|
|
15
|
+
forceEnvVar: 'ORACLE_FORCE_OSC_PROGRESS',
|
|
16
|
+
});
|
|
66
17
|
}
|