@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,19 +1,36 @@
|
|
|
1
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';
|
|
2
5
|
import { execFile } from 'node:child_process';
|
|
3
6
|
import { promisify } from 'node:util';
|
|
4
7
|
import CDP from 'chrome-remote-interface';
|
|
5
|
-
import { launch } from 'chrome-launcher';
|
|
8
|
+
import { launch, Launcher } from 'chrome-launcher';
|
|
6
9
|
const execFileAsync = promisify(execFile);
|
|
7
10
|
export async function launchChrome(config, userDataDir, logger) {
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const connectHost = resolveRemoteDebugHost();
|
|
12
|
+
const debugBindAddress = connectHost && connectHost !== '127.0.0.1' ? '0.0.0.0' : connectHost;
|
|
13
|
+
const debugPort = config.debugPort ?? parseDebugPortEnv();
|
|
14
|
+
const chromeFlags = buildChromeFlags(config.headless ?? false, debugBindAddress);
|
|
15
|
+
const usePatchedLauncher = Boolean(connectHost && connectHost !== '127.0.0.1');
|
|
16
|
+
const launcher = usePatchedLauncher
|
|
17
|
+
? await launchWithCustomHost({
|
|
18
|
+
chromeFlags,
|
|
19
|
+
chromePath: config.chromePath ?? undefined,
|
|
20
|
+
userDataDir,
|
|
21
|
+
host: connectHost ?? '127.0.0.1',
|
|
22
|
+
requestedPort: debugPort ?? undefined,
|
|
23
|
+
})
|
|
24
|
+
: await launch({
|
|
25
|
+
chromePath: config.chromePath ?? undefined,
|
|
26
|
+
chromeFlags,
|
|
27
|
+
userDataDir,
|
|
28
|
+
port: debugPort ?? undefined,
|
|
29
|
+
});
|
|
14
30
|
const pidLabel = typeof launcher.pid === 'number' ? ` (pid ${launcher.pid})` : '';
|
|
15
|
-
|
|
16
|
-
|
|
31
|
+
const hostLabel = connectHost ? ` on ${connectHost}` : '';
|
|
32
|
+
logger(`Launched Chrome${pidLabel} on port ${launcher.port}${hostLabel}`);
|
|
33
|
+
return Object.assign(launcher, { host: connectHost ?? '127.0.0.1' });
|
|
17
34
|
}
|
|
18
35
|
export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logger) {
|
|
19
36
|
const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
|
|
@@ -23,15 +40,20 @@ export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logge
|
|
|
23
40
|
return;
|
|
24
41
|
}
|
|
25
42
|
handling = true;
|
|
26
|
-
|
|
43
|
+
if (keepBrowser) {
|
|
44
|
+
logger(`Received ${signal}; leaving Chrome running for potential reattach`);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
logger(`Received ${signal}; terminating Chrome process`);
|
|
48
|
+
}
|
|
27
49
|
void (async () => {
|
|
28
|
-
try {
|
|
29
|
-
await chrome.kill();
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
// ignore kill failures
|
|
33
|
-
}
|
|
34
50
|
if (!keepBrowser) {
|
|
51
|
+
try {
|
|
52
|
+
await chrome.kill();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// ignore kill failures
|
|
56
|
+
}
|
|
35
57
|
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
36
58
|
}
|
|
37
59
|
})().finally(() => {
|
|
@@ -71,17 +93,44 @@ export async function hideChromeWindow(chrome, logger) {
|
|
|
71
93
|
logger(`Failed to hide Chrome window: ${message}`);
|
|
72
94
|
}
|
|
73
95
|
}
|
|
74
|
-
export async function connectToChrome(port, logger) {
|
|
75
|
-
const client = await CDP({ port });
|
|
96
|
+
export async function connectToChrome(port, logger, host) {
|
|
97
|
+
const client = await CDP({ port, host });
|
|
76
98
|
logger('Connected to Chrome DevTools protocol');
|
|
77
99
|
return client;
|
|
78
100
|
}
|
|
79
|
-
export async function connectToRemoteChrome(host, port, logger) {
|
|
80
|
-
|
|
101
|
+
export async function connectToRemoteChrome(host, port, logger, targetUrl) {
|
|
102
|
+
if (targetUrl) {
|
|
103
|
+
try {
|
|
104
|
+
const target = await CDP.New({ host, port, url: targetUrl });
|
|
105
|
+
const client = await CDP({ host, port, target: target.id });
|
|
106
|
+
logger(`Opened dedicated remote Chrome tab targeting ${targetUrl}`);
|
|
107
|
+
return { client, targetId: target.id };
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
111
|
+
logger(`Failed to open dedicated remote Chrome tab (${message}); falling back to first target.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const fallbackClient = await CDP({ host, port });
|
|
81
115
|
logger(`Connected to remote Chrome DevTools protocol at ${host}:${port}`);
|
|
82
|
-
return client;
|
|
116
|
+
return { client: fallbackClient };
|
|
83
117
|
}
|
|
84
|
-
function
|
|
118
|
+
export async function closeRemoteChromeTarget(host, port, targetId, logger) {
|
|
119
|
+
if (!targetId) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await CDP.Close({ host, port, id: targetId });
|
|
124
|
+
if (logger.verbose) {
|
|
125
|
+
logger(`Closed remote Chrome tab ${targetId}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
130
|
+
logger(`Failed to close remote Chrome tab ${targetId}: ${message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function buildChromeFlags(headless, debugBindAddress) {
|
|
85
134
|
const flags = [
|
|
86
135
|
'--disable-background-networking',
|
|
87
136
|
'--disable-background-timer-throttling',
|
|
@@ -99,9 +148,101 @@ function buildChromeFlags(_headless) {
|
|
|
99
148
|
'--disable-features=TranslateUI,AutomationControlled',
|
|
100
149
|
'--mute-audio',
|
|
101
150
|
'--window-size=1280,720',
|
|
102
|
-
'--password-store=basic',
|
|
103
|
-
'--use-mock-keychain',
|
|
104
151
|
];
|
|
105
|
-
|
|
152
|
+
if (process.platform !== 'win32' && !isWsl()) {
|
|
153
|
+
flags.push('--password-store=basic', '--use-mock-keychain');
|
|
154
|
+
}
|
|
155
|
+
if (debugBindAddress) {
|
|
156
|
+
flags.push(`--remote-debugging-address=${debugBindAddress}`);
|
|
157
|
+
}
|
|
158
|
+
if (headless) {
|
|
159
|
+
flags.push('--headless=new');
|
|
160
|
+
}
|
|
106
161
|
return flags;
|
|
107
162
|
}
|
|
163
|
+
function parseDebugPortEnv() {
|
|
164
|
+
const raw = process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT;
|
|
165
|
+
if (!raw)
|
|
166
|
+
return null;
|
|
167
|
+
const value = Number.parseInt(raw, 10);
|
|
168
|
+
if (!Number.isFinite(value) || value <= 0 || value > 65535) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
function resolveRemoteDebugHost() {
|
|
174
|
+
const override = process.env.ORACLE_BROWSER_REMOTE_DEBUG_HOST?.trim() || process.env.WSL_HOST_IP?.trim();
|
|
175
|
+
if (override) {
|
|
176
|
+
return override;
|
|
177
|
+
}
|
|
178
|
+
if (!isWsl()) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const resolv = readFileSync('/etc/resolv.conf', 'utf8');
|
|
183
|
+
for (const line of resolv.split('\n')) {
|
|
184
|
+
const match = line.match(/^nameserver\s+([0-9.]+)/);
|
|
185
|
+
if (match?.[1]) {
|
|
186
|
+
return match[1];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// ignore; fall back to localhost
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
function isWsl() {
|
|
196
|
+
if (process.platform !== 'linux') {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
if (process.env.WSL_DISTRO_NAME) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
const release = os.release();
|
|
203
|
+
return release.toLowerCase().includes('microsoft');
|
|
204
|
+
}
|
|
205
|
+
async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, }) {
|
|
206
|
+
const launcher = new Launcher({
|
|
207
|
+
chromePath: chromePath ?? undefined,
|
|
208
|
+
chromeFlags,
|
|
209
|
+
userDataDir,
|
|
210
|
+
port: requestedPort ?? undefined,
|
|
211
|
+
});
|
|
212
|
+
if (host) {
|
|
213
|
+
const patched = launcher;
|
|
214
|
+
patched.isDebuggerReady = function patchedIsDebuggerReady() {
|
|
215
|
+
const debugPort = this.port ?? 0;
|
|
216
|
+
if (!debugPort) {
|
|
217
|
+
return Promise.reject(new Error('Missing Chrome debug port'));
|
|
218
|
+
}
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
const client = net.createConnection({ port: debugPort, host });
|
|
221
|
+
const cleanup = () => {
|
|
222
|
+
client.removeAllListeners();
|
|
223
|
+
client.end();
|
|
224
|
+
client.destroy();
|
|
225
|
+
client.unref();
|
|
226
|
+
};
|
|
227
|
+
client.once('error', (err) => {
|
|
228
|
+
cleanup();
|
|
229
|
+
reject(err);
|
|
230
|
+
});
|
|
231
|
+
client.once('connect', () => {
|
|
232
|
+
cleanup();
|
|
233
|
+
resolve();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
await launcher.launch();
|
|
239
|
+
const kill = async () => launcher.kill();
|
|
240
|
+
return {
|
|
241
|
+
pid: launcher.pid ?? undefined,
|
|
242
|
+
port: launcher.port ?? 0,
|
|
243
|
+
process: launcher.chromeProcess,
|
|
244
|
+
kill,
|
|
245
|
+
host: host ?? undefined,
|
|
246
|
+
remoteDebuggingPipes: launcher.remoteDebuggingPipes,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { CHATGPT_URL, DEFAULT_MODEL_TARGET } from './constants.js';
|
|
2
2
|
import { normalizeChatgptUrl } from './utils.js';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
3
5
|
export const DEFAULT_BROWSER_CONFIG = {
|
|
4
6
|
chromeProfile: null,
|
|
5
7
|
chromePath: null,
|
|
@@ -7,6 +9,7 @@ export const DEFAULT_BROWSER_CONFIG = {
|
|
|
7
9
|
url: CHATGPT_URL,
|
|
8
10
|
chatgptUrl: CHATGPT_URL,
|
|
9
11
|
timeoutMs: 1_200_000,
|
|
12
|
+
debugPort: null,
|
|
10
13
|
inputTimeoutMs: 30_000,
|
|
11
14
|
cookieSync: true,
|
|
12
15
|
cookieNames: null,
|
|
@@ -19,20 +22,30 @@ export const DEFAULT_BROWSER_CONFIG = {
|
|
|
19
22
|
debug: false,
|
|
20
23
|
allowCookieErrors: false,
|
|
21
24
|
remoteChrome: null,
|
|
25
|
+
manualLogin: false,
|
|
26
|
+
manualLoginProfileDir: null,
|
|
22
27
|
};
|
|
23
28
|
export function resolveBrowserConfig(config) {
|
|
29
|
+
const debugPortEnv = parseDebugPort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT);
|
|
24
30
|
const envAllowCookieErrors = (process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? '').trim().toLowerCase() === 'true' ||
|
|
25
31
|
(process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? '').trim() === '1';
|
|
26
32
|
const rawUrl = config?.chatgptUrl ?? config?.url ?? DEFAULT_BROWSER_CONFIG.url;
|
|
27
33
|
const normalizedUrl = normalizeChatgptUrl(rawUrl ?? DEFAULT_BROWSER_CONFIG.url, DEFAULT_BROWSER_CONFIG.url);
|
|
34
|
+
const isWindows = process.platform === 'win32';
|
|
35
|
+
const manualLogin = config?.manualLogin ?? (isWindows ? true : DEFAULT_BROWSER_CONFIG.manualLogin);
|
|
36
|
+
const cookieSyncDefault = isWindows ? false : DEFAULT_BROWSER_CONFIG.cookieSync;
|
|
37
|
+
const resolvedProfileDir = config?.manualLoginProfileDir ??
|
|
38
|
+
process.env.ORACLE_BROWSER_PROFILE_DIR ??
|
|
39
|
+
path.join(os.homedir(), '.oracle', 'browser-profile');
|
|
28
40
|
return {
|
|
29
41
|
...DEFAULT_BROWSER_CONFIG,
|
|
30
42
|
...(config ?? {}),
|
|
31
43
|
url: normalizedUrl,
|
|
32
44
|
chatgptUrl: normalizedUrl,
|
|
33
45
|
timeoutMs: config?.timeoutMs ?? DEFAULT_BROWSER_CONFIG.timeoutMs,
|
|
46
|
+
debugPort: config?.debugPort ?? debugPortEnv ?? DEFAULT_BROWSER_CONFIG.debugPort,
|
|
34
47
|
inputTimeoutMs: config?.inputTimeoutMs ?? DEFAULT_BROWSER_CONFIG.inputTimeoutMs,
|
|
35
|
-
cookieSync: config?.cookieSync ??
|
|
48
|
+
cookieSync: config?.cookieSync ?? cookieSyncDefault,
|
|
36
49
|
cookieNames: config?.cookieNames ?? DEFAULT_BROWSER_CONFIG.cookieNames,
|
|
37
50
|
inlineCookies: config?.inlineCookies ?? DEFAULT_BROWSER_CONFIG.inlineCookies,
|
|
38
51
|
inlineCookiesSource: config?.inlineCookiesSource ?? DEFAULT_BROWSER_CONFIG.inlineCookiesSource,
|
|
@@ -45,5 +58,16 @@ export function resolveBrowserConfig(config) {
|
|
|
45
58
|
chromeCookiePath: config?.chromeCookiePath ?? DEFAULT_BROWSER_CONFIG.chromeCookiePath,
|
|
46
59
|
debug: config?.debug ?? DEFAULT_BROWSER_CONFIG.debug,
|
|
47
60
|
allowCookieErrors: config?.allowCookieErrors ?? envAllowCookieErrors ?? DEFAULT_BROWSER_CONFIG.allowCookieErrors,
|
|
61
|
+
manualLogin,
|
|
62
|
+
manualLoginProfileDir: manualLogin ? resolvedProfileDir : null,
|
|
48
63
|
};
|
|
49
64
|
}
|
|
65
|
+
function parseDebugPort(raw) {
|
|
66
|
+
if (!raw)
|
|
67
|
+
return null;
|
|
68
|
+
const value = Number.parseInt(raw, 10);
|
|
69
|
+
if (!Number.isFinite(value) || value <= 0 || value > 65535) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
@@ -24,18 +24,37 @@ export const CLOUDFLARE_SCRIPT_SELECTOR = 'script[src*="/challenge-platform/"]';
|
|
|
24
24
|
export const CLOUDFLARE_TITLE = 'just a moment';
|
|
25
25
|
export const PROMPT_PRIMARY_SELECTOR = '#prompt-textarea';
|
|
26
26
|
export const PROMPT_FALLBACK_SELECTOR = 'textarea[name="prompt-textarea"]';
|
|
27
|
-
export const
|
|
28
|
-
|
|
27
|
+
export const FILE_INPUT_SELECTORS = [
|
|
28
|
+
'form input[type="file"]:not([accept])',
|
|
29
|
+
'input[type="file"][multiple]:not([accept])',
|
|
30
|
+
'input[type="file"][multiple]',
|
|
31
|
+
'input[type="file"]:not([accept])',
|
|
32
|
+
'input[type="file"][data-testid*="file"]',
|
|
33
|
+
];
|
|
34
|
+
// Legacy single selectors kept for compatibility with older call-sites
|
|
35
|
+
export const FILE_INPUT_SELECTOR = FILE_INPUT_SELECTORS[0];
|
|
36
|
+
export const GENERIC_FILE_INPUT_SELECTOR = FILE_INPUT_SELECTORS[3];
|
|
29
37
|
export const MENU_CONTAINER_SELECTOR = '[role="menu"], [data-radix-collection-root]';
|
|
30
38
|
export const MENU_ITEM_SELECTOR = 'button, [role="menuitem"], [role="menuitemradio"], [data-testid*="model-switcher-"]';
|
|
31
39
|
export const UPLOAD_STATUS_SELECTORS = [
|
|
32
40
|
'[data-testid*="upload"]',
|
|
33
41
|
'[data-testid*="attachment"]',
|
|
42
|
+
'[data-testid*="progress"]',
|
|
34
43
|
'[data-state="loading"]',
|
|
44
|
+
'[data-state="uploading"]',
|
|
45
|
+
'[data-state="pending"]',
|
|
35
46
|
'[aria-live="polite"]',
|
|
47
|
+
'[aria-live="assertive"]',
|
|
36
48
|
];
|
|
37
49
|
export const STOP_BUTTON_SELECTOR = '[data-testid="stop-button"]';
|
|
38
|
-
export const
|
|
50
|
+
export const SEND_BUTTON_SELECTORS = [
|
|
51
|
+
'[data-testid="send-button"]',
|
|
52
|
+
'button[data-testid="composer-send-button"]',
|
|
53
|
+
'button[aria-label="Send message"]',
|
|
54
|
+
'button[aria-label*="Send"]',
|
|
55
|
+
'button[type="submit"][data-testid*="send"]',
|
|
56
|
+
];
|
|
57
|
+
export const SEND_BUTTON_SELECTOR = SEND_BUTTON_SELECTORS[0];
|
|
39
58
|
export const MODEL_BUTTON_SELECTOR = '[data-testid="model-switcher-dropdown-button"]';
|
|
40
59
|
export const COPY_BUTTON_SELECTOR = 'button[data-testid="copy-turn-action-button"]';
|
|
41
60
|
// Action buttons that only appear once a turn has finished rendering.
|