@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,71 @@
|
|
|
1
|
+
export const CHATGPT_URL = 'https://chatgpt.com/';
|
|
2
|
+
export const DEFAULT_MODEL_TARGET = 'GPT-5.2 Pro';
|
|
3
|
+
export const DEFAULT_MODEL_STRATEGY = 'select';
|
|
4
|
+
export const COOKIE_URLS = ['https://chatgpt.com', 'https://chat.openai.com', 'https://atlas.openai.com'];
|
|
5
|
+
export const INPUT_SELECTORS = [
|
|
6
|
+
'textarea[data-id="prompt-textarea"]',
|
|
7
|
+
'textarea[placeholder*="Send a message"]',
|
|
8
|
+
'textarea[aria-label="Message ChatGPT"]',
|
|
9
|
+
'textarea:not([disabled])',
|
|
10
|
+
'textarea[name="prompt-textarea"]',
|
|
11
|
+
'#prompt-textarea',
|
|
12
|
+
'.ProseMirror',
|
|
13
|
+
'[contenteditable="true"][data-virtualkeyboard="true"]',
|
|
14
|
+
];
|
|
15
|
+
export const ANSWER_SELECTORS = [
|
|
16
|
+
'article[data-testid^="conversation-turn"][data-message-author-role="assistant"]',
|
|
17
|
+
'article[data-testid^="conversation-turn"][data-turn="assistant"]',
|
|
18
|
+
'article[data-testid^="conversation-turn"] [data-message-author-role="assistant"]',
|
|
19
|
+
'article[data-testid^="conversation-turn"] [data-turn="assistant"]',
|
|
20
|
+
'article[data-testid^="conversation-turn"] .markdown',
|
|
21
|
+
'[data-message-author-role="assistant"] .markdown',
|
|
22
|
+
'[data-turn="assistant"] .markdown',
|
|
23
|
+
'[data-message-author-role="assistant"]',
|
|
24
|
+
'[data-turn="assistant"]',
|
|
25
|
+
];
|
|
26
|
+
export const CONVERSATION_TURN_SELECTOR = 'article[data-testid^="conversation-turn"], div[data-testid^="conversation-turn"], section[data-testid^="conversation-turn"], ' +
|
|
27
|
+
'article[data-message-author-role], div[data-message-author-role], section[data-message-author-role], ' +
|
|
28
|
+
'article[data-turn], div[data-turn], section[data-turn]';
|
|
29
|
+
export const ASSISTANT_ROLE_SELECTOR = '[data-message-author-role="assistant"], [data-turn="assistant"]';
|
|
30
|
+
export const CLOUDFLARE_SCRIPT_SELECTOR = 'script[src*="/challenge-platform/"]';
|
|
31
|
+
export const CLOUDFLARE_TITLE = 'just a moment';
|
|
32
|
+
export const PROMPT_PRIMARY_SELECTOR = '#prompt-textarea';
|
|
33
|
+
export const PROMPT_FALLBACK_SELECTOR = 'textarea[name="prompt-textarea"]';
|
|
34
|
+
export const FILE_INPUT_SELECTORS = [
|
|
35
|
+
'form input[type="file"]:not([accept])',
|
|
36
|
+
'input[type="file"][multiple]:not([accept])',
|
|
37
|
+
'input[type="file"][multiple]',
|
|
38
|
+
'input[type="file"]:not([accept])',
|
|
39
|
+
'form input[type="file"][accept]',
|
|
40
|
+
'input[type="file"][accept]',
|
|
41
|
+
'input[type="file"]',
|
|
42
|
+
'input[type="file"][data-testid*="file"]',
|
|
43
|
+
];
|
|
44
|
+
// Legacy single selectors kept for compatibility with older call-sites
|
|
45
|
+
export const FILE_INPUT_SELECTOR = FILE_INPUT_SELECTORS[0];
|
|
46
|
+
export const GENERIC_FILE_INPUT_SELECTOR = FILE_INPUT_SELECTORS[3];
|
|
47
|
+
export const MENU_CONTAINER_SELECTOR = '[role="menu"], [data-radix-collection-root]';
|
|
48
|
+
export const MENU_ITEM_SELECTOR = 'button, [role="menuitem"], [role="menuitemradio"], [data-testid*="model-switcher-"]';
|
|
49
|
+
export const UPLOAD_STATUS_SELECTORS = [
|
|
50
|
+
'[data-testid*="upload"]',
|
|
51
|
+
'[data-testid*="attachment"]',
|
|
52
|
+
'[data-testid*="progress"]',
|
|
53
|
+
'[data-state="loading"]',
|
|
54
|
+
'[data-state="uploading"]',
|
|
55
|
+
'[data-state="pending"]',
|
|
56
|
+
'[aria-live="polite"]',
|
|
57
|
+
'[aria-live="assertive"]',
|
|
58
|
+
];
|
|
59
|
+
export const STOP_BUTTON_SELECTOR = '[data-testid="stop-button"]';
|
|
60
|
+
export const SEND_BUTTON_SELECTORS = [
|
|
61
|
+
'button[data-testid="send-button"]',
|
|
62
|
+
'button[data-testid*="composer-send"]',
|
|
63
|
+
'form button[type="submit"]',
|
|
64
|
+
'button[type="submit"][data-testid*="send"]',
|
|
65
|
+
'button[aria-label*="Send"]',
|
|
66
|
+
];
|
|
67
|
+
export const SEND_BUTTON_SELECTOR = SEND_BUTTON_SELECTORS[0];
|
|
68
|
+
export const MODEL_BUTTON_SELECTOR = '[data-testid="model-switcher-dropdown-button"]';
|
|
69
|
+
export const COPY_BUTTON_SELECTOR = 'button[data-testid="copy-turn-action-button"]';
|
|
70
|
+
// Action buttons that only appear once a turn has finished rendering.
|
|
71
|
+
export const FINISHED_ACTIONS_SELECTOR = 'button[data-testid="copy-turn-action-button"], button[data-testid="good-response-turn-action-button"], button[data-testid="bad-response-turn-action-button"], button[aria-label="Share"]';
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { COOKIE_URLS } from './constants.js';
|
|
2
|
+
import { delay } from './utils.js';
|
|
3
|
+
import { getCookies } from '@steipete/sweet-cookie';
|
|
4
|
+
export class ChromeCookieSyncError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export async function syncCookies(Network, url, profile, logger, options = {}) {
|
|
7
|
+
const { allowErrors = false, filterNames, inlineCookies, cookiePath, waitMs = 0 } = options;
|
|
8
|
+
try {
|
|
9
|
+
// Learned: inline cookies are the most deterministic (avoid Keychain + profile ambiguity).
|
|
10
|
+
const cookies = inlineCookies?.length
|
|
11
|
+
? normalizeInlineCookies(inlineCookies, new URL(url).hostname)
|
|
12
|
+
: await readChromeCookiesWithWait(url, profile, filterNames ?? undefined, cookiePath ?? undefined, waitMs, logger);
|
|
13
|
+
if (!cookies.length) {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
let applied = 0;
|
|
17
|
+
for (const cookie of cookies) {
|
|
18
|
+
const cookieWithUrl = attachUrl(cookie, url);
|
|
19
|
+
try {
|
|
20
|
+
// Learned: CDP will silently drop cookies without a url; always attach one.
|
|
21
|
+
const result = await Network.setCookie(cookieWithUrl);
|
|
22
|
+
if (result?.success) {
|
|
23
|
+
applied += 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28
|
+
logger(`Failed to set cookie ${cookie.name}: ${message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return applied;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
+
if (allowErrors) {
|
|
36
|
+
logger(`Cookie sync failed (continuing with override): ${message}`);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
throw error instanceof ChromeCookieSyncError ? error : new ChromeCookieSyncError(message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function readChromeCookiesWithWait(url, profile, filterNames, cookiePath, waitMs, logger) {
|
|
43
|
+
if (waitMs <= 0) {
|
|
44
|
+
return readChromeCookies(url, profile, filterNames, cookiePath);
|
|
45
|
+
}
|
|
46
|
+
let cookies = [];
|
|
47
|
+
let firstError;
|
|
48
|
+
try {
|
|
49
|
+
cookies = await readChromeCookies(url, profile, filterNames, cookiePath);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
firstError = error;
|
|
53
|
+
}
|
|
54
|
+
if (cookies.length > 0 && !firstError) {
|
|
55
|
+
return cookies;
|
|
56
|
+
}
|
|
57
|
+
const waitLabel = waitMs >= 1000 ? `${Math.round(waitMs / 1000)}s` : `${waitMs}ms`;
|
|
58
|
+
const message = firstError instanceof Error ? firstError.message : String(firstError ?? '');
|
|
59
|
+
if (firstError) {
|
|
60
|
+
logger(`[cookies] Cookie read failed (${message}); waiting ${waitLabel} then retrying once.`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
logger(`[cookies] No cookies found; waiting ${waitLabel} then retrying once.`);
|
|
64
|
+
}
|
|
65
|
+
await delay(waitMs);
|
|
66
|
+
return readChromeCookies(url, profile, filterNames, cookiePath);
|
|
67
|
+
}
|
|
68
|
+
async function readChromeCookies(url, profile, filterNames, cookiePath) {
|
|
69
|
+
const origins = Array.from(new Set([stripQuery(url), ...COOKIE_URLS]));
|
|
70
|
+
const chromeProfile = cookiePath ?? profile ?? undefined;
|
|
71
|
+
const timeoutMs = readDuration('ORACLE_COOKIE_LOAD_TIMEOUT_MS', 5_000);
|
|
72
|
+
// Learned: read from multiple origins to capture auth cookies that land on chat.openai.com + atlas.
|
|
73
|
+
const { cookies, warnings } = await getCookies({
|
|
74
|
+
url,
|
|
75
|
+
origins,
|
|
76
|
+
names: filterNames?.length ? filterNames : undefined,
|
|
77
|
+
browsers: ['chrome'],
|
|
78
|
+
mode: 'merge',
|
|
79
|
+
chromeProfile,
|
|
80
|
+
timeoutMs,
|
|
81
|
+
});
|
|
82
|
+
if (process.env.ORACLE_DEBUG_COOKIES === '1' && warnings.length) {
|
|
83
|
+
// eslint-disable-next-line no-console
|
|
84
|
+
console.log(`[cookies] sweet-cookie warnings:\n- ${warnings.join('\n- ')}`);
|
|
85
|
+
}
|
|
86
|
+
const merged = new Map();
|
|
87
|
+
for (const cookie of cookies) {
|
|
88
|
+
const normalized = toCdpCookie(cookie);
|
|
89
|
+
if (!normalized)
|
|
90
|
+
continue;
|
|
91
|
+
const key = `${normalized.domain ?? ''}:${normalized.name}`;
|
|
92
|
+
if (!merged.has(key))
|
|
93
|
+
merged.set(key, normalized);
|
|
94
|
+
}
|
|
95
|
+
return Array.from(merged.values());
|
|
96
|
+
}
|
|
97
|
+
function normalizeInlineCookies(rawCookies, fallbackHost) {
|
|
98
|
+
const merged = new Map();
|
|
99
|
+
for (const cookie of rawCookies) {
|
|
100
|
+
if (!cookie?.name)
|
|
101
|
+
continue;
|
|
102
|
+
// Learned: inline cookies may omit url/domain; default to current host with a safe path.
|
|
103
|
+
const normalized = {
|
|
104
|
+
name: cookie.name,
|
|
105
|
+
value: cookie.value ?? '',
|
|
106
|
+
url: cookie.url,
|
|
107
|
+
domain: cookie.domain ?? fallbackHost,
|
|
108
|
+
path: cookie.path ?? '/',
|
|
109
|
+
expires: normalizeExpiration(cookie.expires),
|
|
110
|
+
secure: cookie.secure ?? true,
|
|
111
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
112
|
+
sameSite: cookie.sameSite,
|
|
113
|
+
};
|
|
114
|
+
const key = `${normalized.domain ?? fallbackHost}:${normalized.name}`;
|
|
115
|
+
if (!merged.has(key)) {
|
|
116
|
+
merged.set(key, normalized);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return Array.from(merged.values());
|
|
120
|
+
}
|
|
121
|
+
function toCdpCookie(cookie) {
|
|
122
|
+
if (!cookie?.name)
|
|
123
|
+
return null;
|
|
124
|
+
const out = {
|
|
125
|
+
name: cookie.name,
|
|
126
|
+
value: cookie.value,
|
|
127
|
+
domain: cookie.domain,
|
|
128
|
+
path: cookie.path ?? '/',
|
|
129
|
+
secure: cookie.secure ?? true,
|
|
130
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
131
|
+
};
|
|
132
|
+
if (typeof cookie.expires === 'number')
|
|
133
|
+
out.expires = cookie.expires;
|
|
134
|
+
if (cookie.sameSite === 'Lax' || cookie.sameSite === 'Strict' || cookie.sameSite === 'None') {
|
|
135
|
+
out.sameSite = cookie.sameSite;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
function attachUrl(cookie, fallbackUrl) {
|
|
140
|
+
const cookieWithUrl = { ...cookie };
|
|
141
|
+
if (!cookieWithUrl.url) {
|
|
142
|
+
if (!cookieWithUrl.domain || cookieWithUrl.domain === 'localhost') {
|
|
143
|
+
cookieWithUrl.url = fallbackUrl;
|
|
144
|
+
}
|
|
145
|
+
else if (!cookieWithUrl.domain.startsWith('.')) {
|
|
146
|
+
cookieWithUrl.url = `https://${cookieWithUrl.domain}`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// When url is present, let Chrome derive the host from it; keeping domain can trigger CDP sanitization errors.
|
|
150
|
+
if (cookieWithUrl.url) {
|
|
151
|
+
// Learned: CDP rejects cookies with both url + domain in some cases; drop domain to avoid failures.
|
|
152
|
+
delete cookieWithUrl.domain;
|
|
153
|
+
}
|
|
154
|
+
return cookieWithUrl;
|
|
155
|
+
}
|
|
156
|
+
function stripQuery(url) {
|
|
157
|
+
try {
|
|
158
|
+
const parsed = new URL(url);
|
|
159
|
+
parsed.hash = '';
|
|
160
|
+
parsed.search = '';
|
|
161
|
+
return parsed.toString();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return url;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function normalizeExpiration(expires) {
|
|
168
|
+
if (!expires || Number.isNaN(expires)) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const value = Number(expires);
|
|
172
|
+
if (value <= 0) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
if (value > 1_000_000_000_000) {
|
|
176
|
+
// Learned: Chrome may store WebKit microseconds since 1601; convert to Unix seconds.
|
|
177
|
+
return Math.round(value / 1_000_000 - 11644473600);
|
|
178
|
+
}
|
|
179
|
+
if (value > 1_000_000_000) {
|
|
180
|
+
// Likely milliseconds; normalize to seconds for CDP.
|
|
181
|
+
return Math.round(value / 1000);
|
|
182
|
+
}
|
|
183
|
+
return Math.round(value);
|
|
184
|
+
}
|
|
185
|
+
function readDuration(envKey, defaultValueMs) {
|
|
186
|
+
const raw = process.env[envKey];
|
|
187
|
+
if (!raw)
|
|
188
|
+
return defaultValueMs;
|
|
189
|
+
const value = Number.parseInt(raw, 10);
|
|
190
|
+
return Number.isFinite(value) && value > 0 ? value : defaultValueMs;
|
|
191
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Launcher } from 'chrome-launcher';
|
|
5
|
+
export async function detectChromeBinary() {
|
|
6
|
+
const envPath = (process.env.CHROME_PATH ?? '').trim();
|
|
7
|
+
if (envPath) {
|
|
8
|
+
const ok = await isExecutable(envPath);
|
|
9
|
+
if (ok) {
|
|
10
|
+
return { path: envPath };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const launcherDetected = Launcher.getFirstInstallation();
|
|
14
|
+
if (launcherDetected) {
|
|
15
|
+
return { path: launcherDetected };
|
|
16
|
+
}
|
|
17
|
+
const candidates = platformChromeCandidates();
|
|
18
|
+
for (const candidate of candidates.absolutePaths) {
|
|
19
|
+
if (await isExecutable(candidate)) {
|
|
20
|
+
return { path: candidate };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const fromPath = await findOnPath(candidates.binaryNames);
|
|
24
|
+
if (fromPath) {
|
|
25
|
+
return { path: fromPath };
|
|
26
|
+
}
|
|
27
|
+
return { path: null };
|
|
28
|
+
}
|
|
29
|
+
export async function detectChromeCookieDb({ profile }) {
|
|
30
|
+
const profileName = profile?.trim() ? profile.trim() : 'Default';
|
|
31
|
+
if (process.platform === 'win32') {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const roots = platformProfileRoots();
|
|
35
|
+
for (const root of roots) {
|
|
36
|
+
const dir = path.join(root, profileName);
|
|
37
|
+
const direct = path.join(dir, 'Cookies');
|
|
38
|
+
if (await isFile(direct))
|
|
39
|
+
return direct;
|
|
40
|
+
const network = path.join(dir, 'Network', 'Cookies');
|
|
41
|
+
if (await isFile(network))
|
|
42
|
+
return network;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function platformChromeCandidates() {
|
|
47
|
+
if (process.platform === 'linux') {
|
|
48
|
+
return {
|
|
49
|
+
binaryNames: [
|
|
50
|
+
'google-chrome',
|
|
51
|
+
'google-chrome-stable',
|
|
52
|
+
'chromium',
|
|
53
|
+
'chromium-browser',
|
|
54
|
+
'brave-browser',
|
|
55
|
+
'microsoft-edge',
|
|
56
|
+
'microsoft-edge-stable',
|
|
57
|
+
],
|
|
58
|
+
absolutePaths: [
|
|
59
|
+
'/usr/bin/google-chrome',
|
|
60
|
+
'/usr/bin/google-chrome-stable',
|
|
61
|
+
'/usr/bin/google-chrome-beta',
|
|
62
|
+
'/usr/bin/google-chrome-unstable',
|
|
63
|
+
'/usr/bin/chromium',
|
|
64
|
+
'/usr/bin/chromium-browser',
|
|
65
|
+
'/usr/bin/brave-browser',
|
|
66
|
+
'/usr/bin/microsoft-edge',
|
|
67
|
+
'/usr/bin/microsoft-edge-stable',
|
|
68
|
+
'/snap/bin/chromium',
|
|
69
|
+
'/snap/bin/brave',
|
|
70
|
+
'/snap/bin/brave-browser',
|
|
71
|
+
'/snap/bin/microsoft-edge',
|
|
72
|
+
'/opt/google/chrome/chrome',
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (process.platform === 'darwin') {
|
|
77
|
+
return {
|
|
78
|
+
binaryNames: [],
|
|
79
|
+
absolutePaths: [
|
|
80
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
81
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
82
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
83
|
+
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (process.platform === 'win32') {
|
|
88
|
+
const programFiles = process.env.ProgramFiles ?? 'C:\\Program Files';
|
|
89
|
+
const programFilesX86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)';
|
|
90
|
+
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
|
91
|
+
return {
|
|
92
|
+
binaryNames: [],
|
|
93
|
+
absolutePaths: [
|
|
94
|
+
path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
95
|
+
path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
96
|
+
path.join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
97
|
+
path.join(programFiles, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
98
|
+
path.join(programFilesX86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return { binaryNames: [], absolutePaths: [] };
|
|
103
|
+
}
|
|
104
|
+
function platformProfileRoots() {
|
|
105
|
+
const home = os.homedir();
|
|
106
|
+
if (process.platform === 'darwin') {
|
|
107
|
+
return [
|
|
108
|
+
path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'),
|
|
109
|
+
path.join(home, 'Library', 'Application Support', 'Chromium'),
|
|
110
|
+
path.join(home, 'Library', 'Application Support', 'Microsoft Edge'),
|
|
111
|
+
path.join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser'),
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
if (process.platform === 'linux') {
|
|
115
|
+
return [
|
|
116
|
+
path.join(home, '.config', 'google-chrome'),
|
|
117
|
+
path.join(home, '.config', 'google-chrome-beta'),
|
|
118
|
+
path.join(home, '.config', 'google-chrome-unstable'),
|
|
119
|
+
path.join(home, '.config', 'chromium'),
|
|
120
|
+
path.join(home, '.config', 'microsoft-edge'),
|
|
121
|
+
path.join(home, '.config', 'BraveSoftware', 'Brave-Browser'),
|
|
122
|
+
// Snap Chromium profiles
|
|
123
|
+
path.join(home, 'snap', 'chromium', 'common', 'chromium'),
|
|
124
|
+
path.join(home, 'snap', 'chromium', 'current', 'chromium'),
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
async function isExecutable(candidate) {
|
|
130
|
+
try {
|
|
131
|
+
const stat = await fs.stat(candidate);
|
|
132
|
+
if (!stat.isFile())
|
|
133
|
+
return false;
|
|
134
|
+
if (process.platform === 'win32')
|
|
135
|
+
return true;
|
|
136
|
+
// eslint-disable-next-line no-bitwise
|
|
137
|
+
return (stat.mode & 0o111) !== 0;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function isFile(candidate) {
|
|
144
|
+
try {
|
|
145
|
+
const stat = await fs.stat(candidate);
|
|
146
|
+
return stat.isFile();
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function findOnPath(names) {
|
|
153
|
+
const rawPath = process.env.PATH ?? '';
|
|
154
|
+
const dirs = rawPath.split(path.delimiter).filter(Boolean);
|
|
155
|
+
for (const name of names) {
|
|
156
|
+
for (const dir of dirs) {
|
|
157
|
+
const candidate = path.join(dir, name);
|
|
158
|
+
if (await isExecutable(candidate)) {
|
|
159
|
+
return candidate;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CONVERSATION_TURN_SELECTOR } from './constants.js';
|
|
2
|
+
export function buildConversationDebugExpression() {
|
|
3
|
+
return `(() => {
|
|
4
|
+
const CONVERSATION_SELECTOR = ${JSON.stringify(CONVERSATION_TURN_SELECTOR)};
|
|
5
|
+
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
6
|
+
return turns.map((node) => ({
|
|
7
|
+
role: node.getAttribute('data-message-author-role'),
|
|
8
|
+
text: node.innerText?.slice(0, 200),
|
|
9
|
+
testid: node.getAttribute('data-testid'),
|
|
10
|
+
}));
|
|
11
|
+
})()`;
|
|
12
|
+
}
|
|
13
|
+
export async function logConversationSnapshot(Runtime, logger) {
|
|
14
|
+
const expression = buildConversationDebugExpression();
|
|
15
|
+
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
16
|
+
if (Array.isArray(result.value)) {
|
|
17
|
+
const recent = result.value.slice(-3);
|
|
18
|
+
logger(`Conversation snapshot: ${JSON.stringify(recent)}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function logDomFailure(Runtime, logger, context) {
|
|
22
|
+
if (!logger?.verbose) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const entry = `Browser automation failure (${context}); capturing DOM snapshot for debugging...`;
|
|
27
|
+
logger(entry);
|
|
28
|
+
if (logger.sessionLog && logger.sessionLog !== logger) {
|
|
29
|
+
logger.sessionLog(entry);
|
|
30
|
+
}
|
|
31
|
+
await logConversationSnapshot(Runtime, logger);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// ignore snapshot failures
|
|
35
|
+
}
|
|
36
|
+
}
|