@steipete/oracle 0.5.6 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/bin/oracle-cli.js +8 -2
- package/dist/src/browser/actions/assistantResponse.js +65 -6
- package/dist/src/browser/actions/modelSelection.js +22 -0
- package/dist/src/browser/actions/promptComposer.js +67 -3
- package/dist/src/browser/actions/thinkingTime.js +190 -0
- package/dist/src/browser/config.js +1 -0
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +106 -74
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/profileState.js +171 -0
- package/dist/src/browser/prompt.js +63 -19
- package/dist/src/browser/sessionRunner.js +10 -6
- package/dist/src/cli/browserConfig.js +6 -2
- package/dist/src/cli/sessionDisplay.js +8 -1
- package/dist/src/cli/sessionRunner.js +2 -8
- package/dist/src/cli/tui/index.js +1 -0
- package/dist/src/config.js +2 -3
- package/dist/src/oracle/config.js +38 -3
- package/dist/src/oracle/errors.js +3 -3
- package/dist/src/oracle/gemini.js +4 -1
- package/dist/src/oracle/run.js +4 -7
- package/dist/src/oracleHome.js +13 -0
- package/dist/src/remote/server.js +17 -11
- package/dist/src/sessionManager.js +10 -8
- package/dist/src/sessionStore.js +2 -2
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +22 -38
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/vendor/oracle-notifier/README.md +0 -24
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { mkdtemp, rm, mkdir
|
|
1
|
+
import { mkdtemp, rm, mkdir } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import net from 'node:net';
|
|
5
5
|
import { resolveBrowserConfig } from './config.js';
|
|
6
6
|
import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToChrome, connectToRemoteChrome, closeRemoteChromeTarget, } from './chromeLifecycle.js';
|
|
7
7
|
import { syncCookies } from './cookies.js';
|
|
8
|
-
import { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, readAssistantSnapshot, } from './pageActions.js';
|
|
8
|
+
import { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureModelSelection, submitPrompt, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, readAssistantSnapshot, } from './pageActions.js';
|
|
9
9
|
import { uploadAttachmentViaDataTransfer } from './actions/remoteFileTransfer.js';
|
|
10
|
+
import { ensureExtendedThinking } from './actions/thinkingTime.js';
|
|
10
11
|
import { estimateTokenCount, withRetries, delay } from './utils.js';
|
|
11
12
|
import { formatElapsed } from '../oracle/format.js';
|
|
12
13
|
import { CHATGPT_URL } from './constants.js';
|
|
13
14
|
import { BrowserAutomationError } from '../oracle/errors.js';
|
|
15
|
+
import { cleanupStaleProfileState, readChromePid, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from './profileState.js';
|
|
14
16
|
export { CHATGPT_URL, DEFAULT_MODEL_TARGET } from './constants.js';
|
|
15
17
|
export { parseDuration, delay, normalizeChatgptUrl } from './utils.js';
|
|
16
18
|
export async function runBrowserMode(options) {
|
|
@@ -19,6 +21,7 @@ export async function runBrowserMode(options) {
|
|
|
19
21
|
throw new Error('Prompt text is required when using browser mode.');
|
|
20
22
|
}
|
|
21
23
|
const attachments = options.attachments ?? [];
|
|
24
|
+
const fallbackSubmission = options.fallbackSubmission;
|
|
22
25
|
let config = resolveBrowserConfig(options.config);
|
|
23
26
|
const logger = options.log ?? ((_message) => { });
|
|
24
27
|
if (logger.verbose === undefined) {
|
|
@@ -96,6 +99,13 @@ export async function runBrowserMode(options) {
|
|
|
96
99
|
remoteChrome: config.remoteChrome,
|
|
97
100
|
}, userDataDir, logger));
|
|
98
101
|
const chromeHost = chrome.host ?? '127.0.0.1';
|
|
102
|
+
// Persist profile state so future manual-login runs can reuse this Chrome.
|
|
103
|
+
if (manualLogin && chrome.port) {
|
|
104
|
+
await writeDevToolsActivePort(userDataDir, chrome.port);
|
|
105
|
+
if (!reusedChrome && chrome.pid) {
|
|
106
|
+
await writeChromePid(userDataDir, chrome.pid);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
99
109
|
let removeTerminationHooks = null;
|
|
100
110
|
try {
|
|
101
111
|
removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger, {
|
|
@@ -254,20 +264,49 @@ export async function runBrowserMode(options) {
|
|
|
254
264
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
255
265
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
256
266
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
267
|
+
if (config.extendedThinking) {
|
|
268
|
+
await raceWithDisconnect(withRetries(() => ensureExtendedThinking(Runtime, logger), {
|
|
269
|
+
retries: 2,
|
|
270
|
+
delayMs: 300,
|
|
271
|
+
onRetry: (attempt, error) => {
|
|
272
|
+
if (options.verbose) {
|
|
273
|
+
logger(`[retry] Extended thinking attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
const submitOnce = async (prompt, submissionAttachments) => {
|
|
279
|
+
const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
|
|
280
|
+
if (submissionAttachments.length > 0) {
|
|
281
|
+
if (!DOM) {
|
|
282
|
+
throw new Error('Chrome DOM domain unavailable while uploading attachments.');
|
|
283
|
+
}
|
|
284
|
+
for (const attachment of submissionAttachments) {
|
|
285
|
+
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
286
|
+
await uploadAttachmentFile({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
287
|
+
}
|
|
288
|
+
const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
|
|
289
|
+
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
290
|
+
logger('All attachments uploaded');
|
|
261
291
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
292
|
+
await submitPrompt({ runtime: Runtime, input: Input, attachmentNames }, prompt, logger);
|
|
293
|
+
};
|
|
294
|
+
try {
|
|
295
|
+
await raceWithDisconnect(submitOnce(promptText, attachments));
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
const isPromptTooLarge = error instanceof BrowserAutomationError &&
|
|
299
|
+
error.details?.code === 'prompt-too-large';
|
|
300
|
+
if (fallbackSubmission && isPromptTooLarge) {
|
|
301
|
+
logger('[browser] Inline prompt too large; retrying with file uploads.');
|
|
302
|
+
await raceWithDisconnect(clearPromptComposer(Runtime, logger));
|
|
303
|
+
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
304
|
+
await raceWithDisconnect(submitOnce(fallbackSubmission.prompt, fallbackSubmission.attachments));
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
throw error;
|
|
265
308
|
}
|
|
266
|
-
const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
|
|
267
|
-
await raceWithDisconnect(waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger));
|
|
268
|
-
logger('All attachments uploaded');
|
|
269
309
|
}
|
|
270
|
-
await raceWithDisconnect(submitPrompt({ runtime: Runtime, input: Input, attachmentNames }, promptText, logger));
|
|
271
310
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
272
311
|
const answer = await raceWithDisconnect(waitForAssistantResponse(Runtime, config.timeoutMs, logger));
|
|
273
312
|
answerText = answer.text;
|
|
@@ -502,57 +541,21 @@ async function maybeReuseRunningChrome(userDataDir, logger) {
|
|
|
502
541
|
const port = await readDevToolsPort(userDataDir);
|
|
503
542
|
if (!port)
|
|
504
543
|
return null;
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
clearTimeout(timeout);
|
|
511
|
-
if (!response.ok)
|
|
512
|
-
throw new Error(`HTTP ${response.status}`);
|
|
513
|
-
const pidPath = path.join(userDataDir, 'chrome.pid');
|
|
514
|
-
let pid;
|
|
515
|
-
try {
|
|
516
|
-
const rawPid = (await readFile(pidPath, 'utf8')).trim();
|
|
517
|
-
pid = Number.parseInt(rawPid, 10);
|
|
518
|
-
if (Number.isNaN(pid))
|
|
519
|
-
pid = undefined;
|
|
520
|
-
}
|
|
521
|
-
catch {
|
|
522
|
-
pid = undefined;
|
|
523
|
-
}
|
|
524
|
-
logger(`Found running Chrome for ${userDataDir}; reusing (DevTools port ${port}${pid ? `, pid ${pid}` : ''})`);
|
|
525
|
-
return {
|
|
526
|
-
port,
|
|
527
|
-
pid,
|
|
528
|
-
kill: async () => { },
|
|
529
|
-
process: undefined,
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
catch (error) {
|
|
533
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
534
|
-
logger(`DevToolsActivePort found for ${userDataDir} but unreachable (${message}); launching new Chrome.`);
|
|
544
|
+
const probe = await verifyDevToolsReachable({ port });
|
|
545
|
+
if (!probe.ok) {
|
|
546
|
+
logger(`DevToolsActivePort found for ${userDataDir} but unreachable (${probe.error}); launching new Chrome.`);
|
|
547
|
+
// Safe cleanup: remove stale DevToolsActivePort; only remove lock files if this was an Oracle-owned pid that died.
|
|
548
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: 'if_oracle_pid_dead' });
|
|
535
549
|
return null;
|
|
536
550
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const raw = await readFile(candidate, 'utf8');
|
|
546
|
-
const firstLine = raw.split(/\r?\n/u)[0]?.trim();
|
|
547
|
-
const port = Number.parseInt(firstLine ?? '', 10);
|
|
548
|
-
if (Number.isFinite(port)) {
|
|
549
|
-
return port;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
catch {
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
return null;
|
|
551
|
+
const pid = await readChromePid(userDataDir);
|
|
552
|
+
logger(`Found running Chrome for ${userDataDir}; reusing (DevTools port ${port}${pid ? `, pid ${pid}` : ''})`);
|
|
553
|
+
return {
|
|
554
|
+
port,
|
|
555
|
+
pid: pid ?? undefined,
|
|
556
|
+
kill: async () => { },
|
|
557
|
+
process: undefined,
|
|
558
|
+
};
|
|
556
559
|
}
|
|
557
560
|
async function runRemoteBrowserMode(promptText, attachments, config, logger, options) {
|
|
558
561
|
const remoteChromeConfig = config.remoteChrome;
|
|
@@ -636,21 +639,50 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
636
639
|
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
637
640
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
638
641
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
642
|
+
if (config.extendedThinking) {
|
|
643
|
+
await withRetries(() => ensureExtendedThinking(Runtime, logger), {
|
|
644
|
+
retries: 2,
|
|
645
|
+
delayMs: 300,
|
|
646
|
+
onRetry: (attempt, error) => {
|
|
647
|
+
if (options.verbose) {
|
|
648
|
+
logger(`[retry] Extended thinking attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
const submitOnce = async (prompt, submissionAttachments) => {
|
|
654
|
+
const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
|
|
655
|
+
if (submissionAttachments.length > 0) {
|
|
656
|
+
if (!DOM) {
|
|
657
|
+
throw new Error('Chrome DOM domain unavailable while uploading attachments.');
|
|
658
|
+
}
|
|
659
|
+
// Use remote file transfer for remote Chrome (reads local files and injects via CDP)
|
|
660
|
+
for (const attachment of submissionAttachments) {
|
|
661
|
+
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
662
|
+
await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
663
|
+
}
|
|
664
|
+
const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
|
|
665
|
+
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
666
|
+
logger('All attachments uploaded');
|
|
643
667
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
668
|
+
await submitPrompt({ runtime: Runtime, input: Input, attachmentNames }, prompt, logger);
|
|
669
|
+
};
|
|
670
|
+
try {
|
|
671
|
+
await submitOnce(promptText, attachments);
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
const isPromptTooLarge = error instanceof BrowserAutomationError &&
|
|
675
|
+
error.details?.code === 'prompt-too-large';
|
|
676
|
+
if (options.fallbackSubmission && isPromptTooLarge) {
|
|
677
|
+
logger('[browser] Inline prompt too large; retrying with file uploads.');
|
|
678
|
+
await clearPromptComposer(Runtime, logger);
|
|
679
|
+
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
680
|
+
await submitOnce(options.fallbackSubmission.prompt, options.fallbackSubmission.attachments);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
throw error;
|
|
648
684
|
}
|
|
649
|
-
const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
|
|
650
|
-
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
651
|
-
logger('All attachments uploaded');
|
|
652
685
|
}
|
|
653
|
-
await submitPrompt({ runtime: Runtime, input: Input, attachmentNames }, promptText, logger);
|
|
654
686
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
655
687
|
const answer = await waitForAssistantResponse(Runtime, config.timeoutMs, logger);
|
|
656
688
|
answerText = answer.text;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady } from './actions/navigation.js';
|
|
2
2
|
export { ensureModelSelection } from './actions/modelSelection.js';
|
|
3
|
-
export { submitPrompt } from './actions/promptComposer.js';
|
|
3
|
+
export { submitPrompt, clearPromptComposer } from './actions/promptComposer.js';
|
|
4
4
|
export { uploadAttachmentFile, waitForAttachmentCompletion } from './actions/attachments.js';
|
|
5
5
|
export { waitForAssistantResponse, readAssistantSnapshot, captureAssistantMarkdown, buildAssistantExtractorForTest, buildConversationDebugExpressionForTest, } from './actions/assistantResponse.js';
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const DEVTOOLS_ACTIVE_PORT_FILENAME = 'DevToolsActivePort';
|
|
6
|
+
const DEVTOOLS_ACTIVE_PORT_RELATIVE_PATHS = [
|
|
7
|
+
DEVTOOLS_ACTIVE_PORT_FILENAME,
|
|
8
|
+
path.join('Default', DEVTOOLS_ACTIVE_PORT_FILENAME),
|
|
9
|
+
];
|
|
10
|
+
const CHROME_PID_FILENAME = 'chrome.pid';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
export function getDevToolsActivePortPaths(userDataDir) {
|
|
13
|
+
return DEVTOOLS_ACTIVE_PORT_RELATIVE_PATHS.map((relative) => path.join(userDataDir, relative));
|
|
14
|
+
}
|
|
15
|
+
export async function readDevToolsPort(userDataDir) {
|
|
16
|
+
for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await readFile(candidate, 'utf8');
|
|
19
|
+
const firstLine = raw.split(/\r?\n/u)[0]?.trim();
|
|
20
|
+
const port = Number.parseInt(firstLine ?? '', 10);
|
|
21
|
+
if (Number.isFinite(port)) {
|
|
22
|
+
return port;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// ignore missing/unreadable candidates
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
export async function writeDevToolsActivePort(userDataDir, port) {
|
|
32
|
+
const contents = `${port}\n/devtools/browser`;
|
|
33
|
+
for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
|
|
34
|
+
try {
|
|
35
|
+
await mkdir(path.dirname(candidate), { recursive: true });
|
|
36
|
+
await writeFile(candidate, contents, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// best effort
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function readChromePid(userDataDir) {
|
|
44
|
+
const pidPath = path.join(userDataDir, CHROME_PID_FILENAME);
|
|
45
|
+
try {
|
|
46
|
+
const raw = (await readFile(pidPath, 'utf8')).trim();
|
|
47
|
+
const pid = Number.parseInt(raw, 10);
|
|
48
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return pid;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function writeChromePid(userDataDir, pid) {
|
|
58
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
59
|
+
return;
|
|
60
|
+
const pidPath = path.join(userDataDir, CHROME_PID_FILENAME);
|
|
61
|
+
try {
|
|
62
|
+
await mkdir(path.dirname(pidPath), { recursive: true });
|
|
63
|
+
await writeFile(pidPath, `${Math.trunc(pid)}\n`, 'utf8');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// best effort
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function isProcessAlive(pid) {
|
|
70
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
71
|
+
return false;
|
|
72
|
+
try {
|
|
73
|
+
process.kill(pid, 0);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// EPERM means "exists but no permission"; treat as alive.
|
|
78
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'EPERM') {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function verifyDevToolsReachable({ port, host = '127.0.0.1', attempts = 3, timeoutMs = 3000, }) {
|
|
85
|
+
const versionUrl = `http://${host}:${port}/json/version`;
|
|
86
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
87
|
+
try {
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
90
|
+
const response = await fetch(versionUrl, { signal: controller.signal });
|
|
91
|
+
clearTimeout(timeout);
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error(`HTTP ${response.status}`);
|
|
94
|
+
}
|
|
95
|
+
return { ok: true };
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (attempt < attempts - 1) {
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
103
|
+
return { ok: false, error: message };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { ok: false, error: 'unreachable' };
|
|
107
|
+
}
|
|
108
|
+
export async function cleanupStaleProfileState(userDataDir, logger, options = {}) {
|
|
109
|
+
for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
|
|
110
|
+
try {
|
|
111
|
+
await rm(candidate, { force: true });
|
|
112
|
+
logger?.(`Removed stale DevToolsActivePort: ${candidate}`);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// ignore cleanup errors
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const lockRemovalMode = options.lockRemovalMode ?? 'never';
|
|
119
|
+
if (lockRemovalMode === 'never') {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const pid = await readChromePid(userDataDir);
|
|
123
|
+
if (!pid) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (isProcessAlive(pid)) {
|
|
127
|
+
logger?.(`Chrome pid ${pid} still alive; skipping profile lock cleanup`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Extra safety: if Chrome is running with this profile (but with a different PID, e.g. user relaunched
|
|
131
|
+
// without remote debugging), never delete lock files.
|
|
132
|
+
if (await isChromeUsingUserDataDir(userDataDir)) {
|
|
133
|
+
logger?.('Detected running Chrome using this profile; skipping profile lock cleanup');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const lockFiles = [
|
|
137
|
+
path.join(userDataDir, 'lockfile'),
|
|
138
|
+
path.join(userDataDir, 'SingletonLock'),
|
|
139
|
+
path.join(userDataDir, 'SingletonSocket'),
|
|
140
|
+
path.join(userDataDir, 'SingletonCookie'),
|
|
141
|
+
];
|
|
142
|
+
for (const lock of lockFiles) {
|
|
143
|
+
await rm(lock, { force: true }).catch(() => undefined);
|
|
144
|
+
}
|
|
145
|
+
logger?.('Cleaned up stale Chrome profile locks');
|
|
146
|
+
}
|
|
147
|
+
async function isChromeUsingUserDataDir(userDataDir) {
|
|
148
|
+
if (process.platform === 'win32') {
|
|
149
|
+
// On Windows, lockfiles are typically held open and removal should fail anyway; avoid expensive process scans.
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const { stdout } = await execFileAsync('ps', ['-ax', '-o', 'command='], { maxBuffer: 10 * 1024 * 1024 });
|
|
154
|
+
const lines = String(stdout ?? '').split('\n');
|
|
155
|
+
const needle = userDataDir;
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
if (!line)
|
|
158
|
+
continue;
|
|
159
|
+
const lower = line.toLowerCase();
|
|
160
|
+
if (!lower.includes('chrome') && !lower.includes('chromium'))
|
|
161
|
+
continue;
|
|
162
|
+
if (line.includes(needle) && lower.includes('user-data-dir')) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// best effort
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
@@ -5,6 +5,7 @@ import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS, format
|
|
|
5
5
|
import { isKnownModel } from '../oracle/modelResolver.js';
|
|
6
6
|
import { buildPromptMarkdown } from '../oracle/promptAssembly.js';
|
|
7
7
|
import { buildAttachmentPlan } from './policies.js';
|
|
8
|
+
const DEFAULT_BROWSER_INLINE_CHAR_BUDGET = 60_000;
|
|
8
9
|
export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
9
10
|
const cwd = deps.cwd ?? process.cwd();
|
|
10
11
|
const readFilesFn = deps.readFilesImpl ?? readFiles;
|
|
@@ -14,22 +15,33 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
14
15
|
const systemPrompt = runOptions.system?.trim() || '';
|
|
15
16
|
const sections = createFileSections(files, cwd);
|
|
16
17
|
const markdown = buildPromptMarkdown(systemPrompt, userPrompt, sections);
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const attachmentsPolicy = runOptions.browserInlineFiles
|
|
19
|
+
? 'never'
|
|
20
|
+
: runOptions.browserAttachments ?? 'auto';
|
|
21
|
+
const bundleRequested = Boolean(runOptions.browserBundleFiles);
|
|
22
|
+
const inlinePlan = buildAttachmentPlan(sections, { inlineFiles: true, bundleRequested });
|
|
23
|
+
const uploadPlan = buildAttachmentPlan(sections, { inlineFiles: false, bundleRequested });
|
|
24
|
+
const baseComposerSections = [];
|
|
19
25
|
if (systemPrompt)
|
|
20
|
-
|
|
26
|
+
baseComposerSections.push(systemPrompt);
|
|
21
27
|
if (userPrompt)
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
28
|
+
baseComposerSections.push(userPrompt);
|
|
29
|
+
const inlineComposerText = [...baseComposerSections, inlinePlan.inlineBlock].filter(Boolean).join('\n\n').trim();
|
|
30
|
+
const selectedPlan = attachmentsPolicy === 'always'
|
|
31
|
+
? uploadPlan
|
|
32
|
+
: attachmentsPolicy === 'never'
|
|
33
|
+
? inlinePlan
|
|
34
|
+
: inlineComposerText.length <= DEFAULT_BROWSER_INLINE_CHAR_BUDGET || sections.length === 0
|
|
35
|
+
? inlinePlan
|
|
36
|
+
: uploadPlan;
|
|
37
|
+
const composerText = (selectedPlan.inlineBlock
|
|
38
|
+
? [...baseComposerSections, selectedPlan.inlineBlock]
|
|
39
|
+
: baseComposerSections)
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join('\n\n')
|
|
42
|
+
.trim();
|
|
43
|
+
const attachments = selectedPlan.attachments.slice();
|
|
44
|
+
const shouldBundle = selectedPlan.shouldBundle;
|
|
33
45
|
let bundleText = null;
|
|
34
46
|
if (shouldBundle) {
|
|
35
47
|
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
|
|
@@ -48,11 +60,11 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
48
60
|
sizeBytes: Buffer.byteLength(bundleText, 'utf8'),
|
|
49
61
|
});
|
|
50
62
|
}
|
|
51
|
-
const inlineFileCount =
|
|
63
|
+
const inlineFileCount = selectedPlan.inlineFileCount;
|
|
52
64
|
const modelConfig = isKnownModel(runOptions.model) ? MODEL_CONFIGS[runOptions.model] : MODEL_CONFIGS['gpt-5.1'];
|
|
53
|
-
const tokenizer = modelConfig.tokenizer;
|
|
54
|
-
const tokenizerUserContent = inlineFileCount > 0 &&
|
|
55
|
-
? [userPrompt,
|
|
65
|
+
const tokenizer = deps.tokenizeImpl ?? modelConfig.tokenizer;
|
|
66
|
+
const tokenizerUserContent = inlineFileCount > 0 && selectedPlan.inlineBlock
|
|
67
|
+
? [userPrompt, selectedPlan.inlineBlock].filter((value) => Boolean(value?.trim())).join('\n\n').trim()
|
|
56
68
|
: userPrompt;
|
|
57
69
|
const tokenizerMessages = [
|
|
58
70
|
systemPrompt ? { role: 'system', content: systemPrompt } : null,
|
|
@@ -61,7 +73,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
61
73
|
let estimatedInputTokens = tokenizer(tokenizerMessages.length > 0
|
|
62
74
|
? tokenizerMessages
|
|
63
75
|
: [{ role: 'user', content: '' }], TOKENIZER_OPTIONS);
|
|
64
|
-
const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(
|
|
76
|
+
const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(selectedPlan.inlineBlock);
|
|
65
77
|
if (!tokenEstimateIncludesInlineFiles && sections.length > 0) {
|
|
66
78
|
const attachmentText = bundleText ??
|
|
67
79
|
sections
|
|
@@ -70,6 +82,35 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
70
82
|
const attachmentTokens = tokenizer([{ role: 'user', content: attachmentText }], TOKENIZER_OPTIONS);
|
|
71
83
|
estimatedInputTokens += attachmentTokens;
|
|
72
84
|
}
|
|
85
|
+
let fallback = null;
|
|
86
|
+
if (attachmentsPolicy === 'auto' && selectedPlan.mode === 'inline' && sections.length > 0) {
|
|
87
|
+
const fallbackComposerText = baseComposerSections.join('\n\n').trim();
|
|
88
|
+
const fallbackAttachments = uploadPlan.attachments.slice();
|
|
89
|
+
let fallbackBundled = null;
|
|
90
|
+
if (uploadPlan.shouldBundle) {
|
|
91
|
+
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
|
|
92
|
+
const bundlePath = path.join(bundleDir, 'attachments-bundle.txt');
|
|
93
|
+
const bundleLines = [];
|
|
94
|
+
sections.forEach((section) => {
|
|
95
|
+
bundleLines.push(formatFileSection(section.displayPath, section.content).trimEnd());
|
|
96
|
+
bundleLines.push('');
|
|
97
|
+
});
|
|
98
|
+
const fallbackBundleText = `${bundleLines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
|
|
99
|
+
await fs.writeFile(bundlePath, fallbackBundleText, 'utf8');
|
|
100
|
+
fallbackAttachments.length = 0;
|
|
101
|
+
fallbackAttachments.push({
|
|
102
|
+
path: bundlePath,
|
|
103
|
+
displayPath: bundlePath,
|
|
104
|
+
sizeBytes: Buffer.byteLength(fallbackBundleText, 'utf8'),
|
|
105
|
+
});
|
|
106
|
+
fallbackBundled = { originalCount: sections.length, bundlePath };
|
|
107
|
+
}
|
|
108
|
+
fallback = {
|
|
109
|
+
composerText: fallbackComposerText,
|
|
110
|
+
attachments: fallbackAttachments,
|
|
111
|
+
bundled: fallbackBundled,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
73
114
|
return {
|
|
74
115
|
markdown,
|
|
75
116
|
composerText,
|
|
@@ -77,6 +118,9 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
77
118
|
attachments,
|
|
78
119
|
inlineFileCount,
|
|
79
120
|
tokenEstimateIncludesInlineFiles,
|
|
121
|
+
attachmentsPolicy,
|
|
122
|
+
attachmentMode: selectedPlan.mode,
|
|
123
|
+
fallback,
|
|
80
124
|
bundled: shouldBundle && attachments.length === 1 && attachments[0]?.displayPath
|
|
81
125
|
? { originalCount: sections.length, bundlePath: attachments[0].displayPath }
|
|
82
126
|
: null,
|
|
@@ -25,8 +25,8 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
25
25
|
log(chalk.yellow(`[browser] Bundled ${promptArtifacts.bundled.originalCount} files into ${promptArtifacts.bundled.bundlePath}.`));
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
else if (runOptions.file && runOptions.file.length > 0 &&
|
|
29
|
-
log(chalk.dim('[verbose] Browser
|
|
28
|
+
else if (runOptions.file && runOptions.file.length > 0 && promptArtifacts.attachmentMode === 'inline') {
|
|
29
|
+
log(chalk.dim('[verbose] Browser will paste file contents inline (no uploads).'));
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
if (promptArtifacts.bundled) {
|
|
@@ -34,11 +34,12 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
34
34
|
}
|
|
35
35
|
const headerLine = `Launching browser mode (${runOptions.model}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens.`;
|
|
36
36
|
const automationLogger = ((message) => {
|
|
37
|
-
if (
|
|
37
|
+
if (typeof message !== 'string')
|
|
38
38
|
return;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const shouldAlwaysPrint = message.startsWith('[browser] ') && /fallback|retry/i.test(message);
|
|
40
|
+
if (!runOptions.verbose && !shouldAlwaysPrint)
|
|
41
|
+
return;
|
|
42
|
+
log(message);
|
|
42
43
|
});
|
|
43
44
|
automationLogger.verbose = Boolean(runOptions.verbose);
|
|
44
45
|
automationLogger.sessionLog = runOptions.verbose ? log : (() => { });
|
|
@@ -53,6 +54,9 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
53
54
|
browserResult = await executeBrowser({
|
|
54
55
|
prompt: promptArtifacts.composerText,
|
|
55
56
|
attachments: promptArtifacts.attachments,
|
|
57
|
+
fallbackSubmission: promptArtifacts.fallback
|
|
58
|
+
? { prompt: promptArtifacts.fallback.composerText, attachments: promptArtifacts.fallback.attachments }
|
|
59
|
+
: undefined,
|
|
56
60
|
config: browserConfig,
|
|
57
61
|
log: automationLogger,
|
|
58
62
|
heartbeatIntervalMs: runOptions.heartbeatIntervalMs,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import os from 'node:os';
|
|
4
3
|
import { CHATGPT_URL, DEFAULT_MODEL_TARGET, normalizeChatgptUrl, parseDuration } from '../browserMode.js';
|
|
4
|
+
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';
|
|
@@ -9,6 +9,9 @@ const BROWSER_MODEL_LABELS = {
|
|
|
9
9
|
'gpt-5-pro': 'GPT-5 Pro',
|
|
10
10
|
'gpt-5.1-pro': 'GPT-5.1 Pro',
|
|
11
11
|
'gpt-5.1': 'GPT-5.1',
|
|
12
|
+
'gpt-5.2': 'GPT-5.2 Thinking',
|
|
13
|
+
'gpt-5.2-instant': 'GPT-5.2 Instant',
|
|
14
|
+
'gpt-5.2-pro': 'GPT-5.2 Pro',
|
|
12
15
|
'gemini-3-pro': 'Gemini 3 Pro',
|
|
13
16
|
};
|
|
14
17
|
export async function buildBrowserConfig(options) {
|
|
@@ -53,6 +56,7 @@ export async function buildBrowserConfig(options) {
|
|
|
53
56
|
// Allow cookie failures by default so runs can continue without Chrome/Keychain secrets.
|
|
54
57
|
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
55
58
|
remoteChrome,
|
|
59
|
+
extendedThinking: options.browserExtendedThinking ? true : undefined,
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
62
|
function selectBrowserPort(options) {
|
|
@@ -155,7 +159,7 @@ async function resolveInlineCookies({ inlineArg, inlineFileArg, envPayload, envF
|
|
|
155
159
|
return { cookies: parsed, source };
|
|
156
160
|
}
|
|
157
161
|
// fallback: ~/.oracle/cookies.{json,base64}
|
|
158
|
-
const oracleHome =
|
|
162
|
+
const oracleHome = getOracleHomeDir();
|
|
159
163
|
const candidates = ['cookies.json', 'cookies.base64'];
|
|
160
164
|
for (const file of candidates) {
|
|
161
165
|
const fullPath = path.join(oracleHome, file);
|
|
@@ -18,7 +18,14 @@ function isProcessAlive(pid) {
|
|
|
18
18
|
return true;
|
|
19
19
|
}
|
|
20
20
|
catch (error) {
|
|
21
|
-
|
|
21
|
+
const code = error instanceof Error ? error.code : undefined;
|
|
22
|
+
if (code === 'ESRCH' || code === 'EINVAL') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (code === 'EPERM') {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
22
29
|
}
|
|
23
30
|
}
|
|
24
31
|
const CLEANUP_TIP = 'Tip: Run "oracle session --clear --hours 24" to prune cached runs (add --all to wipe everything).';
|