@steipete/oracle 0.4.5 → 0.5.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 +11 -9
- package/dist/.DS_Store +0 -0
- 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 +67 -18
- package/dist/src/browser/actions/remoteFileTransfer.js +36 -4
- package/dist/src/browser/chromeCookies.js +44 -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 +384 -22
- package/dist/src/browser/profileSync.js +141 -0
- 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 +102 -104
- package/dist/src/cli/sessionRunner.js +39 -6
- package/dist/src/cli/sessionTable.js +88 -0
- package/dist/src/cli/tui/index.js +19 -89
- package/dist/src/heartbeat.js +2 -2
- package/dist/src/oracle/background.js +10 -2
- package/dist/src/oracle/client.js +107 -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 +83 -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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight browser connectivity smoke test.
|
|
4
|
+
* - Launches Chrome headful with a fixed DevTools port (default 45871 or env ORACLE_BROWSER_PORT/ORACLE_BROWSER_DEBUG_PORT).
|
|
5
|
+
* - Verifies the DevTools /json/version endpoint responds.
|
|
6
|
+
* - Prints a WSL-friendly firewall hint if the port is unreachable.
|
|
7
|
+
*/
|
|
8
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
9
|
+
import { launch } from 'chrome-launcher';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
const DEFAULT_PORT = 45871;
|
|
13
|
+
const port = normalizePort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT) ?? DEFAULT_PORT;
|
|
14
|
+
const hostHint = resolveWslHost();
|
|
15
|
+
const targetHost = hostHint ?? '127.0.0.1';
|
|
16
|
+
function normalizePort(raw) {
|
|
17
|
+
if (!raw)
|
|
18
|
+
return null;
|
|
19
|
+
const value = Number.parseInt(raw, 10);
|
|
20
|
+
if (!Number.isFinite(value) || value <= 0 || value > 65535)
|
|
21
|
+
return null;
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function isWsl() {
|
|
25
|
+
if (process.platform !== 'linux')
|
|
26
|
+
return false;
|
|
27
|
+
if (process.env.WSL_DISTRO_NAME)
|
|
28
|
+
return true;
|
|
29
|
+
return os.release().toLowerCase().includes('microsoft');
|
|
30
|
+
}
|
|
31
|
+
function resolveWslHost() {
|
|
32
|
+
if (!isWsl())
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
const resolv = readFileSync('/etc/resolv.conf', 'utf8');
|
|
36
|
+
for (const line of resolv.split('\n')) {
|
|
37
|
+
const match = line.match(/^nameserver\s+([0-9.]+)/);
|
|
38
|
+
if (match?.[1])
|
|
39
|
+
return match[1];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// ignore
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function firewallHint(host, devtoolsPort) {
|
|
48
|
+
if (!isWsl())
|
|
49
|
+
return null;
|
|
50
|
+
return [
|
|
51
|
+
`DevTools port ${host}:${devtoolsPort} is blocked from WSL.`,
|
|
52
|
+
'',
|
|
53
|
+
'PowerShell (admin):',
|
|
54
|
+
`New-NetFirewallRule -DisplayName 'Chrome DevTools ${devtoolsPort}' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${devtoolsPort}`,
|
|
55
|
+
"New-NetFirewallRule -DisplayName 'Chrome DevTools (chrome.exe)' -Direction Inbound -Action Allow -Program 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' -Protocol TCP",
|
|
56
|
+
'',
|
|
57
|
+
'Re-run ./runner pnpm test:browser after adding the rule.',
|
|
58
|
+
].join('\n');
|
|
59
|
+
}
|
|
60
|
+
async function fetchVersion(host, devtoolsPort) {
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`http://${host}:${devtoolsPort}/json/version`, { signal: controller.signal });
|
|
65
|
+
if (!res.ok)
|
|
66
|
+
return false;
|
|
67
|
+
const json = (await res.json());
|
|
68
|
+
return Boolean(json.webSocketDebuggerUrl);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function main() {
|
|
78
|
+
console.log(`[browser-test] launching Chrome on ${targetHost}:${port} (headful)…`);
|
|
79
|
+
const chrome = await launch({
|
|
80
|
+
port,
|
|
81
|
+
chromeFlags: ['--remote-debugging-address=0.0.0.0'],
|
|
82
|
+
});
|
|
83
|
+
let ok = await fetchVersion(targetHost, chrome.port);
|
|
84
|
+
if (!ok) {
|
|
85
|
+
await sleep(500);
|
|
86
|
+
ok = await fetchVersion(targetHost, chrome.port);
|
|
87
|
+
}
|
|
88
|
+
await chrome.kill();
|
|
89
|
+
if (ok) {
|
|
90
|
+
console.log(`[browser-test] PASS: DevTools responding on ${targetHost}:${chrome.port}`);
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
const hint = firewallHint(targetHost, chrome.port);
|
|
94
|
+
console.error(`[browser-test] FAIL: DevTools not reachable at ${targetHost}:${chrome.port}`);
|
|
95
|
+
if (hint) {
|
|
96
|
+
console.error(hint);
|
|
97
|
+
}
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
main().catch((error) => {
|
|
101
|
+
console.error('[browser-test] Unexpected failure:', error instanceof Error ? error.message : String(error));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* POC: Test connecting to remote Chrome instance
|
|
4
|
+
*
|
|
5
|
+
* On remote machine with display, run:
|
|
6
|
+
* google-chrome --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0
|
|
7
|
+
*
|
|
8
|
+
* Then run this script:
|
|
9
|
+
* npx tsx scripts/test-remote-chrome.ts <remote-host> [port]
|
|
10
|
+
*/
|
|
11
|
+
import CDP from 'chrome-remote-interface';
|
|
12
|
+
async function main() {
|
|
13
|
+
const host = process.argv[2] || 'localhost';
|
|
14
|
+
const port = parseInt(process.argv[3] || '9222', 10);
|
|
15
|
+
console.log(`Attempting to connect to Chrome at ${host}:${port}...`);
|
|
16
|
+
try {
|
|
17
|
+
// Test connection
|
|
18
|
+
const client = await CDP({ host, port });
|
|
19
|
+
console.log('✓ Connected to Chrome DevTools Protocol');
|
|
20
|
+
const { Network, Page, Runtime } = client;
|
|
21
|
+
// Enable domains
|
|
22
|
+
await Promise.all([Network.enable(), Page.enable()]);
|
|
23
|
+
console.log('✓ Enabled Network and Page domains');
|
|
24
|
+
// Get browser version info
|
|
25
|
+
const version = await CDP.Version({ host, port });
|
|
26
|
+
console.log(`✓ Browser: ${version.Browser}`);
|
|
27
|
+
console.log(`✓ Protocol: ${version['Protocol-Version']}`);
|
|
28
|
+
// Navigate to ChatGPT
|
|
29
|
+
console.log('\nNavigating to ChatGPT...');
|
|
30
|
+
await Page.navigate({ url: 'https://chatgpt.com/' });
|
|
31
|
+
await Page.loadEventFired();
|
|
32
|
+
console.log('✓ Page loaded');
|
|
33
|
+
// Check current URL
|
|
34
|
+
const evalResult = await Runtime.evaluate({ expression: 'window.location.href' });
|
|
35
|
+
console.log(`✓ Current URL: ${evalResult.result.value}`);
|
|
36
|
+
// Check if logged in (look for specific elements)
|
|
37
|
+
const checkLogin = await Runtime.evaluate({
|
|
38
|
+
expression: `
|
|
39
|
+
// Check for composer textarea (indicates logged in)
|
|
40
|
+
const composer = document.querySelector('textarea, [contenteditable="true"]');
|
|
41
|
+
const hasComposer = !!composer;
|
|
42
|
+
|
|
43
|
+
// Check for login button (indicates logged out)
|
|
44
|
+
const loginBtn = document.querySelector('a[href*="login"], button[data-testid*="login"]');
|
|
45
|
+
const hasLogin = !!loginBtn;
|
|
46
|
+
|
|
47
|
+
({ hasComposer, hasLogin, loggedIn: hasComposer && !hasLogin })
|
|
48
|
+
`,
|
|
49
|
+
});
|
|
50
|
+
console.log(`✓ Login status: ${JSON.stringify(checkLogin.result.value)}`);
|
|
51
|
+
await client.close();
|
|
52
|
+
console.log('\n✓ POC successful! Remote Chrome connection works.');
|
|
53
|
+
console.log('\nTo use Oracle with remote Chrome, you would need to:');
|
|
54
|
+
console.log('1. Ensure cookies are loaded in remote Chrome');
|
|
55
|
+
console.log('2. Configure Oracle with --remote-chrome <host:port> to use this instance');
|
|
56
|
+
console.log('3. Ensure Oracle skips local Chrome launch when --remote-chrome is specified');
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error('✗ Connection failed:', error instanceof Error ? error.message : error);
|
|
60
|
+
console.log('\nTroubleshooting:');
|
|
61
|
+
console.log('1. Ensure Chrome is running on remote machine with:');
|
|
62
|
+
console.log(` google-chrome --remote-debugging-port=${port} --remote-debugging-address=0.0.0.0`);
|
|
63
|
+
console.log('2. Check firewall allows connections to port', port);
|
|
64
|
+
console.log('3. Verify network connectivity to', host);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
void main();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { FILE_INPUT_SELECTORS, SEND_BUTTON_SELECTORS, UPLOAD_STATUS_SELECTORS } from '../constants.js';
|
|
3
3
|
import { delay } from '../utils.js';
|
|
4
4
|
import { logDomFailure } from '../domDebug.js';
|
|
5
5
|
export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
@@ -8,7 +8,7 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
8
8
|
throw new Error('DOM domain unavailable while uploading attachments.');
|
|
9
9
|
}
|
|
10
10
|
const documentNode = await dom.getDocument();
|
|
11
|
-
const selectors =
|
|
11
|
+
const selectors = FILE_INPUT_SELECTORS;
|
|
12
12
|
let targetNodeId;
|
|
13
13
|
for (const selector of selectors) {
|
|
14
14
|
const result = await dom.querySelector({ nodeId: documentNode.root.nodeId, selector });
|
|
@@ -33,25 +33,49 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
33
33
|
export async function waitForAttachmentCompletion(Runtime, timeoutMs, logger) {
|
|
34
34
|
const deadline = Date.now() + timeoutMs;
|
|
35
35
|
const expression = `(() => {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const sendSelectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
37
|
+
let button = null;
|
|
38
|
+
for (const selector of sendSelectors) {
|
|
39
|
+
button = document.querySelector(selector);
|
|
40
|
+
if (button) break;
|
|
39
41
|
}
|
|
40
|
-
const disabled = button
|
|
42
|
+
const disabled = button
|
|
43
|
+
? button.hasAttribute('disabled') ||
|
|
44
|
+
button.getAttribute('aria-disabled') === 'true' ||
|
|
45
|
+
button.getAttribute('data-disabled') === 'true' ||
|
|
46
|
+
window.getComputedStyle(button).pointerEvents === 'none'
|
|
47
|
+
: null;
|
|
41
48
|
const uploadingSelectors = ${JSON.stringify(UPLOAD_STATUS_SELECTORS)};
|
|
42
49
|
const uploading = uploadingSelectors.some((selector) => {
|
|
43
50
|
return Array.from(document.querySelectorAll(selector)).some((node) => {
|
|
51
|
+
const ariaBusy = node.getAttribute?.('aria-busy');
|
|
52
|
+
const dataState = node.getAttribute?.('data-state');
|
|
53
|
+
if (ariaBusy === 'true' || dataState === 'loading' || dataState === 'uploading' || dataState === 'pending') {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
44
56
|
const text = node.textContent?.toLowerCase?.() ?? '';
|
|
45
|
-
return text.includes('upload') || text.includes('processing');
|
|
57
|
+
return text.includes('upload') || text.includes('processing') || text.includes('uploading');
|
|
46
58
|
});
|
|
47
59
|
});
|
|
48
|
-
|
|
60
|
+
const fileSelectors = ${JSON.stringify(FILE_INPUT_SELECTORS)};
|
|
61
|
+
const filesAttached = fileSelectors.some((selector) =>
|
|
62
|
+
Array.from(document.querySelectorAll(selector)).some((node) => {
|
|
63
|
+
const el = node instanceof HTMLInputElement ? node : null;
|
|
64
|
+
return Boolean(el?.files?.length);
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
return { state: button ? (disabled ? 'disabled' : 'ready') : 'missing', uploading, filesAttached };
|
|
49
68
|
})()`;
|
|
50
69
|
while (Date.now() < deadline) {
|
|
51
70
|
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
52
71
|
const value = result?.value;
|
|
53
|
-
if (value &&
|
|
54
|
-
|
|
72
|
+
if (value && !value.uploading) {
|
|
73
|
+
if (value.state === 'ready') {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (value.state === 'missing' && value.filesAttached) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
55
79
|
}
|
|
56
80
|
await delay(250);
|
|
57
81
|
}
|
|
@@ -62,13 +86,20 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, logger) {
|
|
|
62
86
|
async function waitForAttachmentSelection(Runtime, expectedName, timeoutMs) {
|
|
63
87
|
const deadline = Date.now() + timeoutMs;
|
|
64
88
|
const expression = `(() => {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
const selectors = ${JSON.stringify(FILE_INPUT_SELECTORS)};
|
|
90
|
+
for (const selector of selectors) {
|
|
91
|
+
const inputs = Array.from(document.querySelectorAll(selector));
|
|
92
|
+
for (const input of inputs) {
|
|
93
|
+
if (!(input instanceof HTMLInputElement) || !input.files) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const names = Array.from(input.files ?? []).map((file) => file?.name ?? '');
|
|
97
|
+
if (names.some((name) => name === ${JSON.stringify(expectedName)})) {
|
|
98
|
+
return { matched: true, names };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
69
101
|
}
|
|
70
|
-
|
|
71
|
-
return { matched: names.some((name) => name === ${JSON.stringify(expectedName)}), names };
|
|
102
|
+
return { matched: false, names: [] };
|
|
72
103
|
})()`;
|
|
73
104
|
while (Date.now() < deadline) {
|
|
74
105
|
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { INPUT_SELECTORS, PROMPT_PRIMARY_SELECTOR, PROMPT_FALLBACK_SELECTOR,
|
|
1
|
+
import { INPUT_SELECTORS, PROMPT_PRIMARY_SELECTOR, PROMPT_FALLBACK_SELECTOR, SEND_BUTTON_SELECTORS, CONVERSATION_TURN_SELECTOR, } from '../constants.js';
|
|
2
2
|
import { delay } from '../utils.js';
|
|
3
3
|
import { logDomFailure } from '../domDebug.js';
|
|
4
|
+
const ENTER_KEY_EVENT = {
|
|
5
|
+
key: 'Enter',
|
|
6
|
+
code: 'Enter',
|
|
7
|
+
windowsVirtualKeyCode: 13,
|
|
8
|
+
nativeVirtualKeyCode: 13,
|
|
9
|
+
};
|
|
10
|
+
const ENTER_KEY_TEXT = '\r';
|
|
4
11
|
export async function submitPrompt(deps, prompt, logger) {
|
|
5
12
|
const { runtime, input } = deps;
|
|
6
13
|
const encodedPrompt = JSON.stringify(prompt);
|
|
@@ -52,6 +59,9 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
52
59
|
throw new Error('Failed to focus prompt textarea');
|
|
53
60
|
}
|
|
54
61
|
await input.insertText({ text: prompt });
|
|
62
|
+
// Some pages (notably ChatGPT when subscriptions/widgets load) need a brief settle
|
|
63
|
+
// before the send button becomes enabled; give it a short breather to avoid races.
|
|
64
|
+
await delay(500);
|
|
55
65
|
const primarySelectorLiteral = JSON.stringify(PROMPT_PRIMARY_SELECTOR);
|
|
56
66
|
const fallbackSelectorLiteral = JSON.stringify(PROMPT_FALLBACK_SELECTOR);
|
|
57
67
|
const verification = await runtime.evaluate({
|
|
@@ -86,18 +96,14 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
86
96
|
const clicked = await attemptSendButton(runtime);
|
|
87
97
|
if (!clicked) {
|
|
88
98
|
await input.dispatchKeyEvent({
|
|
89
|
-
type: '
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
nativeVirtualKeyCode: 13,
|
|
99
|
+
type: 'keyDown',
|
|
100
|
+
...ENTER_KEY_EVENT,
|
|
101
|
+
text: ENTER_KEY_TEXT,
|
|
102
|
+
unmodifiedText: ENTER_KEY_TEXT,
|
|
94
103
|
});
|
|
95
104
|
await input.dispatchKeyEvent({
|
|
96
105
|
type: 'keyUp',
|
|
97
|
-
|
|
98
|
-
code: 'Enter',
|
|
99
|
-
windowsVirtualKeyCode: 13,
|
|
100
|
-
nativeVirtualKeyCode: 13,
|
|
106
|
+
...ENTER_KEY_EVENT,
|
|
101
107
|
});
|
|
102
108
|
logger('Submitted prompt via Enter key');
|
|
103
109
|
}
|
|
@@ -105,19 +111,28 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
105
111
|
logger('Clicked send button');
|
|
106
112
|
}
|
|
107
113
|
await verifyPromptCommitted(runtime, prompt, 30_000, logger);
|
|
114
|
+
await clickAnswerNowIfPresent(runtime, logger);
|
|
108
115
|
}
|
|
109
116
|
async function attemptSendButton(Runtime) {
|
|
110
117
|
const script = `(() => {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
const selectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
119
|
+
let button = null;
|
|
120
|
+
for (const selector of selectors) {
|
|
121
|
+
button = document.querySelector(selector);
|
|
122
|
+
if (button) break;
|
|
114
123
|
}
|
|
124
|
+
if (!button) return 'missing';
|
|
115
125
|
const ariaDisabled = button.getAttribute('aria-disabled');
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
const dataDisabled = button.getAttribute('data-disabled');
|
|
127
|
+
const style = window.getComputedStyle(button);
|
|
128
|
+
const disabled =
|
|
129
|
+
button.hasAttribute('disabled') ||
|
|
130
|
+
ariaDisabled === 'true' ||
|
|
131
|
+
dataDisabled === 'true' ||
|
|
132
|
+
style.pointerEvents === 'none' ||
|
|
133
|
+
style.display === 'none';
|
|
134
|
+
if (disabled) return 'disabled';
|
|
135
|
+
(button as HTMLElement).click();
|
|
121
136
|
return 'clicked';
|
|
122
137
|
})()`;
|
|
123
138
|
const deadline = Date.now() + 2_000;
|
|
@@ -133,6 +148,40 @@ async function attemptSendButton(Runtime) {
|
|
|
133
148
|
}
|
|
134
149
|
return false;
|
|
135
150
|
}
|
|
151
|
+
async function clickAnswerNowIfPresent(Runtime, logger) {
|
|
152
|
+
const script = `(() => {
|
|
153
|
+
const matchesText = (el) => (el?.textContent || '').trim().toLowerCase() === 'answer now';
|
|
154
|
+
const candidate = Array.from(document.querySelectorAll('button,span')).find(matchesText);
|
|
155
|
+
if (!candidate) return 'missing';
|
|
156
|
+
const button = candidate.closest('button') ?? candidate;
|
|
157
|
+
const style = window.getComputedStyle(button);
|
|
158
|
+
const disabled =
|
|
159
|
+
button.hasAttribute('disabled') ||
|
|
160
|
+
button.getAttribute('aria-disabled') === 'true' ||
|
|
161
|
+
style.pointerEvents === 'none' ||
|
|
162
|
+
style.display === 'none';
|
|
163
|
+
if (disabled) return 'disabled';
|
|
164
|
+
(button).dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, cancelable: true }));
|
|
165
|
+
(button).dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
|
|
166
|
+
(button).dispatchEvent(new MouseEvent('pointerup', { bubbles: true, cancelable: true }));
|
|
167
|
+
(button).dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
|
|
168
|
+
(button).dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
169
|
+
return 'clicked';
|
|
170
|
+
})()`;
|
|
171
|
+
const deadline = Date.now() + 3_000;
|
|
172
|
+
while (Date.now() < deadline) {
|
|
173
|
+
const { result } = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
174
|
+
const status = result.value;
|
|
175
|
+
if (status === 'clicked') {
|
|
176
|
+
logger?.('Clicked "Answer now" gate');
|
|
177
|
+
await delay(500);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (status === 'missing')
|
|
181
|
+
return;
|
|
182
|
+
await delay(100);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
136
185
|
async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger) {
|
|
137
186
|
const deadline = Date.now() + timeoutMs;
|
|
138
187
|
const encodedPrompt = JSON.stringify(prompt.trim());
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { FILE_INPUT_SELECTORS } from '../constants.js';
|
|
4
4
|
import { delay } from '../utils.js';
|
|
5
5
|
import { logDomFailure } from '../domDebug.js';
|
|
6
6
|
/**
|
|
@@ -25,9 +25,8 @@ export async function uploadAttachmentViaDataTransfer(deps, attachment, logger)
|
|
|
25
25
|
logger(`Transferring ${fileName} (${fileContent.length} bytes) to remote browser...`);
|
|
26
26
|
// Find file input element
|
|
27
27
|
const documentNode = await dom.getDocument();
|
|
28
|
-
const selectors = [FILE_INPUT_SELECTOR, GENERIC_FILE_INPUT_SELECTOR];
|
|
29
28
|
let fileInputSelector;
|
|
30
|
-
for (const selector of
|
|
29
|
+
for (const selector of FILE_INPUT_SELECTORS) {
|
|
31
30
|
const result = await dom.querySelector({ nodeId: documentNode.root.nodeId, selector });
|
|
32
31
|
if (result.nodeId) {
|
|
33
32
|
fileInputSelector = selector;
|
|
@@ -74,7 +73,40 @@ export async function uploadAttachmentViaDataTransfer(deps, attachment, logger)
|
|
|
74
73
|
// Create DataTransfer and assign to input
|
|
75
74
|
const dataTransfer = new DataTransfer();
|
|
76
75
|
dataTransfer.items.add(file);
|
|
77
|
-
|
|
76
|
+
let assigned = false;
|
|
77
|
+
|
|
78
|
+
const proto = Object.getPrototypeOf(fileInput);
|
|
79
|
+
const descriptor = proto ? Object.getOwnPropertyDescriptor(proto, 'files') : null;
|
|
80
|
+
if (descriptor?.set) {
|
|
81
|
+
try {
|
|
82
|
+
descriptor.set.call(fileInput, dataTransfer.files);
|
|
83
|
+
assigned = true;
|
|
84
|
+
} catch {
|
|
85
|
+
assigned = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!assigned) {
|
|
89
|
+
try {
|
|
90
|
+
Object.defineProperty(fileInput, 'files', {
|
|
91
|
+
configurable: true,
|
|
92
|
+
get: () => dataTransfer.files,
|
|
93
|
+
});
|
|
94
|
+
assigned = true;
|
|
95
|
+
} catch {
|
|
96
|
+
assigned = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!assigned) {
|
|
100
|
+
try {
|
|
101
|
+
fileInput.files = dataTransfer.files;
|
|
102
|
+
assigned = true;
|
|
103
|
+
} catch {
|
|
104
|
+
assigned = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!assigned) {
|
|
108
|
+
return { success: false, error: 'Unable to assign FileList to input' };
|
|
109
|
+
}
|
|
78
110
|
|
|
79
111
|
// Trigger both input and change events for better compatibility
|
|
80
112
|
fileInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
@@ -2,14 +2,17 @@ import path from 'node:path';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
5
6
|
import chromeCookies from 'chrome-cookies-secure';
|
|
6
7
|
import { COOKIE_URLS } from './constants.js';
|
|
7
|
-
import './keytarShim.js';
|
|
8
8
|
import { ensureCookiesDirForFallback } from './windowsCookies.js';
|
|
9
9
|
const COOKIE_READ_TIMEOUT_MS = readDuration('ORACLE_COOKIE_LOAD_TIMEOUT_MS', 5_000);
|
|
10
10
|
const KEYCHAIN_PROBE_TIMEOUT_MS = readDuration('ORACLE_KEYCHAIN_PROBE_TIMEOUT_MS', 3_000);
|
|
11
11
|
const MAC_KEYCHAIN_LABELS = loadKeychainLabels();
|
|
12
12
|
export async function loadChromeCookies({ targetUrl, profile, explicitCookiePath, filterNames, }) {
|
|
13
|
+
if (process.platform === 'win32') {
|
|
14
|
+
throw new Error('Cookie sync is disabled on Windows; use --browser-manual-login (persistent profile) or inline cookies instead.');
|
|
15
|
+
}
|
|
13
16
|
const urlsToCheck = Array.from(new Set([stripQuery(targetUrl), ...COOKIE_URLS]));
|
|
14
17
|
const merged = new Map();
|
|
15
18
|
const cookieFile = await resolveCookieFilePath({ explicitPath: explicitCookiePath, profile });
|
|
@@ -49,13 +52,26 @@ export async function loadChromeCookies({ targetUrl, profile, explicitCookiePath
|
|
|
49
52
|
return Array.from(merged.values());
|
|
50
53
|
}
|
|
51
54
|
async function ensureMacKeychainReadable() {
|
|
52
|
-
if (process.platform
|
|
55
|
+
if (process.platform === 'win32') {
|
|
53
56
|
return;
|
|
54
57
|
}
|
|
55
|
-
// chrome-cookies-secure can hang forever when
|
|
56
|
-
// Probe the
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
// chrome-cookies-secure can hang forever when the platform keyring rejects access (e.g., SSH/no GUI).
|
|
59
|
+
// Probe the keyring ourselves with a timeout so callers fail fast instead of blocking the run.
|
|
60
|
+
let keytar = null;
|
|
61
|
+
let keytarPath = null;
|
|
62
|
+
try {
|
|
63
|
+
const require = createRequire(import.meta.url);
|
|
64
|
+
keytarPath = require.resolve('keytar');
|
|
65
|
+
const keytarModule = await import('keytar');
|
|
66
|
+
keytar = (keytarModule.default ?? keytarModule);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const base = error instanceof Error ? error.message : String(error);
|
|
70
|
+
const rebuildHint = keytarPath
|
|
71
|
+
? ` You may need to rebuild keytar: PYTHON=/usr/bin/python3 /Users/steipete/Projects/oracle/runner npx node-gyp rebuild (run inside ${path.dirname(keytarPath)}).`
|
|
72
|
+
: '';
|
|
73
|
+
throw new Error(`Failed to load keytar for secure cookie copy (${base}). Install keyring deps (macOS Keychain / libsecret) or rerun with --render --copy to paste into ChatGPT manually.${rebuildHint}`);
|
|
74
|
+
}
|
|
59
75
|
const password = await settleWithTimeout(findKeychainPassword(keytar, MAC_KEYCHAIN_LABELS), KEYCHAIN_PROBE_TIMEOUT_MS, `Timed out reading macOS Keychain while looking up Chrome Safe Storage (after ${KEYCHAIN_PROBE_TIMEOUT_MS} ms). Unlock the login keychain or start oracle serve from a GUI session.`);
|
|
60
76
|
if (!password) {
|
|
61
77
|
throw new Error('macOS Keychain denied access to Chrome cookies. Unlock the login keychain or run oracle serve from a GUI session, then retry.');
|
|
@@ -204,6 +220,23 @@ async function defaultProfileRoot() {
|
|
|
204
220
|
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome'), path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge'), path.join(os.homedir(), 'Library', 'Application Support', 'Chromium'));
|
|
205
221
|
}
|
|
206
222
|
else if (process.platform === 'linux') {
|
|
223
|
+
if (isWsl()) {
|
|
224
|
+
const windowsHomes = [process.env.USERPROFILE, process.env.WIN_HOME, process.env.HOME?.replace('/home', '/mnt/c/Users')]
|
|
225
|
+
.filter((p) => Boolean(p))
|
|
226
|
+
.map((p) => p.replace(/\\/g, '/'));
|
|
227
|
+
const wslCandidates = [];
|
|
228
|
+
for (const home of windowsHomes) {
|
|
229
|
+
const normalized = home.startsWith('/mnt/') ? home : `/mnt/c/Users/${path.basename(home)}`;
|
|
230
|
+
wslCandidates.push(path.join(normalized, 'AppData', 'Local', 'Google', 'Chrome', 'User Data'), path.join(normalized, 'AppData', 'Local', 'Microsoft', 'Edge', 'User Data'));
|
|
231
|
+
}
|
|
232
|
+
// Ensure we don't pick a non-user Default profile ahead of real ones.
|
|
233
|
+
for (const candidate of wslCandidates) {
|
|
234
|
+
const hasProfile = existsSync(path.join(candidate, 'Default'));
|
|
235
|
+
if (hasProfile) {
|
|
236
|
+
candidates.push(candidate);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
207
240
|
candidates.push(path.join(os.homedir(), '.config', 'google-chrome'), path.join(os.homedir(), '.config', 'microsoft-edge'), path.join(os.homedir(), '.config', 'chromium'),
|
|
208
241
|
// Snap Chromium profiles
|
|
209
242
|
path.join(os.homedir(), 'snap', 'chromium', 'common', 'chromium'), path.join(os.homedir(), 'snap', 'chromium', 'current', 'chromium'));
|
|
@@ -241,6 +274,11 @@ function readDuration(envKey, fallback) {
|
|
|
241
274
|
const parsed = Number.parseInt(raw, 10);
|
|
242
275
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
243
276
|
}
|
|
277
|
+
function isWsl() {
|
|
278
|
+
if (process.platform !== 'linux')
|
|
279
|
+
return false;
|
|
280
|
+
return Boolean(process.env.WSL_DISTRO_NAME || os.release().toLowerCase().includes('microsoft'));
|
|
281
|
+
}
|
|
244
282
|
function loadKeychainLabels() {
|
|
245
283
|
const defaults = [
|
|
246
284
|
{ service: 'Chrome Safe Storage', account: 'Chrome' },
|