@indykish/oracle 0.9.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/LICENSE +21 -0
- package/README.md +215 -0
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +1252 -0
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/scripts/agent-send.js +147 -0
- package/dist/scripts/browser-tools.js +536 -0
- package/dist/scripts/check.js +21 -0
- package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
- package/dist/scripts/docs-list.js +110 -0
- package/dist/scripts/git-policy.js +125 -0
- package/dist/scripts/run-cli.js +14 -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/bridge/connection.js +103 -0
- package/dist/src/bridge/userConfigFile.js +28 -0
- package/dist/src/browser/actions/assistantResponse.js +1067 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
- package/dist/src/browser/actions/attachments.js +1910 -0
- package/dist/src/browser/actions/domEvents.js +19 -0
- package/dist/src/browser/actions/modelSelection.js +485 -0
- package/dist/src/browser/actions/navigation.js +445 -0
- package/dist/src/browser/actions/promptComposer.js +485 -0
- package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
- package/dist/src/browser/actions/thinkingTime.js +206 -0
- package/dist/src/browser/chromeLifecycle.js +344 -0
- package/dist/src/browser/config.js +103 -0
- package/dist/src/browser/constants.js +71 -0
- package/dist/src/browser/cookies.js +191 -0
- package/dist/src/browser/detect.js +164 -0
- package/dist/src/browser/domDebug.js +36 -0
- package/dist/src/browser/index.js +1741 -0
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +5 -0
- package/dist/src/browser/policies.js +43 -0
- package/dist/src/browser/profileState.js +280 -0
- package/dist/src/browser/prompt.js +152 -0
- package/dist/src/browser/promptSummary.js +20 -0
- package/dist/src/browser/reattach.js +186 -0
- package/dist/src/browser/reattachHelpers.js +382 -0
- package/dist/src/browser/sessionRunner.js +119 -0
- package/dist/src/browser/types.js +1 -0
- package/dist/src/browser/utils.js +122 -0
- package/dist/src/browserMode.js +1 -0
- package/dist/src/cli/bridge/claudeConfig.js +54 -0
- package/dist/src/cli/bridge/client.js +73 -0
- package/dist/src/cli/bridge/codexConfig.js +43 -0
- package/dist/src/cli/bridge/doctor.js +107 -0
- package/dist/src/cli/bridge/host.js +259 -0
- package/dist/src/cli/browserConfig.js +278 -0
- package/dist/src/cli/browserDefaults.js +81 -0
- package/dist/src/cli/bundleWarnings.js +9 -0
- package/dist/src/cli/clipboard.js +10 -0
- package/dist/src/cli/detach.js +11 -0
- package/dist/src/cli/dryRun.js +105 -0
- package/dist/src/cli/duplicatePromptGuard.js +14 -0
- package/dist/src/cli/engine.js +41 -0
- package/dist/src/cli/errorUtils.js +9 -0
- package/dist/src/cli/format.js +13 -0
- package/dist/src/cli/help.js +77 -0
- package/dist/src/cli/hiddenAliases.js +22 -0
- package/dist/src/cli/markdownBundle.js +17 -0
- package/dist/src/cli/markdownRenderer.js +97 -0
- package/dist/src/cli/notifier.js +306 -0
- package/dist/src/cli/options.js +281 -0
- package/dist/src/cli/oscUtils.js +2 -0
- package/dist/src/cli/promptRequirement.js +17 -0
- package/dist/src/cli/renderFlags.js +9 -0
- package/dist/src/cli/renderOutput.js +26 -0
- package/dist/src/cli/rootAlias.js +30 -0
- package/dist/src/cli/runOptions.js +78 -0
- package/dist/src/cli/sessionCommand.js +111 -0
- package/dist/src/cli/sessionDisplay.js +567 -0
- package/dist/src/cli/sessionRunner.js +602 -0
- package/dist/src/cli/sessionTable.js +92 -0
- package/dist/src/cli/tagline.js +258 -0
- package/dist/src/cli/tui/index.js +486 -0
- package/dist/src/cli/writeOutputPath.js +21 -0
- package/dist/src/config.js +26 -0
- package/dist/src/gemini-web/client.js +328 -0
- package/dist/src/gemini-web/executor.js +285 -0
- package/dist/src/gemini-web/index.js +1 -0
- package/dist/src/gemini-web/types.js +1 -0
- package/dist/src/heartbeat.js +43 -0
- package/dist/src/mcp/server.js +40 -0
- package/dist/src/mcp/tools/consult.js +290 -0
- package/dist/src/mcp/tools/sessionResources.js +75 -0
- package/dist/src/mcp/tools/sessions.js +105 -0
- package/dist/src/mcp/types.js +22 -0
- package/dist/src/mcp/utils.js +37 -0
- package/dist/src/oracle/background.js +141 -0
- package/dist/src/oracle/claude.js +101 -0
- package/dist/src/oracle/client.js +197 -0
- package/dist/src/oracle/config.js +227 -0
- package/dist/src/oracle/errors.js +132 -0
- package/dist/src/oracle/files.js +378 -0
- package/dist/src/oracle/finishLine.js +32 -0
- package/dist/src/oracle/format.js +30 -0
- package/dist/src/oracle/fsAdapter.js +10 -0
- package/dist/src/oracle/gemini.js +195 -0
- package/dist/src/oracle/logging.js +36 -0
- package/dist/src/oracle/markdown.js +46 -0
- package/dist/src/oracle/modelResolver.js +183 -0
- package/dist/src/oracle/multiModelRunner.js +153 -0
- package/dist/src/oracle/oscProgress.js +24 -0
- package/dist/src/oracle/promptAssembly.js +13 -0
- package/dist/src/oracle/request.js +50 -0
- package/dist/src/oracle/run.js +596 -0
- package/dist/src/oracle/runUtils.js +31 -0
- package/dist/src/oracle/tokenEstimate.js +37 -0
- package/dist/src/oracle/tokenStats.js +39 -0
- package/dist/src/oracle/tokenStringifier.js +24 -0
- package/dist/src/oracle/types.js +1 -0
- package/dist/src/oracle.js +12 -0
- package/dist/src/oracleHome.js +13 -0
- package/dist/src/remote/client.js +129 -0
- package/dist/src/remote/health.js +113 -0
- package/dist/src/remote/remoteServiceConfig.js +31 -0
- package/dist/src/remote/server.js +533 -0
- package/dist/src/remote/types.js +1 -0
- package/dist/src/sessionManager.js +637 -0
- package/dist/src/sessionStore.js +56 -0
- package/dist/src/version.js +39 -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/package.json +115 -0
- package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/vendor/oracle-notifier/README.md +24 -0
- package/vendor/oracle-notifier/build-notifier.sh +93 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR } from '../constants.js';
|
|
2
|
+
import { logDomFailure } from '../domDebug.js';
|
|
3
|
+
import { buildClickDispatcher } from './domEvents.js';
|
|
4
|
+
/**
|
|
5
|
+
* Selects a specific thinking time level in ChatGPT's composer pill menu.
|
|
6
|
+
* @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
|
|
7
|
+
*/
|
|
8
|
+
export async function ensureThinkingTime(Runtime, level, logger) {
|
|
9
|
+
const result = await evaluateThinkingTimeSelection(Runtime, level);
|
|
10
|
+
const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
|
|
11
|
+
switch (result?.status) {
|
|
12
|
+
case 'already-selected':
|
|
13
|
+
logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
|
|
14
|
+
return;
|
|
15
|
+
case 'switched':
|
|
16
|
+
logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
|
|
17
|
+
return;
|
|
18
|
+
case 'chip-not-found': {
|
|
19
|
+
await logDomFailure(Runtime, logger, 'thinking-chip');
|
|
20
|
+
throw new Error('Unable to find the Thinking chip button in the composer area.');
|
|
21
|
+
}
|
|
22
|
+
case 'menu-not-found': {
|
|
23
|
+
await logDomFailure(Runtime, logger, 'thinking-time-menu');
|
|
24
|
+
throw new Error('Unable to find the Thinking time dropdown menu.');
|
|
25
|
+
}
|
|
26
|
+
case 'option-not-found': {
|
|
27
|
+
await logDomFailure(Runtime, logger, `${level}-option`);
|
|
28
|
+
throw new Error(`Unable to find the ${capitalizedLevel} option in the Thinking time menu.`);
|
|
29
|
+
}
|
|
30
|
+
default: {
|
|
31
|
+
await logDomFailure(Runtime, logger, 'thinking-time-unknown');
|
|
32
|
+
throw new Error(`Unknown error selecting ${capitalizedLevel} thinking time.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Best-effort selection of a thinking time level in ChatGPT's composer pill menu.
|
|
38
|
+
* Safe by default: if the pill/menu/option isn't present, we continue without throwing.
|
|
39
|
+
* @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
|
|
40
|
+
*/
|
|
41
|
+
export async function ensureThinkingTimeIfAvailable(Runtime, level, logger) {
|
|
42
|
+
try {
|
|
43
|
+
const result = await evaluateThinkingTimeSelection(Runtime, level);
|
|
44
|
+
const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
|
|
45
|
+
switch (result?.status) {
|
|
46
|
+
case 'already-selected':
|
|
47
|
+
logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
|
|
48
|
+
return true;
|
|
49
|
+
case 'switched':
|
|
50
|
+
logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
|
|
51
|
+
return true;
|
|
52
|
+
case 'chip-not-found':
|
|
53
|
+
case 'menu-not-found':
|
|
54
|
+
case 'option-not-found':
|
|
55
|
+
if (logger.verbose) {
|
|
56
|
+
logger(`Thinking time: ${result.status.replaceAll('-', ' ')}; continuing with default.`);
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
default:
|
|
60
|
+
if (logger.verbose) {
|
|
61
|
+
logger('Thinking time: unknown outcome; continuing with default.');
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
68
|
+
if (logger.verbose) {
|
|
69
|
+
logger(`Thinking time selection failed (${message}); continuing with default.`);
|
|
70
|
+
await logDomFailure(Runtime, logger, 'thinking-time');
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function evaluateThinkingTimeSelection(Runtime, level) {
|
|
76
|
+
const outcome = await Runtime.evaluate({
|
|
77
|
+
expression: buildThinkingTimeExpression(level),
|
|
78
|
+
awaitPromise: true,
|
|
79
|
+
returnByValue: true,
|
|
80
|
+
});
|
|
81
|
+
return outcome.result?.value;
|
|
82
|
+
}
|
|
83
|
+
function buildThinkingTimeExpression(level) {
|
|
84
|
+
const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
|
|
85
|
+
const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
|
|
86
|
+
const targetLevelLiteral = JSON.stringify(level.toLowerCase());
|
|
87
|
+
return `(async () => {
|
|
88
|
+
${buildClickDispatcher()}
|
|
89
|
+
|
|
90
|
+
const MENU_CONTAINER_SELECTOR = ${menuContainerLiteral};
|
|
91
|
+
const MENU_ITEM_SELECTOR = ${menuItemLiteral};
|
|
92
|
+
const TARGET_LEVEL = ${targetLevelLiteral};
|
|
93
|
+
|
|
94
|
+
const CHIP_SELECTORS = [
|
|
95
|
+
'[data-testid="composer-footer-actions"] button[aria-haspopup="menu"]',
|
|
96
|
+
'button.__composer-pill[aria-haspopup="menu"]',
|
|
97
|
+
'.__composer-pill-composite button[aria-haspopup="menu"]',
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const INITIAL_WAIT_MS = 150;
|
|
101
|
+
const MAX_WAIT_MS = 10000;
|
|
102
|
+
|
|
103
|
+
const normalize = (value) => (value || '')
|
|
104
|
+
.toLowerCase()
|
|
105
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
106
|
+
.replace(/\\s+/g, ' ')
|
|
107
|
+
.trim();
|
|
108
|
+
|
|
109
|
+
const findThinkingChip = () => {
|
|
110
|
+
for (const selector of CHIP_SELECTORS) {
|
|
111
|
+
const buttons = document.querySelectorAll(selector);
|
|
112
|
+
for (const btn of buttons) {
|
|
113
|
+
// Skip toggle buttons (no haspopup) - only click dropdown triggers to avoid disabling Pro mode
|
|
114
|
+
if (btn.getAttribute?.('aria-haspopup') !== 'menu') continue;
|
|
115
|
+
const aria = normalize(btn.getAttribute?.('aria-label') ?? '');
|
|
116
|
+
const text = normalize(btn.textContent ?? '');
|
|
117
|
+
if (aria.includes('thinking') || text.includes('thinking')) {
|
|
118
|
+
return btn;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// In some cases the pill is labeled "Pro".
|
|
122
|
+
if (aria.includes('pro') || text.includes('pro')) {
|
|
123
|
+
return btn;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const chip = findThinkingChip();
|
|
131
|
+
if (!chip) {
|
|
132
|
+
return { status: 'chip-not-found' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
dispatchClickSequence(chip);
|
|
136
|
+
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
const start = performance.now();
|
|
139
|
+
|
|
140
|
+
const findMenu = () => {
|
|
141
|
+
const menus = document.querySelectorAll(MENU_CONTAINER_SELECTOR + ', [role="group"]');
|
|
142
|
+
for (const menu of menus) {
|
|
143
|
+
const label = menu.querySelector?.('.__menu-label, [class*="menu-label"]');
|
|
144
|
+
if (normalize(label?.textContent ?? '').includes('thinking time')) {
|
|
145
|
+
return menu;
|
|
146
|
+
}
|
|
147
|
+
const text = normalize(menu.textContent ?? '');
|
|
148
|
+
if (text.includes('standard') && text.includes('extended')) {
|
|
149
|
+
return menu;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const findTargetOption = (menu) => {
|
|
156
|
+
const items = menu.querySelectorAll(MENU_ITEM_SELECTOR);
|
|
157
|
+
for (const item of items) {
|
|
158
|
+
const text = normalize(item.textContent ?? '');
|
|
159
|
+
if (text.includes(TARGET_LEVEL)) {
|
|
160
|
+
return item;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const optionIsSelected = (node) => {
|
|
167
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
168
|
+
const ariaChecked = node.getAttribute('aria-checked');
|
|
169
|
+
const dataState = (node.getAttribute('data-state') || '').toLowerCase();
|
|
170
|
+
if (ariaChecked === 'true') return true;
|
|
171
|
+
if (dataState === 'checked' || dataState === 'selected' || dataState === 'on') return true;
|
|
172
|
+
return false;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const attempt = () => {
|
|
176
|
+
const menu = findMenu();
|
|
177
|
+
if (!menu) {
|
|
178
|
+
if (performance.now() - start > MAX_WAIT_MS) {
|
|
179
|
+
resolve({ status: 'menu-not-found' });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
setTimeout(attempt, 100);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const targetOption = findTargetOption(menu);
|
|
187
|
+
if (!targetOption) {
|
|
188
|
+
resolve({ status: 'option-not-found' });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const alreadySelected =
|
|
193
|
+
optionIsSelected(targetOption) ||
|
|
194
|
+
optionIsSelected(targetOption.querySelector?.('[aria-checked="true"], [data-state="checked"], [data-state="selected"]'));
|
|
195
|
+
const label = targetOption.textContent?.trim?.() || null;
|
|
196
|
+
dispatchClickSequence(targetOption);
|
|
197
|
+
resolve({ status: alreadySelected ? 'already-selected' : 'switched', label });
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
setTimeout(attempt, INITIAL_WAIT_MS);
|
|
201
|
+
});
|
|
202
|
+
})()`;
|
|
203
|
+
}
|
|
204
|
+
export function buildThinkingTimeExpressionForTest(level = 'extended') {
|
|
205
|
+
return buildThinkingTimeExpression(level);
|
|
206
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { rm } from 'node:fs/promises';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import CDP from 'chrome-remote-interface';
|
|
8
|
+
import { launch, Launcher } from 'chrome-launcher';
|
|
9
|
+
import { cleanupStaleProfileState } from './profileState.js';
|
|
10
|
+
import { delay } from './utils.js';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
export async function launchChrome(config, userDataDir, logger) {
|
|
13
|
+
const connectHost = resolveRemoteDebugHost();
|
|
14
|
+
const debugBindAddress = connectHost && connectHost !== '127.0.0.1' ? '0.0.0.0' : connectHost;
|
|
15
|
+
const debugPort = config.debugPort ?? parseDebugPortEnv();
|
|
16
|
+
const chromeFlags = buildChromeFlags(config.headless ?? false, debugBindAddress);
|
|
17
|
+
const usePatchedLauncher = Boolean(connectHost && connectHost !== '127.0.0.1');
|
|
18
|
+
const launcher = usePatchedLauncher
|
|
19
|
+
? await launchWithCustomHost({
|
|
20
|
+
chromeFlags,
|
|
21
|
+
chromePath: config.chromePath ?? undefined,
|
|
22
|
+
userDataDir,
|
|
23
|
+
host: connectHost ?? '127.0.0.1',
|
|
24
|
+
requestedPort: debugPort ?? undefined,
|
|
25
|
+
})
|
|
26
|
+
: await launch({
|
|
27
|
+
chromePath: config.chromePath ?? undefined,
|
|
28
|
+
chromeFlags,
|
|
29
|
+
userDataDir,
|
|
30
|
+
handleSIGINT: false,
|
|
31
|
+
port: debugPort ?? undefined,
|
|
32
|
+
});
|
|
33
|
+
const pidLabel = typeof launcher.pid === 'number' ? ` (pid ${launcher.pid})` : '';
|
|
34
|
+
const hostLabel = connectHost ? ` on ${connectHost}` : '';
|
|
35
|
+
logger(`Launched Chrome${pidLabel} on port ${launcher.port}${hostLabel}`);
|
|
36
|
+
return Object.assign(launcher, { host: connectHost ?? '127.0.0.1' });
|
|
37
|
+
}
|
|
38
|
+
export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logger, opts) {
|
|
39
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
|
|
40
|
+
let handling;
|
|
41
|
+
const handleSignal = (signal) => {
|
|
42
|
+
if (handling) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
handling = true;
|
|
46
|
+
const inFlight = opts?.isInFlight?.() ?? false;
|
|
47
|
+
const leaveRunning = keepBrowser || inFlight;
|
|
48
|
+
if (leaveRunning) {
|
|
49
|
+
logger(`Received ${signal}; leaving Chrome running${inFlight ? ' (assistant response pending)' : ''}`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
logger(`Received ${signal}; terminating Chrome process`);
|
|
53
|
+
}
|
|
54
|
+
void (async () => {
|
|
55
|
+
if (leaveRunning) {
|
|
56
|
+
// Ensure reattach hints are written before we exit.
|
|
57
|
+
await opts?.emitRuntimeHint?.().catch(() => undefined);
|
|
58
|
+
if (inFlight) {
|
|
59
|
+
logger('Session still in flight; reattach with "oracle session <slug>" to continue.');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
try {
|
|
64
|
+
await chrome.kill();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// ignore kill failures
|
|
68
|
+
}
|
|
69
|
+
if (opts?.preserveUserDataDir) {
|
|
70
|
+
// Preserve the profile directory (manual login), but clear reattach hints so we don't
|
|
71
|
+
// try to reuse a dead DevTools port on the next run.
|
|
72
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: 'never' }).catch(() => undefined);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
})().finally(() => {
|
|
79
|
+
const exitCode = signal === 'SIGINT' ? 130 : 1;
|
|
80
|
+
// Vitest treats any `process.exit()` call as an unhandled failure, even if mocked.
|
|
81
|
+
// Keep production behavior (hard-exit on signals) while letting tests observe state changes.
|
|
82
|
+
process.exitCode = exitCode;
|
|
83
|
+
const isTestRun = process.env.VITEST === '1' || process.env.NODE_ENV === 'test';
|
|
84
|
+
if (!isTestRun) {
|
|
85
|
+
process.exit(exitCode);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
for (const signal of signals) {
|
|
90
|
+
process.on(signal, handleSignal);
|
|
91
|
+
}
|
|
92
|
+
return () => {
|
|
93
|
+
for (const signal of signals) {
|
|
94
|
+
process.removeListener(signal, handleSignal);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export async function hideChromeWindow(chrome, logger) {
|
|
99
|
+
if (process.platform !== 'darwin') {
|
|
100
|
+
logger('Window hiding is only supported on macOS');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!chrome.pid) {
|
|
104
|
+
logger('Unable to hide window: missing Chrome PID');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const script = `tell application "System Events"
|
|
108
|
+
try
|
|
109
|
+
set visible of (first process whose unix id is ${chrome.pid}) to false
|
|
110
|
+
end try
|
|
111
|
+
end tell`;
|
|
112
|
+
try {
|
|
113
|
+
await execFileAsync('osascript', ['-e', script]);
|
|
114
|
+
logger('Chrome window hidden (Cmd-H)');
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
118
|
+
logger(`Failed to hide Chrome window: ${message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export async function connectToChrome(port, logger, host) {
|
|
122
|
+
const client = await CDP({ port, host });
|
|
123
|
+
logger('Connected to Chrome DevTools protocol');
|
|
124
|
+
return client;
|
|
125
|
+
}
|
|
126
|
+
export async function connectToRemoteChrome(host, port, logger, targetUrl) {
|
|
127
|
+
if (targetUrl) {
|
|
128
|
+
const targetConnection = await connectToNewTarget(host, port, targetUrl, logger, {
|
|
129
|
+
opened: () => `Opened dedicated remote Chrome tab targeting ${targetUrl}`,
|
|
130
|
+
openFailed: (message) => `Failed to open dedicated remote Chrome tab (${message}); falling back to first target.`,
|
|
131
|
+
attachFailed: (targetId, message) => `Failed to attach to dedicated remote Chrome tab ${targetId} (${message}); falling back to first target.`,
|
|
132
|
+
closeFailed: (targetId, message) => `Failed to close unused remote Chrome tab ${targetId}: ${message}`,
|
|
133
|
+
});
|
|
134
|
+
if (targetConnection) {
|
|
135
|
+
return { client: targetConnection.client, targetId: targetConnection.targetId };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const fallbackClient = await CDP({ host, port });
|
|
139
|
+
logger(`Connected to remote Chrome DevTools protocol at ${host}:${port}`);
|
|
140
|
+
return { client: fallbackClient };
|
|
141
|
+
}
|
|
142
|
+
export async function closeRemoteChromeTarget(host, port, targetId, logger) {
|
|
143
|
+
if (!targetId) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
await CDP.Close({ host, port, id: targetId });
|
|
148
|
+
if (logger.verbose) {
|
|
149
|
+
logger(`Closed remote Chrome tab ${targetId}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
154
|
+
logger(`Failed to close remote Chrome tab ${targetId}: ${message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function connectToNewTarget(host, port, url, logger, messages) {
|
|
158
|
+
try {
|
|
159
|
+
const target = await CDP.New({ host, port, url });
|
|
160
|
+
try {
|
|
161
|
+
const client = await CDP({ host, port, target: target.id });
|
|
162
|
+
if (messages.opened) {
|
|
163
|
+
logger(messages.opened(target.id));
|
|
164
|
+
}
|
|
165
|
+
return { client, targetId: target.id };
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
169
|
+
logger(messages.attachFailed(target.id, message));
|
|
170
|
+
try {
|
|
171
|
+
await CDP.Close({ host, port, id: target.id });
|
|
172
|
+
}
|
|
173
|
+
catch (closeError) {
|
|
174
|
+
const closeMessage = closeError instanceof Error ? closeError.message : String(closeError);
|
|
175
|
+
logger(messages.closeFailed(target.id, closeMessage));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
181
|
+
logger(messages.openFailed(message));
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
export async function connectWithNewTab(port, logger, initialUrl, host, options) {
|
|
186
|
+
const effectiveHost = host ?? '127.0.0.1';
|
|
187
|
+
const url = initialUrl ?? 'about:blank';
|
|
188
|
+
const fallbackToDefault = options?.fallbackToDefault ?? true;
|
|
189
|
+
const retries = Math.max(0, options?.retries ?? 0);
|
|
190
|
+
const retryDelayMs = Math.max(0, options?.retryDelayMs ?? 250);
|
|
191
|
+
const fallbackLabel = fallbackToDefault ? 'falling back to default target.' : 'strict mode: not falling back.';
|
|
192
|
+
let attempt = 0;
|
|
193
|
+
while (attempt <= retries) {
|
|
194
|
+
const targetConnection = await connectToNewTarget(effectiveHost, port, url, logger, {
|
|
195
|
+
opened: (targetId) => `Opened isolated browser tab (target=${targetId})`,
|
|
196
|
+
openFailed: (message) => `Failed to open isolated browser tab (${message}); ${fallbackLabel}`,
|
|
197
|
+
attachFailed: (targetId, message) => `Failed to attach to isolated browser tab ${targetId} (${message}); ${fallbackLabel}`,
|
|
198
|
+
closeFailed: (targetId, message) => `Failed to close unused browser tab ${targetId}: ${message}`,
|
|
199
|
+
});
|
|
200
|
+
if (targetConnection) {
|
|
201
|
+
return targetConnection;
|
|
202
|
+
}
|
|
203
|
+
if (attempt >= retries) {
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
attempt += 1;
|
|
207
|
+
await delay(retryDelayMs * attempt);
|
|
208
|
+
}
|
|
209
|
+
if (!fallbackToDefault) {
|
|
210
|
+
throw new Error('Failed to open isolated browser tab; refusing to attach to default target.');
|
|
211
|
+
}
|
|
212
|
+
const client = await connectToChrome(port, logger, effectiveHost);
|
|
213
|
+
return { client };
|
|
214
|
+
}
|
|
215
|
+
export async function closeTab(port, targetId, logger, host) {
|
|
216
|
+
const effectiveHost = host ?? '127.0.0.1';
|
|
217
|
+
try {
|
|
218
|
+
await CDP.Close({ host: effectiveHost, port, id: targetId });
|
|
219
|
+
logger(`Closed isolated browser tab (target=${targetId})`);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
223
|
+
logger(`Failed to close browser tab ${targetId}: ${message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function buildChromeFlags(headless, debugBindAddress) {
|
|
227
|
+
const flags = [
|
|
228
|
+
'--disable-background-networking',
|
|
229
|
+
'--disable-background-timer-throttling',
|
|
230
|
+
'--disable-breakpad',
|
|
231
|
+
'--disable-client-side-phishing-detection',
|
|
232
|
+
'--disable-default-apps',
|
|
233
|
+
'--disable-hang-monitor',
|
|
234
|
+
'--disable-popup-blocking',
|
|
235
|
+
'--disable-prompt-on-repost',
|
|
236
|
+
'--disable-sync',
|
|
237
|
+
'--disable-translate',
|
|
238
|
+
'--metrics-recording-only',
|
|
239
|
+
'--no-first-run',
|
|
240
|
+
'--safebrowsing-disable-auto-update',
|
|
241
|
+
'--disable-features=TranslateUI,AutomationControlled',
|
|
242
|
+
'--mute-audio',
|
|
243
|
+
'--window-size=1280,720',
|
|
244
|
+
'--lang=en-US',
|
|
245
|
+
'--accept-lang=en-US,en',
|
|
246
|
+
];
|
|
247
|
+
if (process.platform !== 'win32' && !isWsl()) {
|
|
248
|
+
flags.push('--password-store=basic', '--use-mock-keychain');
|
|
249
|
+
}
|
|
250
|
+
if (debugBindAddress) {
|
|
251
|
+
flags.push(`--remote-debugging-address=${debugBindAddress}`);
|
|
252
|
+
}
|
|
253
|
+
if (headless) {
|
|
254
|
+
flags.push('--headless=new');
|
|
255
|
+
}
|
|
256
|
+
return flags;
|
|
257
|
+
}
|
|
258
|
+
function parseDebugPortEnv() {
|
|
259
|
+
const raw = process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT;
|
|
260
|
+
if (!raw)
|
|
261
|
+
return null;
|
|
262
|
+
const value = Number.parseInt(raw, 10);
|
|
263
|
+
if (!Number.isFinite(value) || value <= 0 || value > 65535) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
function resolveRemoteDebugHost() {
|
|
269
|
+
const override = process.env.ORACLE_BROWSER_REMOTE_DEBUG_HOST?.trim() || process.env.WSL_HOST_IP?.trim();
|
|
270
|
+
if (override) {
|
|
271
|
+
return override;
|
|
272
|
+
}
|
|
273
|
+
if (!isWsl()) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const resolv = readFileSync('/etc/resolv.conf', 'utf8');
|
|
278
|
+
for (const line of resolv.split('\n')) {
|
|
279
|
+
const match = line.match(/^nameserver\s+([0-9.]+)/);
|
|
280
|
+
if (match?.[1]) {
|
|
281
|
+
return match[1];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// ignore; fall back to localhost
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
function isWsl() {
|
|
291
|
+
if (process.platform !== 'linux') {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
if (process.env.WSL_DISTRO_NAME) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
const release = os.release();
|
|
298
|
+
return release.toLowerCase().includes('microsoft');
|
|
299
|
+
}
|
|
300
|
+
async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, }) {
|
|
301
|
+
const launcher = new Launcher({
|
|
302
|
+
chromePath: chromePath ?? undefined,
|
|
303
|
+
chromeFlags,
|
|
304
|
+
userDataDir,
|
|
305
|
+
handleSIGINT: false,
|
|
306
|
+
port: requestedPort ?? undefined,
|
|
307
|
+
});
|
|
308
|
+
if (host) {
|
|
309
|
+
const patched = launcher;
|
|
310
|
+
patched.isDebuggerReady = function patchedIsDebuggerReady() {
|
|
311
|
+
const debugPort = this.port ?? 0;
|
|
312
|
+
if (!debugPort) {
|
|
313
|
+
return Promise.reject(new Error('Missing Chrome debug port'));
|
|
314
|
+
}
|
|
315
|
+
return new Promise((resolve, reject) => {
|
|
316
|
+
const client = net.createConnection({ port: debugPort, host });
|
|
317
|
+
const cleanup = () => {
|
|
318
|
+
client.removeAllListeners();
|
|
319
|
+
client.end();
|
|
320
|
+
client.destroy();
|
|
321
|
+
client.unref();
|
|
322
|
+
};
|
|
323
|
+
client.once('error', (err) => {
|
|
324
|
+
cleanup();
|
|
325
|
+
reject(err);
|
|
326
|
+
});
|
|
327
|
+
client.once('connect', () => {
|
|
328
|
+
cleanup();
|
|
329
|
+
resolve();
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
await launcher.launch();
|
|
335
|
+
const kill = async () => launcher.kill();
|
|
336
|
+
return {
|
|
337
|
+
pid: launcher.pid ?? undefined,
|
|
338
|
+
port: launcher.port ?? 0,
|
|
339
|
+
process: launcher.chromeProcess,
|
|
340
|
+
kill,
|
|
341
|
+
host: host ?? undefined,
|
|
342
|
+
remoteDebuggingPipes: launcher.remoteDebuggingPipes,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from './constants.js';
|
|
2
|
+
import { normalizeBrowserModelStrategy } from './modelStrategy.js';
|
|
3
|
+
import { isTemporaryChatUrl, normalizeChatgptUrl } from './utils.js';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
export const DEFAULT_BROWSER_CONFIG = {
|
|
7
|
+
chromeProfile: null,
|
|
8
|
+
chromePath: null,
|
|
9
|
+
chromeCookiePath: null,
|
|
10
|
+
url: CHATGPT_URL,
|
|
11
|
+
chatgptUrl: CHATGPT_URL,
|
|
12
|
+
timeoutMs: 1_200_000,
|
|
13
|
+
debugPort: null,
|
|
14
|
+
inputTimeoutMs: 60_000,
|
|
15
|
+
assistantRecheckDelayMs: 0,
|
|
16
|
+
assistantRecheckTimeoutMs: 120_000,
|
|
17
|
+
reuseChromeWaitMs: 10_000,
|
|
18
|
+
profileLockTimeoutMs: 300_000,
|
|
19
|
+
autoReattachDelayMs: 0,
|
|
20
|
+
autoReattachIntervalMs: 0,
|
|
21
|
+
autoReattachTimeoutMs: 120_000,
|
|
22
|
+
cookieSync: true,
|
|
23
|
+
cookieNames: null,
|
|
24
|
+
cookieSyncWaitMs: 0,
|
|
25
|
+
inlineCookies: null,
|
|
26
|
+
inlineCookiesSource: null,
|
|
27
|
+
headless: false,
|
|
28
|
+
keepBrowser: false,
|
|
29
|
+
hideWindow: false,
|
|
30
|
+
desiredModel: DEFAULT_MODEL_TARGET,
|
|
31
|
+
modelStrategy: DEFAULT_MODEL_STRATEGY,
|
|
32
|
+
debug: false,
|
|
33
|
+
allowCookieErrors: false,
|
|
34
|
+
remoteChrome: null,
|
|
35
|
+
manualLogin: false,
|
|
36
|
+
manualLoginProfileDir: null,
|
|
37
|
+
manualLoginCookieSync: false,
|
|
38
|
+
};
|
|
39
|
+
export function resolveBrowserConfig(config) {
|
|
40
|
+
const debugPortEnv = parseDebugPort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT);
|
|
41
|
+
const envAllowCookieErrors = (process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? '').trim().toLowerCase() === 'true' ||
|
|
42
|
+
(process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? '').trim() === '1';
|
|
43
|
+
const rawUrl = config?.chatgptUrl ?? config?.url ?? DEFAULT_BROWSER_CONFIG.url;
|
|
44
|
+
const normalizedUrl = normalizeChatgptUrl(rawUrl ?? DEFAULT_BROWSER_CONFIG.url, DEFAULT_BROWSER_CONFIG.url);
|
|
45
|
+
const desiredModel = config?.desiredModel ?? DEFAULT_BROWSER_CONFIG.desiredModel ?? DEFAULT_MODEL_TARGET;
|
|
46
|
+
const modelStrategy = normalizeBrowserModelStrategy(config?.modelStrategy) ??
|
|
47
|
+
DEFAULT_BROWSER_CONFIG.modelStrategy ??
|
|
48
|
+
DEFAULT_MODEL_STRATEGY;
|
|
49
|
+
if (modelStrategy === 'select' && isTemporaryChatUrl(normalizedUrl) && /\bpro\b/i.test(desiredModel)) {
|
|
50
|
+
throw new Error('Temporary Chat mode does not expose Pro models in the ChatGPT model picker. ' +
|
|
51
|
+
'Remove "temporary-chat=true" from your browser URL, or use a non-Pro model label (e.g. "GPT-5.2").');
|
|
52
|
+
}
|
|
53
|
+
const isWindows = process.platform === 'win32';
|
|
54
|
+
const manualLogin = config?.manualLogin ?? (isWindows ? true : DEFAULT_BROWSER_CONFIG.manualLogin);
|
|
55
|
+
const cookieSyncDefault = isWindows ? false : DEFAULT_BROWSER_CONFIG.cookieSync;
|
|
56
|
+
const resolvedProfileDir = config?.manualLoginProfileDir ??
|
|
57
|
+
process.env.ORACLE_BROWSER_PROFILE_DIR ??
|
|
58
|
+
path.join(os.homedir(), '.oracle', 'browser-profile');
|
|
59
|
+
return {
|
|
60
|
+
...DEFAULT_BROWSER_CONFIG,
|
|
61
|
+
...(config ?? {}),
|
|
62
|
+
url: normalizedUrl,
|
|
63
|
+
chatgptUrl: normalizedUrl,
|
|
64
|
+
timeoutMs: config?.timeoutMs ?? DEFAULT_BROWSER_CONFIG.timeoutMs,
|
|
65
|
+
debugPort: config?.debugPort ?? debugPortEnv ?? DEFAULT_BROWSER_CONFIG.debugPort,
|
|
66
|
+
inputTimeoutMs: config?.inputTimeoutMs ?? DEFAULT_BROWSER_CONFIG.inputTimeoutMs,
|
|
67
|
+
assistantRecheckDelayMs: config?.assistantRecheckDelayMs ?? DEFAULT_BROWSER_CONFIG.assistantRecheckDelayMs,
|
|
68
|
+
assistantRecheckTimeoutMs: config?.assistantRecheckTimeoutMs ?? DEFAULT_BROWSER_CONFIG.assistantRecheckTimeoutMs,
|
|
69
|
+
reuseChromeWaitMs: config?.reuseChromeWaitMs ?? DEFAULT_BROWSER_CONFIG.reuseChromeWaitMs,
|
|
70
|
+
profileLockTimeoutMs: config?.profileLockTimeoutMs ?? DEFAULT_BROWSER_CONFIG.profileLockTimeoutMs,
|
|
71
|
+
autoReattachDelayMs: config?.autoReattachDelayMs ?? DEFAULT_BROWSER_CONFIG.autoReattachDelayMs,
|
|
72
|
+
autoReattachIntervalMs: config?.autoReattachIntervalMs ?? DEFAULT_BROWSER_CONFIG.autoReattachIntervalMs,
|
|
73
|
+
autoReattachTimeoutMs: config?.autoReattachTimeoutMs ?? DEFAULT_BROWSER_CONFIG.autoReattachTimeoutMs,
|
|
74
|
+
cookieSync: config?.cookieSync ?? cookieSyncDefault,
|
|
75
|
+
cookieNames: config?.cookieNames ?? DEFAULT_BROWSER_CONFIG.cookieNames,
|
|
76
|
+
cookieSyncWaitMs: config?.cookieSyncWaitMs ?? DEFAULT_BROWSER_CONFIG.cookieSyncWaitMs,
|
|
77
|
+
inlineCookies: config?.inlineCookies ?? DEFAULT_BROWSER_CONFIG.inlineCookies,
|
|
78
|
+
inlineCookiesSource: config?.inlineCookiesSource ?? DEFAULT_BROWSER_CONFIG.inlineCookiesSource,
|
|
79
|
+
headless: config?.headless ?? DEFAULT_BROWSER_CONFIG.headless,
|
|
80
|
+
keepBrowser: config?.keepBrowser ?? DEFAULT_BROWSER_CONFIG.keepBrowser,
|
|
81
|
+
hideWindow: config?.hideWindow ?? DEFAULT_BROWSER_CONFIG.hideWindow,
|
|
82
|
+
desiredModel,
|
|
83
|
+
modelStrategy,
|
|
84
|
+
chromeProfile: config?.chromeProfile ?? DEFAULT_BROWSER_CONFIG.chromeProfile,
|
|
85
|
+
chromePath: config?.chromePath ?? DEFAULT_BROWSER_CONFIG.chromePath,
|
|
86
|
+
chromeCookiePath: config?.chromeCookiePath ?? DEFAULT_BROWSER_CONFIG.chromeCookiePath,
|
|
87
|
+
debug: config?.debug ?? DEFAULT_BROWSER_CONFIG.debug,
|
|
88
|
+
allowCookieErrors: config?.allowCookieErrors ?? envAllowCookieErrors ?? DEFAULT_BROWSER_CONFIG.allowCookieErrors,
|
|
89
|
+
thinkingTime: config?.thinkingTime,
|
|
90
|
+
manualLogin,
|
|
91
|
+
manualLoginProfileDir: manualLogin ? resolvedProfileDir : null,
|
|
92
|
+
manualLoginCookieSync: config?.manualLoginCookieSync ?? DEFAULT_BROWSER_CONFIG.manualLoginCookieSync,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function parseDebugPort(raw) {
|
|
96
|
+
if (!raw)
|
|
97
|
+
return null;
|
|
98
|
+
const value = Number.parseInt(raw, 10);
|
|
99
|
+
if (!Number.isFinite(value) || value <= 0 || value > 65535) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|