@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
|
@@ -9,7 +9,8 @@ export async function ensureModelSelection(Runtime, desiredModel, logger) {
|
|
|
9
9
|
const result = outcome.result?.value;
|
|
10
10
|
switch (result?.status) {
|
|
11
11
|
case 'already-selected':
|
|
12
|
-
case 'switched':
|
|
12
|
+
case 'switched':
|
|
13
|
+
case 'switched-best-effort': {
|
|
13
14
|
const label = result.label ?? desiredModel;
|
|
14
15
|
logger(`Model picker: ${label}`);
|
|
15
16
|
return;
|
|
@@ -24,18 +25,26 @@ export async function ensureModelSelection(Runtime, desiredModel, logger) {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Builds the DOM expression that runs inside the ChatGPT tab to select a model.
|
|
30
|
+
* The string is evaluated inside Chrome, so keep it self-contained and well-commented.
|
|
31
|
+
*/
|
|
27
32
|
function buildModelSelectionExpression(targetModel) {
|
|
28
33
|
const matchers = buildModelMatchersLiteral(targetModel);
|
|
29
34
|
const labelLiteral = JSON.stringify(matchers.labelTokens);
|
|
30
35
|
const idLiteral = JSON.stringify(matchers.testIdTokens);
|
|
36
|
+
const primaryLabelLiteral = JSON.stringify(targetModel);
|
|
31
37
|
const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
|
|
32
38
|
const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
|
|
33
39
|
return `(() => {
|
|
40
|
+
// Capture the selectors and matcher literals up front so the browser expression stays pure.
|
|
34
41
|
const BUTTON_SELECTOR = '${MODEL_BUTTON_SELECTOR}';
|
|
35
42
|
const LABEL_TOKENS = ${labelLiteral};
|
|
36
43
|
const TEST_IDS = ${idLiteral};
|
|
37
|
-
const
|
|
38
|
-
const
|
|
44
|
+
const PRIMARY_LABEL = ${primaryLabelLiteral};
|
|
45
|
+
const INITIAL_WAIT_MS = 150;
|
|
46
|
+
const REOPEN_INTERVAL_MS = 400;
|
|
47
|
+
const MAX_WAIT_MS = 20000;
|
|
39
48
|
const normalizeText = (value) => {
|
|
40
49
|
if (!value) {
|
|
41
50
|
return '';
|
|
@@ -46,6 +55,12 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
46
55
|
.replace(/\\s+/g, ' ')
|
|
47
56
|
.trim();
|
|
48
57
|
};
|
|
58
|
+
// Normalize every candidate token to keep fuzzy matching deterministic.
|
|
59
|
+
const normalizedTarget = normalizeText(PRIMARY_LABEL);
|
|
60
|
+
const normalizedTokens = Array.from(new Set([normalizedTarget, ...LABEL_TOKENS]))
|
|
61
|
+
.map((token) => normalizeText(token))
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
const targetWords = normalizedTarget.split(' ').filter(Boolean);
|
|
49
64
|
|
|
50
65
|
const button = document.querySelector(BUTTON_SELECTOR);
|
|
51
66
|
if (!button) {
|
|
@@ -54,6 +69,7 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
54
69
|
|
|
55
70
|
let lastPointerClick = 0;
|
|
56
71
|
const pointerClick = () => {
|
|
72
|
+
// Some menus ignore synthetic click events.
|
|
57
73
|
const down = new PointerEvent('pointerdown', { bubbles: true, pointerId: 1, pointerType: 'mouse' });
|
|
58
74
|
const up = new PointerEvent('pointerup', { bubbles: true, pointerId: 1, pointerType: 'mouse' });
|
|
59
75
|
const click = new MouseEvent('click', { bubbles: true });
|
|
@@ -86,64 +102,109 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
86
102
|
return false;
|
|
87
103
|
};
|
|
88
104
|
|
|
89
|
-
const
|
|
105
|
+
const scoreOption = (normalizedText, testid) => {
|
|
106
|
+
// Assign a score to every node so we can pick the most likely match without brittle equality checks.
|
|
107
|
+
if (!normalizedText && !testid) {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
let score = 0;
|
|
111
|
+
const normalizedTestId = (testid ?? '').toLowerCase();
|
|
112
|
+
if (normalizedTestId && TEST_IDS.some((id) => normalizedTestId.includes(id))) {
|
|
113
|
+
score += 1000;
|
|
114
|
+
}
|
|
115
|
+
if (normalizedText && normalizedTarget) {
|
|
116
|
+
if (normalizedText === normalizedTarget) {
|
|
117
|
+
score += 500;
|
|
118
|
+
} else if (normalizedText.startsWith(normalizedTarget)) {
|
|
119
|
+
score += 420;
|
|
120
|
+
} else if (normalizedText.includes(normalizedTarget)) {
|
|
121
|
+
score += 380;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const token of normalizedTokens) {
|
|
125
|
+
// Reward partial matches to the expanded label/token set.
|
|
126
|
+
if (token && normalizedText.includes(token)) {
|
|
127
|
+
const tokenWeight = Math.min(120, Math.max(10, token.length * 4));
|
|
128
|
+
score += tokenWeight;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (targetWords.length > 1) {
|
|
132
|
+
let missing = 0;
|
|
133
|
+
for (const word of targetWords) {
|
|
134
|
+
if (!normalizedText.includes(word)) {
|
|
135
|
+
missing += 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
score -= missing * 12;
|
|
139
|
+
}
|
|
140
|
+
return Math.max(score, 0);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const findBestOption = () => {
|
|
144
|
+
// Walk through every menu item and keep whichever earns the highest score.
|
|
145
|
+
let bestMatch = null;
|
|
90
146
|
const menus = Array.from(document.querySelectorAll(${menuContainerLiteral}));
|
|
91
147
|
for (const menu of menus) {
|
|
92
148
|
const buttons = Array.from(menu.querySelectorAll(${menuItemLiteral}));
|
|
93
149
|
for (const option of buttons) {
|
|
94
|
-
const testid = (option.getAttribute('data-testid') ?? '').toLowerCase();
|
|
95
150
|
const text = option.textContent ?? '';
|
|
96
151
|
const normalizedText = normalizeText(text);
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (matchesTestId || matchesText) {
|
|
106
|
-
return option;
|
|
152
|
+
const testid = option.getAttribute('data-testid') ?? '';
|
|
153
|
+
const score = scoreOption(normalizedText, testid);
|
|
154
|
+
if (score <= 0) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const label = getOptionLabel(option);
|
|
158
|
+
if (!bestMatch || score > bestMatch.score) {
|
|
159
|
+
bestMatch = { node: option, label, score };
|
|
107
160
|
}
|
|
108
161
|
}
|
|
109
162
|
}
|
|
110
|
-
return
|
|
163
|
+
return bestMatch;
|
|
111
164
|
};
|
|
112
165
|
|
|
113
|
-
pointerClick();
|
|
114
166
|
return new Promise((resolve) => {
|
|
115
167
|
const start = performance.now();
|
|
116
168
|
const ensureMenuOpen = () => {
|
|
117
169
|
const menuOpen = document.querySelector('[role="menu"], [data-radix-collection-root]');
|
|
118
|
-
if (!menuOpen && performance.now() - lastPointerClick >
|
|
170
|
+
if (!menuOpen && performance.now() - lastPointerClick > REOPEN_INTERVAL_MS) {
|
|
119
171
|
pointerClick();
|
|
120
172
|
}
|
|
121
173
|
};
|
|
122
|
-
|
|
174
|
+
|
|
175
|
+
// Open once and wait a tick before first scan.
|
|
176
|
+
pointerClick();
|
|
177
|
+
const openDelay = () => new Promise((r) => setTimeout(r, INITIAL_WAIT_MS));
|
|
178
|
+
let initialized = false;
|
|
179
|
+
const attempt = async () => {
|
|
180
|
+
if (!initialized) {
|
|
181
|
+
initialized = true;
|
|
182
|
+
await openDelay();
|
|
183
|
+
}
|
|
123
184
|
ensureMenuOpen();
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
if (optionIsSelected(
|
|
127
|
-
resolve({ status: 'already-selected', label:
|
|
185
|
+
const match = findBestOption();
|
|
186
|
+
if (match) {
|
|
187
|
+
if (optionIsSelected(match.node)) {
|
|
188
|
+
resolve({ status: 'already-selected', label: match.label });
|
|
128
189
|
return;
|
|
129
190
|
}
|
|
130
|
-
|
|
131
|
-
resolve({ status: 'switched', label:
|
|
191
|
+
match.node.click();
|
|
192
|
+
resolve({ status: 'switched', label: match.label });
|
|
132
193
|
return;
|
|
133
194
|
}
|
|
134
195
|
if (performance.now() - start > MAX_WAIT_MS) {
|
|
135
196
|
resolve({ status: 'option-not-found' });
|
|
136
197
|
return;
|
|
137
198
|
}
|
|
138
|
-
|
|
139
|
-
pointerClick();
|
|
140
|
-
}
|
|
141
|
-
setTimeout(attempt, CLICK_INTERVAL_MS);
|
|
199
|
+
setTimeout(attempt, REOPEN_INTERVAL_MS / 2);
|
|
142
200
|
};
|
|
143
201
|
attempt();
|
|
144
202
|
});
|
|
145
203
|
})()`;
|
|
146
204
|
}
|
|
205
|
+
export function buildModelMatchersLiteralForTest(targetModel) {
|
|
206
|
+
return buildModelMatchersLiteral(targetModel);
|
|
207
|
+
}
|
|
147
208
|
function buildModelMatchersLiteral(targetModel) {
|
|
148
209
|
const base = targetModel.trim().toLowerCase();
|
|
149
210
|
const labelTokens = new Set();
|
|
@@ -164,6 +225,28 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
164
225
|
push(`chatgpt ${dotless}`, labelTokens);
|
|
165
226
|
push(`gpt ${base}`, labelTokens);
|
|
166
227
|
push(`gpt ${dotless}`, labelTokens);
|
|
228
|
+
// Numeric variations (5.1 ↔ 51 ↔ gpt-5-1)
|
|
229
|
+
if (base.includes('5.1') || base.includes('5-1') || base.includes('51')) {
|
|
230
|
+
push('5.1', labelTokens);
|
|
231
|
+
push('gpt-5.1', labelTokens);
|
|
232
|
+
push('gpt5.1', labelTokens);
|
|
233
|
+
push('gpt-5-1', labelTokens);
|
|
234
|
+
push('gpt5-1', labelTokens);
|
|
235
|
+
push('gpt51', labelTokens);
|
|
236
|
+
push('chatgpt 5.1', labelTokens);
|
|
237
|
+
testIdTokens.add('gpt-5-1');
|
|
238
|
+
testIdTokens.add('gpt5-1');
|
|
239
|
+
testIdTokens.add('gpt51');
|
|
240
|
+
}
|
|
241
|
+
// Pro / research variants
|
|
242
|
+
if (base.includes('pro')) {
|
|
243
|
+
push('proresearch', labelTokens);
|
|
244
|
+
push('research grade', labelTokens);
|
|
245
|
+
push('advanced reasoning', labelTokens);
|
|
246
|
+
testIdTokens.add('gpt-5-pro');
|
|
247
|
+
testIdTokens.add('pro');
|
|
248
|
+
testIdTokens.add('proresearch');
|
|
249
|
+
}
|
|
167
250
|
base
|
|
168
251
|
.split(/\s+/)
|
|
169
252
|
.map((token) => token.trim())
|
|
@@ -175,8 +258,10 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
175
258
|
push(hyphenated, testIdTokens);
|
|
176
259
|
push(collapsed, testIdTokens);
|
|
177
260
|
push(dotless, testIdTokens);
|
|
261
|
+
// data-testid values observed in the ChatGPT picker (e.g., model-switcher-gpt-5-pro)
|
|
178
262
|
push(`model-switcher-${hyphenated}`, testIdTokens);
|
|
179
263
|
push(`model-switcher-${collapsed}`, testIdTokens);
|
|
264
|
+
push(`model-switcher-${dotless}`, testIdTokens);
|
|
180
265
|
if (!labelTokens.size) {
|
|
181
266
|
labelTokens.add(base);
|
|
182
267
|
}
|
|
@@ -188,3 +273,6 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
188
273
|
testIdTokens: Array.from(testIdTokens).filter(Boolean),
|
|
189
274
|
};
|
|
190
275
|
}
|
|
276
|
+
export function buildModelSelectionExpressionForTest(targetModel) {
|
|
277
|
+
return buildModelSelectionExpression(targetModel);
|
|
278
|
+
}
|
|
@@ -6,6 +6,9 @@ export const DEFAULT_BROWSER_CONFIG = {
|
|
|
6
6
|
timeoutMs: 900_000,
|
|
7
7
|
inputTimeoutMs: 30_000,
|
|
8
8
|
cookieSync: true,
|
|
9
|
+
cookieNames: null,
|
|
10
|
+
inlineCookies: null,
|
|
11
|
+
inlineCookiesSource: null,
|
|
9
12
|
headless: false,
|
|
10
13
|
keepBrowser: false,
|
|
11
14
|
hideWindow: false,
|
|
@@ -21,6 +24,9 @@ export function resolveBrowserConfig(config) {
|
|
|
21
24
|
timeoutMs: config?.timeoutMs ?? DEFAULT_BROWSER_CONFIG.timeoutMs,
|
|
22
25
|
inputTimeoutMs: config?.inputTimeoutMs ?? DEFAULT_BROWSER_CONFIG.inputTimeoutMs,
|
|
23
26
|
cookieSync: config?.cookieSync ?? DEFAULT_BROWSER_CONFIG.cookieSync,
|
|
27
|
+
cookieNames: config?.cookieNames ?? DEFAULT_BROWSER_CONFIG.cookieNames,
|
|
28
|
+
inlineCookies: config?.inlineCookies ?? DEFAULT_BROWSER_CONFIG.inlineCookies,
|
|
29
|
+
inlineCookiesSource: config?.inlineCookiesSource ?? DEFAULT_BROWSER_CONFIG.inlineCookiesSource,
|
|
24
30
|
headless: config?.headless ?? DEFAULT_BROWSER_CONFIG.headless,
|
|
25
31
|
keepBrowser: config?.keepBrowser ?? DEFAULT_BROWSER_CONFIG.keepBrowser,
|
|
26
32
|
hideWindow: config?.hideWindow ?? DEFAULT_BROWSER_CONFIG.hideWindow,
|
|
@@ -4,21 +4,18 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { COOKIE_URLS } from './constants.js';
|
|
5
5
|
export class ChromeCookieSyncError extends Error {
|
|
6
6
|
}
|
|
7
|
-
export async function syncCookies(Network, url, profile, logger,
|
|
7
|
+
export async function syncCookies(Network, url, profile, logger, options = {}) {
|
|
8
|
+
const { allowErrors = false, filterNames, inlineCookies } = options;
|
|
8
9
|
try {
|
|
9
|
-
const cookies =
|
|
10
|
+
const cookies = inlineCookies?.length
|
|
11
|
+
? normalizeInlineCookies(inlineCookies, new URL(url).hostname)
|
|
12
|
+
: await readChromeCookies(url, profile, filterNames ?? undefined);
|
|
10
13
|
if (!cookies.length) {
|
|
11
14
|
return 0;
|
|
12
15
|
}
|
|
13
16
|
let applied = 0;
|
|
14
17
|
for (const cookie of cookies) {
|
|
15
|
-
const cookieWithUrl =
|
|
16
|
-
if (!cookieWithUrl.domain || cookieWithUrl.domain === 'localhost') {
|
|
17
|
-
cookieWithUrl.url = url;
|
|
18
|
-
}
|
|
19
|
-
else if (!cookieWithUrl.domain.startsWith('.')) {
|
|
20
|
-
cookieWithUrl.url = `https://${cookieWithUrl.domain}`;
|
|
21
|
-
}
|
|
18
|
+
const cookieWithUrl = attachUrl(cookie, url);
|
|
22
19
|
try {
|
|
23
20
|
const result = await Network.setCookie(cookieWithUrl);
|
|
24
21
|
if (result?.success) {
|
|
@@ -41,10 +38,11 @@ export async function syncCookies(Network, url, profile, logger, allowErrors = f
|
|
|
41
38
|
throw error instanceof ChromeCookieSyncError ? error : new ChromeCookieSyncError(message);
|
|
42
39
|
}
|
|
43
40
|
}
|
|
44
|
-
async function readChromeCookies(url, profile) {
|
|
41
|
+
async function readChromeCookies(url, profile, filterNames) {
|
|
45
42
|
const chromeModule = await loadChromeCookiesModule();
|
|
46
43
|
const urlsToCheck = Array.from(new Set([stripQuery(url), ...COOKIE_URLS]));
|
|
47
44
|
const merged = new Map();
|
|
45
|
+
const allowlist = normalizeCookieNames(filterNames);
|
|
48
46
|
for (const candidateUrl of urlsToCheck) {
|
|
49
47
|
let rawCookies;
|
|
50
48
|
rawCookies = await chromeModule.getCookiesPromised(candidateUrl, 'puppeteer', profile ?? undefined);
|
|
@@ -54,7 +52,7 @@ async function readChromeCookies(url, profile) {
|
|
|
54
52
|
const fallbackHostname = new URL(candidateUrl).hostname;
|
|
55
53
|
for (const cookie of rawCookies) {
|
|
56
54
|
const normalized = normalizeCookie(cookie, fallbackHostname);
|
|
57
|
-
if (!normalized) {
|
|
55
|
+
if (!normalized || (allowlist && !allowlist.has(normalized.name))) {
|
|
58
56
|
continue;
|
|
59
57
|
}
|
|
60
58
|
const key = `${normalized.domain ?? fallbackHostname}:${normalized.name}`;
|
|
@@ -83,6 +81,46 @@ function normalizeCookie(cookie, fallbackHost) {
|
|
|
83
81
|
httpOnly,
|
|
84
82
|
};
|
|
85
83
|
}
|
|
84
|
+
function normalizeInlineCookies(rawCookies, fallbackHost) {
|
|
85
|
+
const merged = new Map();
|
|
86
|
+
for (const cookie of rawCookies) {
|
|
87
|
+
if (!cookie?.name)
|
|
88
|
+
continue;
|
|
89
|
+
const normalized = {
|
|
90
|
+
...cookie,
|
|
91
|
+
name: cookie.name,
|
|
92
|
+
value: cookie.value ?? '',
|
|
93
|
+
domain: cookie.domain ?? fallbackHost,
|
|
94
|
+
path: cookie.path ?? '/',
|
|
95
|
+
expires: normalizeExpiration(cookie.expires),
|
|
96
|
+
secure: cookie.secure ?? true,
|
|
97
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
98
|
+
};
|
|
99
|
+
const key = `${normalized.domain ?? fallbackHost}:${normalized.name}`;
|
|
100
|
+
if (!merged.has(key)) {
|
|
101
|
+
merged.set(key, normalized);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return Array.from(merged.values());
|
|
105
|
+
}
|
|
106
|
+
function normalizeCookieNames(names) {
|
|
107
|
+
if (!names || names.length === 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return new Set(names.map((name) => name.trim()).filter(Boolean));
|
|
111
|
+
}
|
|
112
|
+
function attachUrl(cookie, fallbackUrl) {
|
|
113
|
+
const cookieWithUrl = { ...cookie };
|
|
114
|
+
if (!cookieWithUrl.url) {
|
|
115
|
+
if (!cookieWithUrl.domain || cookieWithUrl.domain === 'localhost') {
|
|
116
|
+
cookieWithUrl.url = fallbackUrl;
|
|
117
|
+
}
|
|
118
|
+
else if (!cookieWithUrl.domain.startsWith('.')) {
|
|
119
|
+
cookieWithUrl.url = `https://${cookieWithUrl.domain}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return cookieWithUrl;
|
|
123
|
+
}
|
|
86
124
|
function stripQuery(url) {
|
|
87
125
|
try {
|
|
88
126
|
const parsed = new URL(url);
|
|
@@ -177,7 +215,7 @@ async function attemptSqliteRebuild() {
|
|
|
177
215
|
}
|
|
178
216
|
attemptedSqliteRebuild = true;
|
|
179
217
|
if (process.env.ORACLE_ALLOW_SQLITE_REBUILD !== '1') {
|
|
180
|
-
console.warn('[oracle] sqlite3 bindings missing. Set ORACLE_ALLOW_SQLITE_REBUILD=1 if you want
|
|
218
|
+
console.warn('[oracle] sqlite3 bindings missing. Set ORACLE_ALLOW_SQLITE_REBUILD=1 if you want oracle to attempt an automatic rebuild, or run `pnpm rebuild chrome-cookies-secure sqlite3 keytar --workspace-root` manually.');
|
|
181
219
|
return false;
|
|
182
220
|
}
|
|
183
221
|
const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
|
|
@@ -64,10 +64,24 @@ export async function runBrowserMode(options) {
|
|
|
64
64
|
await Promise.all(domainEnablers);
|
|
65
65
|
await Network.clearBrowserCookies();
|
|
66
66
|
if (config.cookieSync) {
|
|
67
|
-
|
|
67
|
+
if (!config.inlineCookies) {
|
|
68
|
+
logger('Heads-up: macOS may prompt for your Keychain password to read Chrome cookies; approve it to stay signed in or rerun with --browser-no-cookie-sync / --browser-allow-cookie-errors / --browser-inline-cookies[(-file)]. Inline cookies skip Chrome + Keychain entirely.');
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
logger('Applying inline cookies (skipping Chrome profile read and Keychain prompt)');
|
|
72
|
+
}
|
|
73
|
+
const cookieCount = await syncCookies(Network, config.url, config.chromeProfile, logger, {
|
|
74
|
+
allowErrors: config.allowCookieErrors ?? false,
|
|
75
|
+
filterNames: config.cookieNames ?? undefined,
|
|
76
|
+
inlineCookies: config.inlineCookies ?? undefined,
|
|
77
|
+
});
|
|
68
78
|
logger(cookieCount > 0
|
|
69
|
-
?
|
|
70
|
-
|
|
79
|
+
? config.inlineCookies
|
|
80
|
+
? `Applied ${cookieCount} inline cookies`
|
|
81
|
+
: `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ?? 'Default'}`
|
|
82
|
+
: config.inlineCookies
|
|
83
|
+
? 'No inline cookies applied; continuing without session reuse'
|
|
84
|
+
: 'No Chrome cookies found; continuing without session reuse');
|
|
71
85
|
}
|
|
72
86
|
else {
|
|
73
87
|
logger('Skipping Chrome cookie sync (--browser-no-cookie-sync)');
|
|
@@ -155,7 +169,7 @@ export async function runBrowserMode(options) {
|
|
|
155
169
|
logger(`Chrome window closed before completion: ${normalizedError.message}`);
|
|
156
170
|
logger(normalizedError.stack);
|
|
157
171
|
}
|
|
158
|
-
throw new Error('Chrome window closed before
|
|
172
|
+
throw new Error('Chrome window closed before oracle finished. Please keep it open until completion.', {
|
|
159
173
|
cause: normalizedError,
|
|
160
174
|
});
|
|
161
175
|
}
|
|
@@ -211,7 +225,7 @@ export function formatThinkingLog(startedAt, now, message, locatorSuffix) {
|
|
|
211
225
|
.toString()
|
|
212
226
|
.padStart(3, ' ');
|
|
213
227
|
const statusLabel = message ? ` — ${message}` : '';
|
|
214
|
-
return
|
|
228
|
+
return `${bar} ${pct}% [${elapsedText} / ~10m]${statusLabel}${locatorSuffix}`;
|
|
215
229
|
}
|
|
216
230
|
function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false) {
|
|
217
231
|
let stopped = false;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { readFiles, createFileSections,
|
|
4
|
+
import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS } from '../oracle.js';
|
|
5
5
|
export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
6
6
|
const cwd = deps.cwd ?? process.cwd();
|
|
7
7
|
const readFilesFn = deps.readFilesImpl ?? readFiles;
|
|
8
8
|
const files = await readFilesFn(runOptions.file ?? [], { cwd });
|
|
9
9
|
const basePrompt = (runOptions.prompt ?? '').trim();
|
|
10
10
|
const userPrompt = basePrompt;
|
|
11
|
-
const systemPrompt = runOptions.system?.trim() ||
|
|
11
|
+
const systemPrompt = runOptions.system?.trim() || '';
|
|
12
12
|
const sections = createFileSections(files, cwd);
|
|
13
13
|
const lines = ['[SYSTEM]', systemPrompt, '', '[USER]', userPrompt, ''];
|
|
14
14
|
sections.forEach((section) => {
|
|
@@ -43,7 +43,8 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
43
43
|
sizeBytes: Buffer.byteLength(section.content, 'utf8'),
|
|
44
44
|
}));
|
|
45
45
|
const MAX_BROWSER_ATTACHMENTS = 10;
|
|
46
|
-
|
|
46
|
+
const shouldBundle = !inlineFiles && (runOptions.browserBundleFiles || attachments.length > MAX_BROWSER_ATTACHMENTS);
|
|
47
|
+
if (shouldBundle) {
|
|
47
48
|
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
|
|
48
49
|
const bundlePath = path.join(bundleDir, 'attachments-bundle.txt');
|
|
49
50
|
const bundleLines = [];
|
|
@@ -57,7 +58,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
57
58
|
attachments.length = 0;
|
|
58
59
|
attachments.push({
|
|
59
60
|
path: bundlePath,
|
|
60
|
-
displayPath:
|
|
61
|
+
displayPath: bundlePath,
|
|
61
62
|
sizeBytes: Buffer.byteLength(bundleText, 'utf8'),
|
|
62
63
|
});
|
|
63
64
|
}
|
|
@@ -81,7 +82,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
81
82
|
attachments,
|
|
82
83
|
inlineFileCount,
|
|
83
84
|
tokenEstimateIncludesInlineFiles,
|
|
84
|
-
bundled:
|
|
85
|
+
bundled: shouldBundle && attachments.length === 1 && attachments[0]?.displayPath
|
|
85
86
|
? { originalCount: sections.length, bundlePath: attachments[0].displayPath }
|
|
86
87
|
: null,
|
|
87
88
|
};
|
|
@@ -4,6 +4,11 @@ import { runBrowserMode } from '../browserMode.js';
|
|
|
4
4
|
import { assembleBrowserPrompt } from './prompt.js';
|
|
5
5
|
import { BrowserAutomationError } from '../oracle/errors.js';
|
|
6
6
|
export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log, cliVersion }, deps = {}) {
|
|
7
|
+
if (runOptions.model.startsWith('gemini')) {
|
|
8
|
+
throw new BrowserAutomationError('Gemini models are not available in browser mode. Re-run with --engine api.', {
|
|
9
|
+
stage: 'preflight',
|
|
10
|
+
});
|
|
11
|
+
}
|
|
7
12
|
const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
|
|
8
13
|
const executeBrowser = deps.executeBrowser ?? runBrowserMode;
|
|
9
14
|
const promptArtifacts = await assemblePrompt(runOptions, { cwd });
|
|
@@ -16,14 +21,17 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
16
21
|
const attachmentList = promptArtifacts.attachments.map((attachment) => attachment.displayPath).join(', ');
|
|
17
22
|
log(chalk.dim(`[verbose] Browser attachments: ${attachmentList}`));
|
|
18
23
|
if (promptArtifacts.bundled) {
|
|
19
|
-
log(chalk.yellow(`[browser]
|
|
24
|
+
log(chalk.yellow(`[browser] Bundled ${promptArtifacts.bundled.originalCount} files into ${promptArtifacts.bundled.bundlePath}.`));
|
|
20
25
|
}
|
|
21
26
|
}
|
|
22
27
|
else if (runOptions.file && runOptions.file.length > 0 && runOptions.browserInlineFiles) {
|
|
23
28
|
log(chalk.dim('[verbose] Browser inline file fallback enabled (pasting file contents).'));
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
|
-
const headerLine = `
|
|
31
|
+
const headerLine = `oracle (${cliVersion}) launching browser mode (${runOptions.model}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens`;
|
|
32
|
+
if (promptArtifacts.bundled) {
|
|
33
|
+
log(chalk.yellow(`[browser] Packed ${promptArtifacts.bundled.originalCount} files into ${promptArtifacts.bundled.bundlePath}. If automation fails, you can drag this file into ChatGPT manually.`));
|
|
34
|
+
}
|
|
27
35
|
const automationLogger = ((message) => {
|
|
28
36
|
if (typeof message === 'string') {
|
|
29
37
|
log(message);
|
|
@@ -56,6 +64,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
56
64
|
log(browserResult.answerMarkdown || browserResult.answerText || chalk.dim('(no text output)'));
|
|
57
65
|
log('');
|
|
58
66
|
}
|
|
67
|
+
const answerText = browserResult.answerMarkdown || browserResult.answerText || '';
|
|
59
68
|
const usage = {
|
|
60
69
|
inputTokens: promptArtifacts.estimatedInputTokens,
|
|
61
70
|
outputTokens: browserResult.answerTokens,
|
|
@@ -63,7 +72,8 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
63
72
|
totalTokens: promptArtifacts.estimatedInputTokens + browserResult.answerTokens,
|
|
64
73
|
};
|
|
65
74
|
const tokensDisplay = `${usage.inputTokens}/${usage.outputTokens}/${usage.reasoningTokens}/${usage.totalTokens}`;
|
|
66
|
-
const
|
|
75
|
+
const tokensLabel = runOptions.verbose ? 'tokens (input/output/reasoning/total)' : 'tok(i/o/r/t)';
|
|
76
|
+
const statsParts = [`${runOptions.model}[browser]`, `${tokensLabel}=${tokensDisplay}`];
|
|
67
77
|
if (runOptions.file && runOptions.file.length > 0) {
|
|
68
78
|
statsParts.push(`files=${runOptions.file.length}`);
|
|
69
79
|
}
|
|
@@ -76,5 +86,6 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
76
86
|
chromePort: browserResult.chromePort,
|
|
77
87
|
userDataDir: browserResult.userDataDir,
|
|
78
88
|
},
|
|
89
|
+
answerText,
|
|
79
90
|
};
|
|
80
91
|
}
|
|
@@ -1,16 +1,28 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
1
4
|
import { DEFAULT_MODEL_TARGET, parseDuration } from '../browserMode.js';
|
|
2
5
|
const DEFAULT_BROWSER_TIMEOUT_MS = 900_000;
|
|
3
6
|
const DEFAULT_BROWSER_INPUT_TIMEOUT_MS = 30_000;
|
|
4
7
|
const DEFAULT_CHROME_PROFILE = 'Default';
|
|
5
8
|
const BROWSER_MODEL_LABELS = {
|
|
6
9
|
'gpt-5-pro': 'GPT-5 Pro',
|
|
7
|
-
'gpt-5.1': '
|
|
10
|
+
'gpt-5.1': 'GPT-5.1',
|
|
11
|
+
'gemini-3-pro': 'Gemini 3 Pro',
|
|
8
12
|
};
|
|
9
|
-
export function buildBrowserConfig(options) {
|
|
13
|
+
export async function buildBrowserConfig(options) {
|
|
10
14
|
const desiredModelOverride = options.browserModelLabel?.trim();
|
|
11
15
|
const normalizedOverride = desiredModelOverride?.toLowerCase() ?? '';
|
|
12
16
|
const baseModel = options.model.toLowerCase();
|
|
13
17
|
const shouldUseOverride = normalizedOverride.length > 0 && normalizedOverride !== baseModel;
|
|
18
|
+
const cookieNames = parseCookieNames(options.browserCookieNames ?? process.env.ORACLE_BROWSER_COOKIE_NAMES);
|
|
19
|
+
const inline = await resolveInlineCookies({
|
|
20
|
+
inlineArg: options.browserInlineCookies,
|
|
21
|
+
inlineFileArg: options.browserInlineCookiesFile,
|
|
22
|
+
envPayload: process.env.ORACLE_BROWSER_COOKIES_JSON,
|
|
23
|
+
envFile: process.env.ORACLE_BROWSER_COOKIES_FILE,
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
});
|
|
14
26
|
return {
|
|
15
27
|
chromeProfile: options.browserChromeProfile ?? DEFAULT_CHROME_PROFILE,
|
|
16
28
|
chromePath: options.browserChromePath ?? null,
|
|
@@ -20,6 +32,9 @@ export function buildBrowserConfig(options) {
|
|
|
20
32
|
? parseDuration(options.browserInputTimeout, DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
|
|
21
33
|
: undefined,
|
|
22
34
|
cookieSync: options.browserNoCookieSync ? false : undefined,
|
|
35
|
+
cookieNames,
|
|
36
|
+
inlineCookies: inline?.cookies,
|
|
37
|
+
inlineCookiesSource: inline?.source ?? null,
|
|
23
38
|
headless: options.browserHeadless ? true : undefined,
|
|
24
39
|
keepBrowser: options.browserKeepBrowser ? true : undefined,
|
|
25
40
|
hideWindow: options.browserHideWindow ? true : undefined,
|
|
@@ -42,3 +57,95 @@ export function resolveBrowserModelLabel(input, model) {
|
|
|
42
57
|
}
|
|
43
58
|
return trimmed;
|
|
44
59
|
}
|
|
60
|
+
function parseCookieNames(raw) {
|
|
61
|
+
if (!raw)
|
|
62
|
+
return undefined;
|
|
63
|
+
const names = raw
|
|
64
|
+
.split(',')
|
|
65
|
+
.map((entry) => entry.trim())
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
return names.length ? names : undefined;
|
|
68
|
+
}
|
|
69
|
+
async function resolveInlineCookies({ inlineArg, inlineFileArg, envPayload, envFile, cwd, }) {
|
|
70
|
+
const tryLoad = async (source, allowPathResolution) => {
|
|
71
|
+
if (!source)
|
|
72
|
+
return undefined;
|
|
73
|
+
const trimmed = source.trim();
|
|
74
|
+
if (!trimmed)
|
|
75
|
+
return undefined;
|
|
76
|
+
if (allowPathResolution) {
|
|
77
|
+
const resolved = path.isAbsolute(trimmed) ? trimmed : path.join(cwd, trimmed);
|
|
78
|
+
try {
|
|
79
|
+
const stat = await fs.stat(resolved);
|
|
80
|
+
if (stat.isFile()) {
|
|
81
|
+
const fileContent = await fs.readFile(resolved, 'utf8');
|
|
82
|
+
const parsed = parseInlineCookiesPayload(fileContent);
|
|
83
|
+
if (parsed)
|
|
84
|
+
return parsed;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// not a file; treat as payload below
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return parseInlineCookiesPayload(trimmed);
|
|
92
|
+
};
|
|
93
|
+
const sources = [
|
|
94
|
+
{ value: inlineFileArg, allowPath: true, source: 'inline-file' },
|
|
95
|
+
{ value: inlineArg, allowPath: true, source: 'inline-arg' },
|
|
96
|
+
{ value: envFile, allowPath: true, source: 'env-file' },
|
|
97
|
+
{ value: envPayload, allowPath: false, source: 'env-payload' },
|
|
98
|
+
];
|
|
99
|
+
for (const { value, allowPath, source } of sources) {
|
|
100
|
+
const parsed = await tryLoad(value, allowPath);
|
|
101
|
+
if (parsed)
|
|
102
|
+
return { cookies: parsed, source };
|
|
103
|
+
}
|
|
104
|
+
// fallback: ~/.oracle/cookies.{json,base64}
|
|
105
|
+
const oracleHome = process.env.ORACLE_HOME_DIR ?? path.join(os.homedir(), '.oracle');
|
|
106
|
+
const candidates = ['cookies.json', 'cookies.base64'];
|
|
107
|
+
for (const file of candidates) {
|
|
108
|
+
const fullPath = path.join(oracleHome, file);
|
|
109
|
+
try {
|
|
110
|
+
const stat = await fs.stat(fullPath);
|
|
111
|
+
if (!stat.isFile())
|
|
112
|
+
continue;
|
|
113
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
114
|
+
const parsed = parseInlineCookiesPayload(content);
|
|
115
|
+
if (parsed)
|
|
116
|
+
return { cookies: parsed, source: `home:${file}` };
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// ignore missing/invalid
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
function parseInlineCookiesPayload(raw) {
|
|
125
|
+
if (!raw)
|
|
126
|
+
return undefined;
|
|
127
|
+
const text = raw.trim();
|
|
128
|
+
if (!text)
|
|
129
|
+
return undefined;
|
|
130
|
+
let jsonPayload = text;
|
|
131
|
+
// Attempt base64 decode first; fall back to raw text on failure.
|
|
132
|
+
try {
|
|
133
|
+
const decoded = Buffer.from(text, 'base64').toString('utf8');
|
|
134
|
+
if (decoded.trim().startsWith('[')) {
|
|
135
|
+
jsonPayload = decoded;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// not base64; continue with raw text
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(jsonPayload);
|
|
143
|
+
if (Array.isArray(parsed)) {
|
|
144
|
+
return parsed;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// invalid json; skip silently to keep this hidden flag non-fatal
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function shouldDetachSession({
|
|
2
|
+
// Params kept for future policy tweaks; currently only model/disableDetachEnv matter.
|
|
3
|
+
engine: _engine, model, waitPreference: _waitPreference, disableDetachEnv, }) {
|
|
4
|
+
if (disableDetachEnv)
|
|
5
|
+
return false;
|
|
6
|
+
// Gemini runs must stay inline: forcing detachment can launch the background session runner,
|
|
7
|
+
// which previously led to silent hangs when Gemini picked the browser path. Keep it simple: no detach.
|
|
8
|
+
if (model.startsWith('gemini'))
|
|
9
|
+
return false;
|
|
10
|
+
// For other models, keep legacy behavior (detach if allowed, then reattach when waitPreference=true).
|
|
11
|
+
return true;
|
|
12
|
+
}
|