@steipete/oracle 0.4.5 → 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/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/package.json +14 -14
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
1
|
+
import { mkdtemp, rm, mkdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { resolveBrowserConfig } from './config.js';
|
|
5
|
-
import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToChrome, connectToRemoteChrome, } from './chromeLifecycle.js';
|
|
5
|
+
import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToChrome, connectToRemoteChrome, closeRemoteChromeTarget, } from './chromeLifecycle.js';
|
|
6
6
|
import { syncCookies } from './cookies.js';
|
|
7
7
|
import { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, readAssistantSnapshot, } from './pageActions.js';
|
|
8
8
|
import { uploadAttachmentViaDataTransfer } from './actions/remoteFileTransfer.js';
|
|
9
|
-
import { estimateTokenCount, withRetries } from './utils.js';
|
|
9
|
+
import { estimateTokenCount, withRetries, delay } from './utils.js';
|
|
10
10
|
import { formatElapsed } from '../oracle/format.js';
|
|
11
11
|
import { CHATGPT_URL } from './constants.js';
|
|
12
|
+
import { BrowserAutomationError } from '../oracle/errors.js';
|
|
12
13
|
export { CHATGPT_URL, DEFAULT_MODEL_TARGET } from './constants.js';
|
|
13
14
|
export { parseDuration, delay, normalizeChatgptUrl } from './utils.js';
|
|
14
15
|
export async function runBrowserMode(options) {
|
|
@@ -25,6 +26,30 @@ export async function runBrowserMode(options) {
|
|
|
25
26
|
if (logger.sessionLog === undefined && options.log?.sessionLog) {
|
|
26
27
|
logger.sessionLog = options.log.sessionLog;
|
|
27
28
|
}
|
|
29
|
+
const runtimeHintCb = options.runtimeHintCb;
|
|
30
|
+
let lastTargetId;
|
|
31
|
+
let lastUrl;
|
|
32
|
+
const emitRuntimeHint = async () => {
|
|
33
|
+
if (!runtimeHintCb || !chrome?.port) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const hint = {
|
|
37
|
+
chromePid: chrome.pid,
|
|
38
|
+
chromePort: chrome.port,
|
|
39
|
+
chromeHost,
|
|
40
|
+
chromeTargetId: lastTargetId,
|
|
41
|
+
tabUrl: lastUrl,
|
|
42
|
+
userDataDir,
|
|
43
|
+
controllerPid: process.pid,
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
await runtimeHintCb(hint);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
logger(`Failed to persist runtime hint: ${message}`);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
28
53
|
if (config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === '1') {
|
|
29
54
|
logger(`[browser-mode] config: ${JSON.stringify({
|
|
30
55
|
...config,
|
|
@@ -40,12 +65,31 @@ export async function runBrowserMode(options) {
|
|
|
40
65
|
}
|
|
41
66
|
return runRemoteBrowserMode(promptText, attachments, config, logger, options);
|
|
42
67
|
}
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
const manualLogin = Boolean(config.manualLogin);
|
|
69
|
+
const manualProfileDir = config.manualLoginProfileDir
|
|
70
|
+
? path.resolve(config.manualLoginProfileDir)
|
|
71
|
+
: path.join(os.homedir(), '.oracle', 'browser-profile');
|
|
72
|
+
const userDataDir = manualLogin
|
|
73
|
+
? manualProfileDir
|
|
74
|
+
: await mkdtemp(path.join(await resolveUserDataBaseDir(), 'oracle-browser-'));
|
|
75
|
+
if (manualLogin) {
|
|
76
|
+
await mkdir(userDataDir, { recursive: true });
|
|
77
|
+
logger(`Manual login mode enabled; reusing persistent profile at ${userDataDir}`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
logger(`Created temporary Chrome profile at ${userDataDir}`);
|
|
81
|
+
}
|
|
82
|
+
const effectiveKeepBrowser = config.keepBrowser || manualLogin;
|
|
83
|
+
const reusedChrome = manualLogin ? await maybeReuseRunningChrome(userDataDir, logger) : null;
|
|
84
|
+
const chrome = reusedChrome ??
|
|
85
|
+
(await launchChrome({
|
|
86
|
+
...config,
|
|
87
|
+
remoteChrome: config.remoteChrome,
|
|
88
|
+
}, userDataDir, logger));
|
|
89
|
+
const chromeHost = chrome.host ?? '127.0.0.1';
|
|
46
90
|
let removeTerminationHooks = null;
|
|
47
91
|
try {
|
|
48
|
-
removeTerminationHooks = registerTerminationHooks(chrome, userDataDir,
|
|
92
|
+
removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger);
|
|
49
93
|
}
|
|
50
94
|
catch {
|
|
51
95
|
// ignore failure; cleanup still happens below
|
|
@@ -60,7 +104,16 @@ export async function runBrowserMode(options) {
|
|
|
60
104
|
let stopThinkingMonitor = null;
|
|
61
105
|
let appliedCookies = 0;
|
|
62
106
|
try {
|
|
63
|
-
|
|
107
|
+
try {
|
|
108
|
+
client = await connectToChrome(chrome.port, logger, chromeHost);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const hint = describeDevtoolsFirewallHint(chromeHost, chrome.port);
|
|
112
|
+
if (hint) {
|
|
113
|
+
logger(hint);
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
64
117
|
const disconnectPromise = new Promise((_, reject) => {
|
|
65
118
|
client?.on('disconnect', () => {
|
|
66
119
|
connectionClosedUnexpectedly = true;
|
|
@@ -78,8 +131,11 @@ export async function runBrowserMode(options) {
|
|
|
78
131
|
domainEnablers.push(DOM.enable());
|
|
79
132
|
}
|
|
80
133
|
await Promise.all(domainEnablers);
|
|
81
|
-
|
|
82
|
-
|
|
134
|
+
if (!manualLogin) {
|
|
135
|
+
await Network.clearBrowserCookies();
|
|
136
|
+
}
|
|
137
|
+
const cookieSyncEnabled = config.cookieSync && !manualLogin;
|
|
138
|
+
if (cookieSyncEnabled) {
|
|
83
139
|
if (!config.inlineCookies) {
|
|
84
140
|
logger('Heads-up: macOS may prompt for your Keychain password to read Chrome cookies; use --copy or --render for manual flow.');
|
|
85
141
|
}
|
|
@@ -105,20 +161,57 @@ export async function runBrowserMode(options) {
|
|
|
105
161
|
: 'No Chrome cookies found; continuing without session reuse');
|
|
106
162
|
}
|
|
107
163
|
else {
|
|
108
|
-
logger(
|
|
164
|
+
logger(manualLogin
|
|
165
|
+
? 'Skipping Chrome cookie sync (--browser-manual-login enabled); reuse the opened profile after signing in.'
|
|
166
|
+
: 'Skipping Chrome cookie sync (--browser-no-cookie-sync)');
|
|
109
167
|
}
|
|
110
168
|
const baseUrl = CHATGPT_URL;
|
|
111
169
|
// First load the base ChatGPT homepage to satisfy potential interstitials,
|
|
112
170
|
// then hop to the requested URL if it differs.
|
|
113
171
|
await raceWithDisconnect(navigateToChatGPT(Page, Runtime, baseUrl, logger));
|
|
114
172
|
await raceWithDisconnect(ensureNotBlocked(Runtime, config.headless, logger));
|
|
115
|
-
await raceWithDisconnect(
|
|
173
|
+
await raceWithDisconnect(waitForLogin({ runtime: Runtime, logger, appliedCookies, manualLogin, timeoutMs: config.timeoutMs }));
|
|
116
174
|
if (config.url !== baseUrl) {
|
|
117
175
|
await raceWithDisconnect(navigateToChatGPT(Page, Runtime, config.url, logger));
|
|
118
176
|
await raceWithDisconnect(ensureNotBlocked(Runtime, config.headless, logger));
|
|
119
177
|
}
|
|
120
178
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
121
179
|
logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
|
|
180
|
+
const captureRuntimeSnapshot = async () => {
|
|
181
|
+
try {
|
|
182
|
+
if (client?.Target?.getTargetInfo) {
|
|
183
|
+
const info = await client.Target.getTargetInfo({});
|
|
184
|
+
lastTargetId = info?.targetInfo?.targetId ?? lastTargetId;
|
|
185
|
+
lastUrl = info?.targetInfo?.url ?? lastUrl;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// ignore
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const { result } = await Runtime.evaluate({
|
|
193
|
+
expression: 'location.href',
|
|
194
|
+
returnByValue: true,
|
|
195
|
+
});
|
|
196
|
+
if (typeof result?.value === 'string') {
|
|
197
|
+
lastUrl = result.value;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// ignore
|
|
202
|
+
}
|
|
203
|
+
if (chrome?.port) {
|
|
204
|
+
const suffix = lastTargetId ? ` target=${lastTargetId}` : '';
|
|
205
|
+
if (lastUrl) {
|
|
206
|
+
logger(`[reattach] chrome port=${chrome.port} host=${chromeHost} url=${lastUrl}${suffix}`);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
logger(`[reattach] chrome port=${chrome.port} host=${chromeHost}${suffix}`);
|
|
210
|
+
}
|
|
211
|
+
await emitRuntimeHint();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
await captureRuntimeSnapshot();
|
|
122
215
|
if (config.desiredModel) {
|
|
123
216
|
await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger), {
|
|
124
217
|
retries: 2,
|
|
@@ -223,7 +316,11 @@ export async function runBrowserMode(options) {
|
|
|
223
316
|
answerChars,
|
|
224
317
|
chromePid: chrome.pid,
|
|
225
318
|
chromePort: chrome.port,
|
|
319
|
+
chromeHost,
|
|
226
320
|
userDataDir,
|
|
321
|
+
chromeTargetId: lastTargetId,
|
|
322
|
+
tabUrl: lastUrl,
|
|
323
|
+
controllerPid: process.pid,
|
|
227
324
|
};
|
|
228
325
|
}
|
|
229
326
|
catch (error) {
|
|
@@ -242,9 +339,19 @@ export async function runBrowserMode(options) {
|
|
|
242
339
|
logger(`Chrome window closed before completion: ${normalizedError.message}`);
|
|
243
340
|
logger(normalizedError.stack);
|
|
244
341
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
342
|
+
await emitRuntimeHint();
|
|
343
|
+
throw new BrowserAutomationError('Chrome window closed before oracle finished. Please keep it open until completion.', {
|
|
344
|
+
stage: 'connection-lost',
|
|
345
|
+
runtime: {
|
|
346
|
+
chromePid: chrome.pid,
|
|
347
|
+
chromePort: chrome.port,
|
|
348
|
+
chromeHost,
|
|
349
|
+
userDataDir,
|
|
350
|
+
chromeTargetId: lastTargetId,
|
|
351
|
+
tabUrl: lastUrl,
|
|
352
|
+
controllerPid: process.pid,
|
|
353
|
+
},
|
|
354
|
+
}, normalizedError);
|
|
248
355
|
}
|
|
249
356
|
finally {
|
|
250
357
|
try {
|
|
@@ -256,7 +363,7 @@ export async function runBrowserMode(options) {
|
|
|
256
363
|
// ignore
|
|
257
364
|
}
|
|
258
365
|
removeTerminationHooks?.();
|
|
259
|
-
if (!
|
|
366
|
+
if (!effectiveKeepBrowser) {
|
|
260
367
|
if (!connectionClosedUnexpectedly) {
|
|
261
368
|
try {
|
|
262
369
|
await chrome.kill();
|
|
@@ -276,6 +383,91 @@ export async function runBrowserMode(options) {
|
|
|
276
383
|
}
|
|
277
384
|
}
|
|
278
385
|
}
|
|
386
|
+
async function waitForLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, }) {
|
|
387
|
+
if (!manualLogin) {
|
|
388
|
+
await ensureLoggedIn(runtime, logger, { appliedCookies });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const deadline = Date.now() + Math.min(timeoutMs ?? 1_200_000, 20 * 60_000);
|
|
392
|
+
let lastNotice = 0;
|
|
393
|
+
while (Date.now() < deadline) {
|
|
394
|
+
try {
|
|
395
|
+
await ensureLoggedIn(runtime, logger, { appliedCookies });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
400
|
+
const loginDetected = message?.toLowerCase().includes('login button');
|
|
401
|
+
const sessionMissing = message?.toLowerCase().includes('session not detected');
|
|
402
|
+
if (!loginDetected && !sessionMissing) {
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
const now = Date.now();
|
|
406
|
+
if (now - lastNotice > 5000) {
|
|
407
|
+
logger('Manual login mode: please sign into chatgpt.com in the opened Chrome window; waiting for session to appear...');
|
|
408
|
+
lastNotice = now;
|
|
409
|
+
}
|
|
410
|
+
await delay(1000);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
throw new Error('Manual login mode timed out waiting for ChatGPT session; please sign in and retry.');
|
|
414
|
+
}
|
|
415
|
+
async function maybeReuseRunningChrome(userDataDir, logger) {
|
|
416
|
+
const port = await readDevToolsPort(userDataDir);
|
|
417
|
+
if (!port)
|
|
418
|
+
return null;
|
|
419
|
+
const versionUrl = `http://127.0.0.1:${port}/json/version`;
|
|
420
|
+
try {
|
|
421
|
+
const controller = new AbortController();
|
|
422
|
+
const timeout = setTimeout(() => controller.abort(), 1500);
|
|
423
|
+
const response = await fetch(versionUrl, { signal: controller.signal });
|
|
424
|
+
clearTimeout(timeout);
|
|
425
|
+
if (!response.ok)
|
|
426
|
+
throw new Error(`HTTP ${response.status}`);
|
|
427
|
+
const pidPath = path.join(userDataDir, 'chrome.pid');
|
|
428
|
+
let pid;
|
|
429
|
+
try {
|
|
430
|
+
const rawPid = (await readFile(pidPath, 'utf8')).trim();
|
|
431
|
+
pid = Number.parseInt(rawPid, 10);
|
|
432
|
+
if (Number.isNaN(pid))
|
|
433
|
+
pid = undefined;
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
pid = undefined;
|
|
437
|
+
}
|
|
438
|
+
logger(`Found running Chrome for ${userDataDir}; reusing (DevTools port ${port}${pid ? `, pid ${pid}` : ''})`);
|
|
439
|
+
return {
|
|
440
|
+
port,
|
|
441
|
+
pid,
|
|
442
|
+
kill: async () => { },
|
|
443
|
+
process: undefined,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
448
|
+
logger(`DevToolsActivePort found for ${userDataDir} but unreachable (${message}); launching new Chrome.`);
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function readDevToolsPort(userDataDir) {
|
|
453
|
+
const candidates = [
|
|
454
|
+
path.join(userDataDir, 'DevToolsActivePort'),
|
|
455
|
+
path.join(userDataDir, 'Default', 'DevToolsActivePort'),
|
|
456
|
+
];
|
|
457
|
+
for (const candidate of candidates) {
|
|
458
|
+
try {
|
|
459
|
+
const raw = await readFile(candidate, 'utf8');
|
|
460
|
+
const firstLine = raw.split(/\r?\n/u)[0]?.trim();
|
|
461
|
+
const port = Number.parseInt(firstLine ?? '', 10);
|
|
462
|
+
if (Number.isFinite(port)) {
|
|
463
|
+
return port;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
279
471
|
async function runRemoteBrowserMode(promptText, attachments, config, logger, options) {
|
|
280
472
|
const remoteChromeConfig = config.remoteChrome;
|
|
281
473
|
if (!remoteChromeConfig) {
|
|
@@ -284,6 +476,26 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
284
476
|
const { host, port } = remoteChromeConfig;
|
|
285
477
|
logger(`Connecting to remote Chrome at ${host}:${port}`);
|
|
286
478
|
let client = null;
|
|
479
|
+
let remoteTargetId = null;
|
|
480
|
+
let lastUrl;
|
|
481
|
+
const runtimeHintCb = options.runtimeHintCb;
|
|
482
|
+
const emitRuntimeHint = async () => {
|
|
483
|
+
if (!runtimeHintCb)
|
|
484
|
+
return;
|
|
485
|
+
try {
|
|
486
|
+
await runtimeHintCb({
|
|
487
|
+
chromePort: port,
|
|
488
|
+
chromeHost: host,
|
|
489
|
+
chromeTargetId: remoteTargetId ?? undefined,
|
|
490
|
+
tabUrl: lastUrl,
|
|
491
|
+
controllerPid: process.pid,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
496
|
+
logger(`Failed to persist runtime hint: ${message}`);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
287
499
|
const startedAt = Date.now();
|
|
288
500
|
let answerText = '';
|
|
289
501
|
let answerMarkdown = '';
|
|
@@ -291,7 +503,10 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
291
503
|
let connectionClosedUnexpectedly = false;
|
|
292
504
|
let stopThinkingMonitor = null;
|
|
293
505
|
try {
|
|
294
|
-
|
|
506
|
+
const connection = await connectToRemoteChrome(host, port, logger, config.url);
|
|
507
|
+
client = connection.client;
|
|
508
|
+
remoteTargetId = connection.targetId ?? null;
|
|
509
|
+
await emitRuntimeHint();
|
|
295
510
|
const markConnectionLost = () => {
|
|
296
511
|
connectionClosedUnexpectedly = true;
|
|
297
512
|
};
|
|
@@ -309,6 +524,19 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
309
524
|
await ensureLoggedIn(Runtime, logger, { remoteSession: true });
|
|
310
525
|
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
311
526
|
logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
|
|
527
|
+
try {
|
|
528
|
+
const { result } = await Runtime.evaluate({
|
|
529
|
+
expression: 'location.href',
|
|
530
|
+
returnByValue: true,
|
|
531
|
+
});
|
|
532
|
+
if (typeof result?.value === 'string') {
|
|
533
|
+
lastUrl = result.value;
|
|
534
|
+
}
|
|
535
|
+
await emitRuntimeHint();
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
// ignore
|
|
539
|
+
}
|
|
312
540
|
if (config.desiredModel) {
|
|
313
541
|
await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger), {
|
|
314
542
|
retries: 2,
|
|
@@ -369,7 +597,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
369
597
|
answerChars,
|
|
370
598
|
chromePid: undefined,
|
|
371
599
|
chromePort: port,
|
|
600
|
+
chromeHost: host,
|
|
372
601
|
userDataDir: undefined,
|
|
602
|
+
chromeTargetId: remoteTargetId ?? undefined,
|
|
603
|
+
tabUrl: lastUrl,
|
|
604
|
+
controllerPid: process.pid,
|
|
373
605
|
};
|
|
374
606
|
}
|
|
375
607
|
catch (error) {
|
|
@@ -384,8 +616,15 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
384
616
|
}
|
|
385
617
|
throw normalizedError;
|
|
386
618
|
}
|
|
387
|
-
throw new
|
|
388
|
-
|
|
619
|
+
throw new BrowserAutomationError('Remote Chrome connection lost before Oracle finished.', {
|
|
620
|
+
stage: 'connection-lost',
|
|
621
|
+
runtime: {
|
|
622
|
+
chromeHost: host,
|
|
623
|
+
chromePort: port,
|
|
624
|
+
chromeTargetId: remoteTargetId ?? undefined,
|
|
625
|
+
tabUrl: lastUrl,
|
|
626
|
+
controllerPid: process.pid,
|
|
627
|
+
},
|
|
389
628
|
});
|
|
390
629
|
}
|
|
391
630
|
finally {
|
|
@@ -397,6 +636,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
397
636
|
catch {
|
|
398
637
|
// ignore
|
|
399
638
|
}
|
|
639
|
+
await closeRemoteChromeTarget(host, port, remoteTargetId ?? undefined, logger);
|
|
400
640
|
// Don't kill remote Chrome - it's not ours to manage
|
|
401
641
|
const totalSeconds = (Date.now() - startedAt) / 1000;
|
|
402
642
|
logger(`Remote session complete • ${totalSeconds.toFixed(1)}s total`);
|
|
@@ -429,7 +669,7 @@ function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false)
|
|
|
429
669
|
let lastMessage = null;
|
|
430
670
|
const startedAt = Date.now();
|
|
431
671
|
const interval = setInterval(async () => {
|
|
432
|
-
//
|
|
672
|
+
// stop flag flips asynchronously
|
|
433
673
|
if (stopped || pending) {
|
|
434
674
|
return;
|
|
435
675
|
}
|
|
@@ -460,7 +700,7 @@ function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false)
|
|
|
460
700
|
}, 1500);
|
|
461
701
|
interval.unref?.();
|
|
462
702
|
return () => {
|
|
463
|
-
//
|
|
703
|
+
// multiple callers may race to stop
|
|
464
704
|
if (stopped) {
|
|
465
705
|
return;
|
|
466
706
|
}
|
|
@@ -491,6 +731,46 @@ function sanitizeThinkingText(raw) {
|
|
|
491
731
|
}
|
|
492
732
|
return trimmed;
|
|
493
733
|
}
|
|
734
|
+
function describeDevtoolsFirewallHint(host, port) {
|
|
735
|
+
if (!isWsl())
|
|
736
|
+
return null;
|
|
737
|
+
return [
|
|
738
|
+
`DevTools port ${host}:${port} is blocked from WSL.`,
|
|
739
|
+
'',
|
|
740
|
+
'PowerShell (admin):',
|
|
741
|
+
`New-NetFirewallRule -DisplayName 'Chrome DevTools ${port}' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${port}`,
|
|
742
|
+
"New-NetFirewallRule -DisplayName 'Chrome DevTools (chrome.exe)' -Direction Inbound -Action Allow -Program 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' -Protocol TCP",
|
|
743
|
+
'',
|
|
744
|
+
'Re-run the same oracle command after adding the rule.',
|
|
745
|
+
].join('\n');
|
|
746
|
+
}
|
|
747
|
+
function isWsl() {
|
|
748
|
+
if (process.platform !== 'linux')
|
|
749
|
+
return false;
|
|
750
|
+
if (process.env.WSL_DISTRO_NAME)
|
|
751
|
+
return true;
|
|
752
|
+
return os.release().toLowerCase().includes('microsoft');
|
|
753
|
+
}
|
|
754
|
+
async function resolveUserDataBaseDir() {
|
|
755
|
+
// On WSL, Chrome launched via Windows can choke on UNC paths; prefer a Windows-backed temp folder.
|
|
756
|
+
if (isWsl()) {
|
|
757
|
+
const candidates = [
|
|
758
|
+
'/mnt/c/Users/Public/AppData/Local/Temp',
|
|
759
|
+
'/mnt/c/Temp',
|
|
760
|
+
'/mnt/c/Windows/Temp',
|
|
761
|
+
];
|
|
762
|
+
for (const candidate of candidates) {
|
|
763
|
+
try {
|
|
764
|
+
await mkdir(candidate, { recursive: true });
|
|
765
|
+
return candidate;
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
// try next
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return os.tmpdir();
|
|
773
|
+
}
|
|
494
774
|
function buildThinkingStatusExpression() {
|
|
495
775
|
const selectors = [
|
|
496
776
|
'span.loading-shimmer',
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS, formatFileSection, } from '../oracle.js';
|
|
5
|
+
import { isKnownModel } from '../oracle/modelResolver.js';
|
|
5
6
|
import { buildPromptMarkdown } from '../oracle/promptAssembly.js';
|
|
6
7
|
import { buildAttachmentPlan } from './policies.js';
|
|
7
8
|
export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
@@ -48,7 +49,8 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
48
49
|
});
|
|
49
50
|
}
|
|
50
51
|
const inlineFileCount = attachmentPlan.inlineFileCount;
|
|
51
|
-
const
|
|
52
|
+
const modelConfig = isKnownModel(runOptions.model) ? MODEL_CONFIGS[runOptions.model] : MODEL_CONFIGS['gpt-5.1'];
|
|
53
|
+
const tokenizer = modelConfig.tokenizer;
|
|
52
54
|
const tokenizerUserContent = inlineFileCount > 0 && attachmentPlan.inlineBlock
|
|
53
55
|
? [userPrompt, attachmentPlan.inlineBlock].filter((value) => Boolean(value?.trim())).join('\n\n').trim()
|
|
54
56
|
: userPrompt;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import CDP from 'chrome-remote-interface';
|
|
2
|
+
import { waitForAssistantResponse, captureAssistantMarkdown } from './pageActions.js';
|
|
3
|
+
function pickTarget(targets, runtime) {
|
|
4
|
+
if (!Array.isArray(targets) || targets.length === 0) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
if (runtime.chromeTargetId) {
|
|
8
|
+
const byId = targets.find((t) => t.targetId === runtime.chromeTargetId);
|
|
9
|
+
if (byId)
|
|
10
|
+
return byId;
|
|
11
|
+
}
|
|
12
|
+
if (runtime.tabUrl) {
|
|
13
|
+
const byUrl = targets.find((t) => t.url?.startsWith(runtime.tabUrl)) ||
|
|
14
|
+
targets.find((t) => runtime.tabUrl.startsWith(t.url || ''));
|
|
15
|
+
if (byUrl)
|
|
16
|
+
return byUrl;
|
|
17
|
+
}
|
|
18
|
+
return targets.find((t) => t.type === 'page') ?? targets[0];
|
|
19
|
+
}
|
|
20
|
+
export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
21
|
+
if (!runtime.chromePort) {
|
|
22
|
+
throw new Error('Missing chromePort; cannot reattach.');
|
|
23
|
+
}
|
|
24
|
+
const host = runtime.chromeHost ?? '127.0.0.1';
|
|
25
|
+
const listTargets = deps.listTargets ??
|
|
26
|
+
(async () => {
|
|
27
|
+
const targets = await CDP.List({ host, port: runtime.chromePort });
|
|
28
|
+
return targets;
|
|
29
|
+
});
|
|
30
|
+
const connect = deps.connect ?? ((options) => CDP(options));
|
|
31
|
+
const targetList = (await listTargets());
|
|
32
|
+
const target = pickTarget(targetList, runtime);
|
|
33
|
+
const client = (await connect({
|
|
34
|
+
host,
|
|
35
|
+
port: runtime.chromePort,
|
|
36
|
+
target: target?.targetId,
|
|
37
|
+
}));
|
|
38
|
+
const { Runtime, DOM } = client;
|
|
39
|
+
if (Runtime?.enable) {
|
|
40
|
+
await Runtime.enable();
|
|
41
|
+
}
|
|
42
|
+
if (DOM && typeof DOM.enable === 'function') {
|
|
43
|
+
await DOM.enable();
|
|
44
|
+
}
|
|
45
|
+
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
46
|
+
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
47
|
+
const timeoutMs = config?.timeoutMs ?? 120_000;
|
|
48
|
+
const answer = await waitForResponse(Runtime, timeoutMs, logger);
|
|
49
|
+
const markdown = (await captureMarkdown(Runtime, answer.meta, logger)) ?? answer.text;
|
|
50
|
+
if (client && typeof client.close === 'function') {
|
|
51
|
+
try {
|
|
52
|
+
await client.close();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { answerText: answer.text, answerMarkdown: markdown };
|
|
59
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { formatElapsed } from '../oracle.js';
|
|
3
|
+
import { formatTokenCount } from '../oracle/runUtils.js';
|
|
3
4
|
import { runBrowserMode } from '../browserMode.js';
|
|
4
5
|
import { assembleBrowserPrompt } from './prompt.js';
|
|
5
6
|
import { BrowserAutomationError } from '../oracle/errors.js';
|
|
@@ -46,6 +47,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
46
47
|
if (runOptions.verbose) {
|
|
47
48
|
log(chalk.dim('Chrome automation does not stream output; this may take a minute...'));
|
|
48
49
|
}
|
|
50
|
+
const persistRuntimeHint = deps.persistRuntimeHint ?? (() => { });
|
|
49
51
|
let browserResult;
|
|
50
52
|
try {
|
|
51
53
|
browserResult = await executeBrowser({
|
|
@@ -55,6 +57,9 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
55
57
|
log: automationLogger,
|
|
56
58
|
heartbeatIntervalMs: runOptions.heartbeatIntervalMs,
|
|
57
59
|
verbose: runOptions.verbose,
|
|
60
|
+
runtimeHintCb: async (runtime) => {
|
|
61
|
+
await persistRuntimeHint({ ...runtime, controllerPid: runtime.controllerPid ?? process.pid });
|
|
62
|
+
},
|
|
58
63
|
});
|
|
59
64
|
}
|
|
60
65
|
catch (error) {
|
|
@@ -76,7 +81,14 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
76
81
|
reasoningTokens: 0,
|
|
77
82
|
totalTokens: promptArtifacts.estimatedInputTokens + browserResult.answerTokens,
|
|
78
83
|
};
|
|
79
|
-
const tokensDisplay =
|
|
84
|
+
const tokensDisplay = [
|
|
85
|
+
usage.inputTokens,
|
|
86
|
+
usage.outputTokens,
|
|
87
|
+
usage.reasoningTokens,
|
|
88
|
+
usage.totalTokens,
|
|
89
|
+
]
|
|
90
|
+
.map((value) => formatTokenCount(value))
|
|
91
|
+
.join('/');
|
|
80
92
|
const tokensLabel = runOptions.verbose ? 'tokens (input/output/reasoning/total)' : 'tok(i/o/r/t)';
|
|
81
93
|
const statsParts = [`${runOptions.model}[browser]`, `${tokensLabel}=${tokensDisplay}`];
|
|
82
94
|
if (runOptions.file && runOptions.file.length > 0) {
|
|
@@ -89,7 +101,9 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
89
101
|
runtime: {
|
|
90
102
|
chromePid: browserResult.chromePid,
|
|
91
103
|
chromePort: browserResult.chromePort,
|
|
104
|
+
chromeHost: browserResult.chromeHost,
|
|
92
105
|
userDataDir: browserResult.userDataDir,
|
|
106
|
+
controllerPid: browserResult.controllerPid ?? process.pid,
|
|
93
107
|
},
|
|
94
108
|
answerText,
|
|
95
109
|
};
|
|
@@ -65,7 +65,8 @@ export async function loadWindowsCookies(dbPath, filterNames) {
|
|
|
65
65
|
}
|
|
66
66
|
function decryptCookie(value, aesKey) {
|
|
67
67
|
const prefix = value.slice(0, 3).toString();
|
|
68
|
-
|
|
68
|
+
// Chrome prefixes AES-GCM encrypted cookies with version markers like v10/v11/v20; treat all v** the same.
|
|
69
|
+
if (/^v\d{2}$/u.test(prefix)) {
|
|
69
70
|
const iv = value.slice(3, 15);
|
|
70
71
|
const tag = value.slice(value.length - 16);
|
|
71
72
|
const data = value.slice(15, value.length - 16);
|
|
@@ -35,6 +35,7 @@ export async function buildBrowserConfig(options) {
|
|
|
35
35
|
chromePath: options.browserChromePath ?? null,
|
|
36
36
|
chromeCookiePath: options.browserCookiePath ?? null,
|
|
37
37
|
url,
|
|
38
|
+
debugPort: selectBrowserPort(options),
|
|
38
39
|
timeoutMs: options.browserTimeout ? parseDuration(options.browserTimeout, DEFAULT_BROWSER_TIMEOUT_MS) : undefined,
|
|
39
40
|
inputTimeoutMs: options.browserInputTimeout
|
|
40
41
|
? parseDuration(options.browserInputTimeout, DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
|
|
@@ -45,6 +46,7 @@ export async function buildBrowserConfig(options) {
|
|
|
45
46
|
inlineCookiesSource: inline?.source ?? null,
|
|
46
47
|
headless: undefined, // disable headless; Cloudflare blocks it
|
|
47
48
|
keepBrowser: options.browserKeepBrowser ? true : undefined,
|
|
49
|
+
manualLogin: options.browserManualLogin ? true : undefined,
|
|
48
50
|
hideWindow: options.browserHideWindow ? true : undefined,
|
|
49
51
|
desiredModel: shouldUseOverride ? desiredModelOverride : mapModelToBrowserLabel(options.model),
|
|
50
52
|
debug: options.verbose ? true : undefined,
|
|
@@ -53,6 +55,15 @@ export async function buildBrowserConfig(options) {
|
|
|
53
55
|
remoteChrome,
|
|
54
56
|
};
|
|
55
57
|
}
|
|
58
|
+
function selectBrowserPort(options) {
|
|
59
|
+
const candidate = options.browserPort ?? options.browserDebugPort;
|
|
60
|
+
if (candidate === undefined || candidate === null)
|
|
61
|
+
return null;
|
|
62
|
+
if (!Number.isFinite(candidate) || candidate <= 0 || candidate > 65_535) {
|
|
63
|
+
throw new Error(`Invalid browser port: ${candidate}. Expected a number between 1 and 65535.`);
|
|
64
|
+
}
|
|
65
|
+
return candidate;
|
|
66
|
+
}
|
|
56
67
|
export function mapModelToBrowserLabel(model) {
|
|
57
68
|
return BROWSER_MODEL_LABELS[model] ?? DEFAULT_MODEL_TARGET;
|
|
58
69
|
}
|