@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
|
@@ -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);
|
|
@@ -86,18 +93,14 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
86
93
|
const clicked = await attemptSendButton(runtime);
|
|
87
94
|
if (!clicked) {
|
|
88
95
|
await input.dispatchKeyEvent({
|
|
89
|
-
type: '
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
nativeVirtualKeyCode: 13,
|
|
96
|
+
type: 'keyDown',
|
|
97
|
+
...ENTER_KEY_EVENT,
|
|
98
|
+
text: ENTER_KEY_TEXT,
|
|
99
|
+
unmodifiedText: ENTER_KEY_TEXT,
|
|
94
100
|
});
|
|
95
101
|
await input.dispatchKeyEvent({
|
|
96
102
|
type: 'keyUp',
|
|
97
|
-
|
|
98
|
-
code: 'Enter',
|
|
99
|
-
windowsVirtualKeyCode: 13,
|
|
100
|
-
nativeVirtualKeyCode: 13,
|
|
103
|
+
...ENTER_KEY_EVENT,
|
|
101
104
|
});
|
|
102
105
|
logger('Submitted prompt via Enter key');
|
|
103
106
|
}
|
|
@@ -108,16 +111,24 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
108
111
|
}
|
|
109
112
|
async function attemptSendButton(Runtime) {
|
|
110
113
|
const script = `(() => {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
const selectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
115
|
+
let button = null;
|
|
116
|
+
for (const selector of selectors) {
|
|
117
|
+
button = document.querySelector(selector);
|
|
118
|
+
if (button) break;
|
|
114
119
|
}
|
|
120
|
+
if (!button) return 'missing';
|
|
115
121
|
const ariaDisabled = button.getAttribute('aria-disabled');
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
const dataDisabled = button.getAttribute('data-disabled');
|
|
123
|
+
const style = window.getComputedStyle(button);
|
|
124
|
+
const disabled =
|
|
125
|
+
button.hasAttribute('disabled') ||
|
|
126
|
+
ariaDisabled === 'true' ||
|
|
127
|
+
dataDisabled === 'true' ||
|
|
128
|
+
style.pointerEvents === 'none' ||
|
|
129
|
+
style.display === 'none';
|
|
130
|
+
if (disabled) return 'disabled';
|
|
131
|
+
(button as HTMLElement).click();
|
|
121
132
|
return 'clicked';
|
|
122
133
|
})()`;
|
|
123
134
|
const deadline = Date.now() + 2_000;
|
|
@@ -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 }));
|
|
@@ -4,12 +4,14 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import chromeCookies from 'chrome-cookies-secure';
|
|
6
6
|
import { COOKIE_URLS } from './constants.js';
|
|
7
|
-
import './keytarShim.js';
|
|
8
7
|
import { ensureCookiesDirForFallback } from './windowsCookies.js';
|
|
9
8
|
const COOKIE_READ_TIMEOUT_MS = readDuration('ORACLE_COOKIE_LOAD_TIMEOUT_MS', 5_000);
|
|
10
9
|
const KEYCHAIN_PROBE_TIMEOUT_MS = readDuration('ORACLE_KEYCHAIN_PROBE_TIMEOUT_MS', 3_000);
|
|
11
10
|
const MAC_KEYCHAIN_LABELS = loadKeychainLabels();
|
|
12
11
|
export async function loadChromeCookies({ targetUrl, profile, explicitCookiePath, filterNames, }) {
|
|
12
|
+
if (process.platform === 'win32') {
|
|
13
|
+
throw new Error('Cookie sync is disabled on Windows; use --browser-manual-login (persistent profile) or inline cookies instead.');
|
|
14
|
+
}
|
|
13
15
|
const urlsToCheck = Array.from(new Set([stripQuery(targetUrl), ...COOKIE_URLS]));
|
|
14
16
|
const merged = new Map();
|
|
15
17
|
const cookieFile = await resolveCookieFilePath({ explicitPath: explicitCookiePath, profile });
|
|
@@ -49,13 +51,20 @@ export async function loadChromeCookies({ targetUrl, profile, explicitCookiePath
|
|
|
49
51
|
return Array.from(merged.values());
|
|
50
52
|
}
|
|
51
53
|
async function ensureMacKeychainReadable() {
|
|
52
|
-
if (process.platform
|
|
54
|
+
if (process.platform === 'win32') {
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
55
|
-
// chrome-cookies-secure can hang forever when
|
|
56
|
-
// Probe the
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// chrome-cookies-secure can hang forever when the platform keyring rejects access (e.g., SSH/no GUI).
|
|
58
|
+
// Probe the keyring ourselves with a timeout so callers fail fast instead of blocking the run.
|
|
59
|
+
let keytar = null;
|
|
60
|
+
try {
|
|
61
|
+
const keytarModule = await import('keytar');
|
|
62
|
+
keytar = (keytarModule.default ?? keytarModule);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const base = error instanceof Error ? error.message : String(error);
|
|
66
|
+
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.`);
|
|
67
|
+
}
|
|
59
68
|
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
69
|
if (!password) {
|
|
61
70
|
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 +213,23 @@ async function defaultProfileRoot() {
|
|
|
204
213
|
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
214
|
}
|
|
206
215
|
else if (process.platform === 'linux') {
|
|
216
|
+
if (isWsl()) {
|
|
217
|
+
const windowsHomes = [process.env.USERPROFILE, process.env.WIN_HOME, process.env.HOME?.replace('/home', '/mnt/c/Users')]
|
|
218
|
+
.filter((p) => Boolean(p))
|
|
219
|
+
.map((p) => p.replace(/\\/g, '/'));
|
|
220
|
+
const wslCandidates = [];
|
|
221
|
+
for (const home of windowsHomes) {
|
|
222
|
+
const normalized = home.startsWith('/mnt/') ? home : `/mnt/c/Users/${path.basename(home)}`;
|
|
223
|
+
wslCandidates.push(path.join(normalized, 'AppData', 'Local', 'Google', 'Chrome', 'User Data'), path.join(normalized, 'AppData', 'Local', 'Microsoft', 'Edge', 'User Data'));
|
|
224
|
+
}
|
|
225
|
+
// Ensure we don't pick a non-user Default profile ahead of real ones.
|
|
226
|
+
for (const candidate of wslCandidates) {
|
|
227
|
+
const hasProfile = existsSync(path.join(candidate, 'Default'));
|
|
228
|
+
if (hasProfile) {
|
|
229
|
+
candidates.push(candidate);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
207
233
|
candidates.push(path.join(os.homedir(), '.config', 'google-chrome'), path.join(os.homedir(), '.config', 'microsoft-edge'), path.join(os.homedir(), '.config', 'chromium'),
|
|
208
234
|
// Snap Chromium profiles
|
|
209
235
|
path.join(os.homedir(), 'snap', 'chromium', 'common', 'chromium'), path.join(os.homedir(), 'snap', 'chromium', 'current', 'chromium'));
|
|
@@ -241,6 +267,11 @@ function readDuration(envKey, fallback) {
|
|
|
241
267
|
const parsed = Number.parseInt(raw, 10);
|
|
242
268
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
243
269
|
}
|
|
270
|
+
function isWsl() {
|
|
271
|
+
if (process.platform !== 'linux')
|
|
272
|
+
return false;
|
|
273
|
+
return Boolean(process.env.WSL_DISTRO_NAME || os.release().toLowerCase().includes('microsoft'));
|
|
274
|
+
}
|
|
244
275
|
function loadKeychainLabels() {
|
|
245
276
|
const defaults = [
|
|
246
277
|
{ service: 'Chrome Safe Storage', account: 'Chrome' },
|