@steipete/oracle 0.4.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 +129 -0
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +954 -0
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/bin/oracle.js +683 -0
- package/dist/markdansi/types/index.js +4 -0
- package/dist/oracle/bin/oracle-cli.js +472 -0
- package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
- package/dist/oracle/src/browser/actions/attachments.js +82 -0
- package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
- package/dist/oracle/src/browser/actions/navigation.js +75 -0
- package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
- package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
- package/dist/oracle/src/browser/config.js +33 -0
- package/dist/oracle/src/browser/constants.js +40 -0
- package/dist/oracle/src/browser/cookies.js +210 -0
- package/dist/oracle/src/browser/domDebug.js +36 -0
- package/dist/oracle/src/browser/index.js +331 -0
- package/dist/oracle/src/browser/pageActions.js +5 -0
- package/dist/oracle/src/browser/prompt.js +88 -0
- package/dist/oracle/src/browser/promptSummary.js +20 -0
- package/dist/oracle/src/browser/sessionRunner.js +80 -0
- package/dist/oracle/src/browser/types.js +1 -0
- package/dist/oracle/src/browser/utils.js +62 -0
- package/dist/oracle/src/browserMode.js +1 -0
- package/dist/oracle/src/cli/browserConfig.js +44 -0
- package/dist/oracle/src/cli/dryRun.js +59 -0
- package/dist/oracle/src/cli/engine.js +17 -0
- package/dist/oracle/src/cli/errorUtils.js +9 -0
- package/dist/oracle/src/cli/help.js +70 -0
- package/dist/oracle/src/cli/markdownRenderer.js +15 -0
- package/dist/oracle/src/cli/options.js +103 -0
- package/dist/oracle/src/cli/promptRequirement.js +14 -0
- package/dist/oracle/src/cli/rootAlias.js +30 -0
- package/dist/oracle/src/cli/sessionCommand.js +77 -0
- package/dist/oracle/src/cli/sessionDisplay.js +270 -0
- package/dist/oracle/src/cli/sessionRunner.js +94 -0
- package/dist/oracle/src/heartbeat.js +43 -0
- package/dist/oracle/src/oracle/client.js +48 -0
- package/dist/oracle/src/oracle/config.js +29 -0
- package/dist/oracle/src/oracle/errors.js +101 -0
- package/dist/oracle/src/oracle/files.js +220 -0
- package/dist/oracle/src/oracle/format.js +33 -0
- package/dist/oracle/src/oracle/fsAdapter.js +7 -0
- package/dist/oracle/src/oracle/oscProgress.js +60 -0
- package/dist/oracle/src/oracle/request.js +48 -0
- package/dist/oracle/src/oracle/run.js +444 -0
- package/dist/oracle/src/oracle/tokenStats.js +39 -0
- package/dist/oracle/src/oracle/types.js +1 -0
- package/dist/oracle/src/oracle.js +9 -0
- package/dist/oracle/src/sessionManager.js +205 -0
- package/dist/oracle/src/version.js +39 -0
- package/dist/scripts/browser-tools.js +536 -0
- package/dist/scripts/check.js +21 -0
- package/dist/scripts/chrome/browser-tools.js +295 -0
- package/dist/scripts/run-cli.js +14 -0
- package/dist/src/browser/actions/assistantResponse.js +555 -0
- package/dist/src/browser/actions/attachments.js +82 -0
- package/dist/src/browser/actions/modelSelection.js +300 -0
- package/dist/src/browser/actions/navigation.js +175 -0
- package/dist/src/browser/actions/promptComposer.js +167 -0
- package/dist/src/browser/actions/remoteFileTransfer.js +154 -0
- package/dist/src/browser/chromeCookies.js +274 -0
- package/dist/src/browser/chromeLifecycle.js +107 -0
- package/dist/src/browser/config.js +49 -0
- package/dist/src/browser/constants.js +42 -0
- package/dist/src/browser/cookies.js +130 -0
- package/dist/src/browser/domDebug.js +36 -0
- package/dist/src/browser/index.js +541 -0
- package/dist/src/browser/keytarShim.js +56 -0
- package/dist/src/browser/pageActions.js +5 -0
- package/dist/src/browser/policies.js +43 -0
- package/dist/src/browser/prompt.js +82 -0
- package/dist/src/browser/promptSummary.js +20 -0
- package/dist/src/browser/sessionRunner.js +96 -0
- package/dist/src/browser/types.js +1 -0
- package/dist/src/browser/utils.js +112 -0
- package/dist/src/browser/windowsCookies.js +218 -0
- package/dist/src/browserMode.js +1 -0
- package/dist/src/cli/browserConfig.js +193 -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 +103 -0
- package/dist/src/cli/duplicatePromptGuard.js +14 -0
- package/dist/src/cli/engine.js +25 -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 +300 -0
- package/dist/src/cli/options.js +193 -0
- package/dist/src/cli/oscUtils.js +20 -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 +62 -0
- package/dist/src/cli/sessionCommand.js +111 -0
- package/dist/src/cli/sessionDisplay.js +540 -0
- package/dist/src/cli/sessionRunner.js +419 -0
- package/dist/src/cli/tagline.js +258 -0
- package/dist/src/cli/tui/index.js +520 -0
- package/dist/src/cli/writeOutputPath.js +21 -0
- package/dist/src/config.js +27 -0
- package/dist/src/heartbeat.js +43 -0
- package/dist/src/mcp/server.js +36 -0
- package/dist/src/mcp/tools/consult.js +221 -0
- package/dist/src/mcp/tools/sessionResources.js +75 -0
- package/dist/src/mcp/tools/sessions.js +96 -0
- package/dist/src/mcp/types.js +18 -0
- package/dist/src/mcp/utils.js +27 -0
- package/dist/src/oracle/background.js +134 -0
- package/dist/src/oracle/claude.js +95 -0
- package/dist/src/oracle/client.js +87 -0
- package/dist/src/oracle/config.js +92 -0
- package/dist/src/oracle/errors.js +104 -0
- package/dist/src/oracle/files.js +371 -0
- package/dist/src/oracle/format.js +30 -0
- package/dist/src/oracle/fsAdapter.js +10 -0
- package/dist/src/oracle/gemini.js +185 -0
- package/dist/src/oracle/logging.js +36 -0
- package/dist/src/oracle/markdown.js +46 -0
- package/dist/src/oracle/multiModelRunner.js +164 -0
- package/dist/src/oracle/oscProgress.js +66 -0
- package/dist/src/oracle/promptAssembly.js +13 -0
- package/dist/src/oracle/request.js +49 -0
- package/dist/src/oracle/run.js +492 -0
- package/dist/src/oracle/runUtils.js +27 -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/remote/client.js +128 -0
- package/dist/src/remote/server.js +294 -0
- package/dist/src/remote/types.js +1 -0
- package/dist/src/sessionManager.js +462 -0
- package/dist/src/sessionStore.js +56 -0
- package/dist/src/version.js +39 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -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/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
- package/package.json +102 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -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,154 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { FILE_INPUT_SELECTOR, GENERIC_FILE_INPUT_SELECTOR } from '../constants.js';
|
|
4
|
+
import { delay } from '../utils.js';
|
|
5
|
+
import { logDomFailure } from '../domDebug.js';
|
|
6
|
+
/**
|
|
7
|
+
* Upload file to remote Chrome by transferring content via CDP
|
|
8
|
+
* Used when browser is on a different machine than CLI
|
|
9
|
+
*/
|
|
10
|
+
export async function uploadAttachmentViaDataTransfer(deps, attachment, logger) {
|
|
11
|
+
const { runtime, dom } = deps;
|
|
12
|
+
if (!dom) {
|
|
13
|
+
throw new Error('DOM domain unavailable while uploading attachments.');
|
|
14
|
+
}
|
|
15
|
+
// Read file content from local filesystem
|
|
16
|
+
const fileContent = await readFile(attachment.path);
|
|
17
|
+
// Enforce file size limit to avoid CDP protocol issues
|
|
18
|
+
const MAX_BYTES = 20 * 1024 * 1024; // 20MB limit for CDP transfer
|
|
19
|
+
if (fileContent.length > MAX_BYTES) {
|
|
20
|
+
throw new Error(`Attachment ${path.basename(attachment.path)} is too large for remote upload (${fileContent.length} bytes). Maximum size is ${MAX_BYTES} bytes.`);
|
|
21
|
+
}
|
|
22
|
+
const base64Content = fileContent.toString('base64');
|
|
23
|
+
const fileName = path.basename(attachment.path);
|
|
24
|
+
const mimeType = guessMimeType(fileName);
|
|
25
|
+
logger(`Transferring ${fileName} (${fileContent.length} bytes) to remote browser...`);
|
|
26
|
+
// Find file input element
|
|
27
|
+
const documentNode = await dom.getDocument();
|
|
28
|
+
const selectors = [FILE_INPUT_SELECTOR, GENERIC_FILE_INPUT_SELECTOR];
|
|
29
|
+
let fileInputSelector;
|
|
30
|
+
for (const selector of selectors) {
|
|
31
|
+
const result = await dom.querySelector({ nodeId: documentNode.root.nodeId, selector });
|
|
32
|
+
if (result.nodeId) {
|
|
33
|
+
fileInputSelector = selector;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!fileInputSelector) {
|
|
38
|
+
await logDomFailure(runtime, logger, 'file-input');
|
|
39
|
+
throw new Error('Unable to locate ChatGPT file attachment input.');
|
|
40
|
+
}
|
|
41
|
+
// Inject file via JavaScript DataTransfer API
|
|
42
|
+
const expression = `
|
|
43
|
+
(function() {
|
|
44
|
+
// Check for required file APIs
|
|
45
|
+
if (!('File' in window) || !('Blob' in window) || !('DataTransfer' in window) || typeof atob !== 'function') {
|
|
46
|
+
return { success: false, error: 'Required file APIs are not available in this browser' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fileInput = document.querySelector(${JSON.stringify(fileInputSelector)});
|
|
50
|
+
if (!fileInput) {
|
|
51
|
+
return { success: false, error: 'File input not found' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate that the element is actually a file input
|
|
55
|
+
if (!(fileInput instanceof HTMLInputElement) || fileInput.type !== 'file') {
|
|
56
|
+
return { success: false, error: 'Found element is not a file input' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Convert base64 to Blob
|
|
60
|
+
const base64Data = ${JSON.stringify(base64Content)};
|
|
61
|
+
const binaryString = atob(base64Data);
|
|
62
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
63
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
64
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
65
|
+
}
|
|
66
|
+
const blob = new Blob([bytes], { type: ${JSON.stringify(mimeType)} });
|
|
67
|
+
|
|
68
|
+
// Create File object
|
|
69
|
+
const file = new File([blob], ${JSON.stringify(fileName)}, {
|
|
70
|
+
type: ${JSON.stringify(mimeType)},
|
|
71
|
+
lastModified: Date.now()
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Create DataTransfer and assign to input
|
|
75
|
+
const dataTransfer = new DataTransfer();
|
|
76
|
+
dataTransfer.items.add(file);
|
|
77
|
+
fileInput.files = dataTransfer.files;
|
|
78
|
+
|
|
79
|
+
// Trigger both input and change events for better compatibility
|
|
80
|
+
fileInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
81
|
+
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
82
|
+
|
|
83
|
+
return { success: true, fileName: file.name, size: file.size };
|
|
84
|
+
})()
|
|
85
|
+
`;
|
|
86
|
+
const evalResult = await runtime.evaluate({ expression, returnByValue: true });
|
|
87
|
+
// Check for JavaScript exceptions during evaluation
|
|
88
|
+
if (evalResult.exceptionDetails) {
|
|
89
|
+
const description = evalResult.exceptionDetails.text ?? 'JS evaluation failed';
|
|
90
|
+
throw new Error(`Failed to transfer file to remote browser: ${description}`);
|
|
91
|
+
}
|
|
92
|
+
// Validate result structure before accessing
|
|
93
|
+
if (!evalResult.result || typeof evalResult.result.value !== 'object' || evalResult.result.value == null) {
|
|
94
|
+
throw new Error('Failed to transfer file to remote browser: unexpected evaluation result');
|
|
95
|
+
}
|
|
96
|
+
const uploadResult = evalResult.result.value;
|
|
97
|
+
if (!uploadResult.success) {
|
|
98
|
+
throw new Error(`Failed to transfer file to remote browser: ${uploadResult.error || 'Unknown error'}`);
|
|
99
|
+
}
|
|
100
|
+
logger(`File transferred: ${uploadResult.fileName} (${uploadResult.size} bytes)`);
|
|
101
|
+
// Give ChatGPT a moment to process the file
|
|
102
|
+
await delay(500);
|
|
103
|
+
logger('Attachment queued');
|
|
104
|
+
}
|
|
105
|
+
function guessMimeType(fileName) {
|
|
106
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
107
|
+
const mimeTypes = {
|
|
108
|
+
// Text files
|
|
109
|
+
'.txt': 'text/plain',
|
|
110
|
+
'.md': 'text/markdown',
|
|
111
|
+
'.csv': 'text/csv',
|
|
112
|
+
// Code files
|
|
113
|
+
'.json': 'application/json',
|
|
114
|
+
'.js': 'text/javascript',
|
|
115
|
+
'.ts': 'text/typescript',
|
|
116
|
+
'.jsx': 'text/javascript',
|
|
117
|
+
'.tsx': 'text/typescript',
|
|
118
|
+
'.py': 'text/x-python',
|
|
119
|
+
'.java': 'text/x-java',
|
|
120
|
+
'.c': 'text/x-c',
|
|
121
|
+
'.cpp': 'text/x-c++',
|
|
122
|
+
'.h': 'text/x-c',
|
|
123
|
+
'.hpp': 'text/x-c++',
|
|
124
|
+
'.sh': 'text/x-sh',
|
|
125
|
+
'.bash': 'text/x-sh',
|
|
126
|
+
// Web files
|
|
127
|
+
'.html': 'text/html',
|
|
128
|
+
'.css': 'text/css',
|
|
129
|
+
'.xml': 'text/xml',
|
|
130
|
+
'.yaml': 'text/yaml',
|
|
131
|
+
'.yml': 'text/yaml',
|
|
132
|
+
// Documents
|
|
133
|
+
'.pdf': 'application/pdf',
|
|
134
|
+
'.doc': 'application/msword',
|
|
135
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
136
|
+
'.xls': 'application/vnd.ms-excel',
|
|
137
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
138
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
139
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
140
|
+
// Images
|
|
141
|
+
'.png': 'image/png',
|
|
142
|
+
'.jpg': 'image/jpeg',
|
|
143
|
+
'.jpeg': 'image/jpeg',
|
|
144
|
+
'.gif': 'image/gif',
|
|
145
|
+
'.svg': 'image/svg+xml',
|
|
146
|
+
'.webp': 'image/webp',
|
|
147
|
+
// Archives
|
|
148
|
+
'.zip': 'application/zip',
|
|
149
|
+
'.tar': 'application/x-tar',
|
|
150
|
+
'.gz': 'application/gzip',
|
|
151
|
+
'.7z': 'application/x-7z-compressed',
|
|
152
|
+
};
|
|
153
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
154
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import chromeCookies from 'chrome-cookies-secure';
|
|
6
|
+
import { COOKIE_URLS } from './constants.js';
|
|
7
|
+
import './keytarShim.js';
|
|
8
|
+
import { ensureCookiesDirForFallback } from './windowsCookies.js';
|
|
9
|
+
const COOKIE_READ_TIMEOUT_MS = readDuration('ORACLE_COOKIE_LOAD_TIMEOUT_MS', 5_000);
|
|
10
|
+
const KEYCHAIN_PROBE_TIMEOUT_MS = readDuration('ORACLE_KEYCHAIN_PROBE_TIMEOUT_MS', 3_000);
|
|
11
|
+
const MAC_KEYCHAIN_LABELS = loadKeychainLabels();
|
|
12
|
+
export async function loadChromeCookies({ targetUrl, profile, explicitCookiePath, filterNames, }) {
|
|
13
|
+
const urlsToCheck = Array.from(new Set([stripQuery(targetUrl), ...COOKIE_URLS]));
|
|
14
|
+
const merged = new Map();
|
|
15
|
+
const cookieFile = await resolveCookieFilePath({ explicitPath: explicitCookiePath, profile });
|
|
16
|
+
const cookiesPath = await materializeCookieFile(cookieFile); // returns the copied file (or source on non-Windows)
|
|
17
|
+
const fallbackDir = await ensureCookiesDirForFallback(cookiesPath);
|
|
18
|
+
if (process.env.ORACLE_DEBUG_COOKIES === '1') {
|
|
19
|
+
// eslint-disable-next-line no-console
|
|
20
|
+
console.log(`[cookies] resolved cookie file: ${cookiesPath}`);
|
|
21
|
+
console.log(`[cookies] fallback dir for chrome-cookies-secure: ${fallbackDir}`);
|
|
22
|
+
}
|
|
23
|
+
await ensureMacKeychainReadable();
|
|
24
|
+
for (const url of urlsToCheck) {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
const pathForSecure = await adaptPathForChromeCookies(fallbackDir);
|
|
28
|
+
raw = await settleWithTimeout(chromeCookies.getCookiesPromised(url, 'puppeteer', pathForSecure), COOKIE_READ_TIMEOUT_MS, `Timed out reading Chrome cookies from ${pathForSecure} (after ${COOKIE_READ_TIMEOUT_MS} ms)`);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
throw new Error(`Failed to load Chrome cookies for ${url}: ${message}`);
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(raw))
|
|
35
|
+
continue;
|
|
36
|
+
const fallbackHost = new URL(url).hostname;
|
|
37
|
+
for (const cookie of raw) {
|
|
38
|
+
if (filterNames && filterNames.size > 0 && !filterNames.has(cookie.name))
|
|
39
|
+
continue;
|
|
40
|
+
const normalized = normalizeCookie(cookie, fallbackHost);
|
|
41
|
+
if (!normalized)
|
|
42
|
+
continue;
|
|
43
|
+
const key = `${normalized.domain ?? fallbackHost}:${normalized.name}`;
|
|
44
|
+
if (!merged.has(key)) {
|
|
45
|
+
merged.set(key, normalized);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return Array.from(merged.values());
|
|
50
|
+
}
|
|
51
|
+
async function ensureMacKeychainReadable() {
|
|
52
|
+
if (process.platform !== 'darwin') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// chrome-cookies-secure can hang forever when macOS Keychain rejects access (e.g., SSH/no GUI).
|
|
56
|
+
// Probe the keychain ourselves with a timeout so callers fail fast instead of blocking the run.
|
|
57
|
+
const keytarModule = await import('keytar');
|
|
58
|
+
const keytar = (keytarModule.default ?? keytarModule);
|
|
59
|
+
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
|
+
if (!password) {
|
|
61
|
+
throw new Error('macOS Keychain denied access to Chrome cookies. Unlock the login keychain or run oracle serve from a GUI session, then retry.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function findKeychainPassword(keytar, labels) {
|
|
65
|
+
let lastError = null;
|
|
66
|
+
for (const label of labels) {
|
|
67
|
+
try {
|
|
68
|
+
const value = await keytar.getPassword(label.service, label.account);
|
|
69
|
+
if (value)
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (lastError) {
|
|
77
|
+
throw lastError;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function settleWithTimeout(promise, timeoutMs, timeoutMessage) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
|
84
|
+
timer.unref?.();
|
|
85
|
+
promise.then((value) => {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
resolve(value);
|
|
88
|
+
}, (error) => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
reject(error);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function normalizeCookie(cookie, fallbackHost) {
|
|
95
|
+
if (!cookie?.name)
|
|
96
|
+
return null;
|
|
97
|
+
const domain = cookie.domain?.startsWith('.') ? cookie.domain.slice(1) : cookie.domain ?? fallbackHost;
|
|
98
|
+
const expires = normalizeExpiration(cookie.expires);
|
|
99
|
+
const secure = typeof cookie.Secure === 'boolean' ? cookie.Secure : true;
|
|
100
|
+
const httpOnly = typeof cookie.HttpOnly === 'boolean' ? cookie.HttpOnly : false;
|
|
101
|
+
return {
|
|
102
|
+
name: cookie.name,
|
|
103
|
+
value: cleanValue(cookie.value ?? ''),
|
|
104
|
+
domain,
|
|
105
|
+
path: cookie.path ?? '/',
|
|
106
|
+
expires,
|
|
107
|
+
secure,
|
|
108
|
+
httpOnly,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function cleanValue(value) {
|
|
112
|
+
let i = 0;
|
|
113
|
+
while (i < value.length && value.charCodeAt(i) < 0x20)
|
|
114
|
+
i += 1;
|
|
115
|
+
return value.slice(i);
|
|
116
|
+
}
|
|
117
|
+
function normalizeExpiration(expires) {
|
|
118
|
+
if (!expires || Number.isNaN(expires)) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const value = Number(expires);
|
|
122
|
+
if (value <= 0)
|
|
123
|
+
return undefined;
|
|
124
|
+
if (value > 1_000_000_000_000) {
|
|
125
|
+
return Math.round(value / 1_000_000 - 11644473600);
|
|
126
|
+
}
|
|
127
|
+
if (value > 1_000_000_000) {
|
|
128
|
+
return Math.round(value / 1000);
|
|
129
|
+
}
|
|
130
|
+
return Math.round(value);
|
|
131
|
+
}
|
|
132
|
+
async function resolveCookieFilePath({ explicitPath, profile, }) {
|
|
133
|
+
if (explicitPath && explicitPath.trim().length > 0) {
|
|
134
|
+
return ensureCookieFile(explicitPath);
|
|
135
|
+
}
|
|
136
|
+
if (profile && looksLikePath(profile)) {
|
|
137
|
+
return ensureCookieFile(profile);
|
|
138
|
+
}
|
|
139
|
+
const profileName = profile && profile.trim().length > 0 ? profile : 'Default';
|
|
140
|
+
const baseDir = await defaultProfileRoot();
|
|
141
|
+
return ensureCookieFile(path.join(baseDir, profileName));
|
|
142
|
+
}
|
|
143
|
+
async function adaptPathForChromeCookies(resolved) {
|
|
144
|
+
const stat = await fs.stat(resolved).catch(() => null);
|
|
145
|
+
if (stat?.isFile()) {
|
|
146
|
+
// chrome-cookies-secure appends "Cookies" when given a directory; if we already have the file, return its directory.
|
|
147
|
+
return path.dirname(resolved);
|
|
148
|
+
}
|
|
149
|
+
return resolved;
|
|
150
|
+
}
|
|
151
|
+
async function ensureCookieFile(inputPath) {
|
|
152
|
+
const expanded = expandPath(inputPath);
|
|
153
|
+
const stat = await fs.stat(expanded).catch(() => null);
|
|
154
|
+
if (!stat) {
|
|
155
|
+
throw new Error(`Unable to locate Chrome cookie DB at ${expanded}`);
|
|
156
|
+
}
|
|
157
|
+
if (stat.isDirectory()) {
|
|
158
|
+
const directFile = path.join(expanded, 'Cookies');
|
|
159
|
+
if (await fileExists(directFile))
|
|
160
|
+
return directFile;
|
|
161
|
+
const networkFile = path.join(expanded, 'Network', 'Cookies');
|
|
162
|
+
if (await fileExists(networkFile))
|
|
163
|
+
return networkFile;
|
|
164
|
+
throw new Error(`No Cookies DB found under ${expanded}`);
|
|
165
|
+
}
|
|
166
|
+
return expanded;
|
|
167
|
+
}
|
|
168
|
+
async function fileExists(candidate) {
|
|
169
|
+
try {
|
|
170
|
+
const stat = await fs.stat(candidate);
|
|
171
|
+
return stat.isFile();
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function expandPath(input) {
|
|
178
|
+
if (input.startsWith('~/')) {
|
|
179
|
+
return path.join(os.homedir(), input.slice(2));
|
|
180
|
+
}
|
|
181
|
+
return path.isAbsolute(input) ? input : path.resolve(process.cwd(), input);
|
|
182
|
+
}
|
|
183
|
+
function looksLikePath(value) {
|
|
184
|
+
return value.includes('/') || value.includes('\\');
|
|
185
|
+
}
|
|
186
|
+
async function materializeCookieFile(sourcePath) {
|
|
187
|
+
if (process.platform !== 'win32')
|
|
188
|
+
return sourcePath;
|
|
189
|
+
// Chrome can keep the Cookies DB locked; copy to a temp file so sqlite can open it reliably.
|
|
190
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookies-'));
|
|
191
|
+
const tempPath = path.join(tempDir, 'Cookies');
|
|
192
|
+
try {
|
|
193
|
+
await fs.copyFile(sourcePath, tempPath);
|
|
194
|
+
return tempPath;
|
|
195
|
+
}
|
|
196
|
+
catch (_error) {
|
|
197
|
+
// Fall back to the original path if the copy fails; upstream error handling will surface issues.
|
|
198
|
+
return sourcePath;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function defaultProfileRoot() {
|
|
202
|
+
const candidates = [];
|
|
203
|
+
if (process.platform === 'darwin') {
|
|
204
|
+
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
|
+
}
|
|
206
|
+
else if (process.platform === 'linux') {
|
|
207
|
+
candidates.push(path.join(os.homedir(), '.config', 'google-chrome'), path.join(os.homedir(), '.config', 'microsoft-edge'), path.join(os.homedir(), '.config', 'chromium'),
|
|
208
|
+
// Snap Chromium profiles
|
|
209
|
+
path.join(os.homedir(), 'snap', 'chromium', 'common', 'chromium'), path.join(os.homedir(), 'snap', 'chromium', 'current', 'chromium'));
|
|
210
|
+
}
|
|
211
|
+
else if (process.platform === 'win32') {
|
|
212
|
+
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
|
213
|
+
candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data'), path.join(localAppData, 'Microsoft', 'Edge', 'User Data'), path.join(localAppData, 'Chromium', 'User Data'));
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
217
|
+
}
|
|
218
|
+
for (const candidate of candidates) {
|
|
219
|
+
if (existsSync(candidate)) {
|
|
220
|
+
return candidate;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// fallback: first candidate even if missing; upstream will throw clearer error
|
|
224
|
+
return candidates[0];
|
|
225
|
+
}
|
|
226
|
+
function stripQuery(url) {
|
|
227
|
+
try {
|
|
228
|
+
const parsed = new URL(url);
|
|
229
|
+
parsed.hash = '';
|
|
230
|
+
parsed.search = '';
|
|
231
|
+
return parsed.toString();
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return url;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function readDuration(envKey, fallback) {
|
|
238
|
+
const raw = process.env[envKey];
|
|
239
|
+
if (!raw)
|
|
240
|
+
return fallback;
|
|
241
|
+
const parsed = Number.parseInt(raw, 10);
|
|
242
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
243
|
+
}
|
|
244
|
+
function loadKeychainLabels() {
|
|
245
|
+
const defaults = [
|
|
246
|
+
{ service: 'Chrome Safe Storage', account: 'Chrome' },
|
|
247
|
+
{ service: 'Chromium Safe Storage', account: 'Chromium' },
|
|
248
|
+
{ service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' },
|
|
249
|
+
{ service: 'Brave Safe Storage', account: 'Brave' },
|
|
250
|
+
{ service: 'Vivaldi Safe Storage', account: 'Vivaldi' },
|
|
251
|
+
];
|
|
252
|
+
const rawEnv = process.env.ORACLE_KEYCHAIN_LABELS;
|
|
253
|
+
if (!rawEnv)
|
|
254
|
+
return defaults;
|
|
255
|
+
try {
|
|
256
|
+
const parsed = JSON.parse(rawEnv);
|
|
257
|
+
if (!Array.isArray(parsed))
|
|
258
|
+
return defaults;
|
|
259
|
+
const envLabels = parsed
|
|
260
|
+
.map((entry) => (entry && typeof entry === 'object' ? entry : null))
|
|
261
|
+
.filter((entry) => Boolean(entry?.service && entry?.account));
|
|
262
|
+
return envLabels.length ? [...envLabels, ...defaults] : defaults;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return defaults;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
269
|
+
export const __test__ = {
|
|
270
|
+
normalizeExpiration,
|
|
271
|
+
cleanValue,
|
|
272
|
+
looksLikePath,
|
|
273
|
+
defaultProfileRoot,
|
|
274
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { rm } from 'node:fs/promises';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import CDP from 'chrome-remote-interface';
|
|
5
|
+
import { launch } from 'chrome-launcher';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
export async function launchChrome(config, userDataDir, logger) {
|
|
8
|
+
const chromeFlags = buildChromeFlags(config.headless);
|
|
9
|
+
const launcher = await launch({
|
|
10
|
+
chromePath: config.chromePath ?? undefined,
|
|
11
|
+
chromeFlags,
|
|
12
|
+
userDataDir,
|
|
13
|
+
});
|
|
14
|
+
const pidLabel = typeof launcher.pid === 'number' ? ` (pid ${launcher.pid})` : '';
|
|
15
|
+
logger(`Launched Chrome${pidLabel} on port ${launcher.port}`);
|
|
16
|
+
return launcher;
|
|
17
|
+
}
|
|
18
|
+
export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logger) {
|
|
19
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
|
|
20
|
+
let handling;
|
|
21
|
+
const handleSignal = (signal) => {
|
|
22
|
+
if (handling) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
handling = true;
|
|
26
|
+
logger(`Received ${signal}; terminating Chrome process`);
|
|
27
|
+
void (async () => {
|
|
28
|
+
try {
|
|
29
|
+
await chrome.kill();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// ignore kill failures
|
|
33
|
+
}
|
|
34
|
+
if (!keepBrowser) {
|
|
35
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
36
|
+
}
|
|
37
|
+
})().finally(() => {
|
|
38
|
+
const exitCode = signal === 'SIGINT' ? 130 : 1;
|
|
39
|
+
process.exit(exitCode);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
for (const signal of signals) {
|
|
43
|
+
process.on(signal, handleSignal);
|
|
44
|
+
}
|
|
45
|
+
return () => {
|
|
46
|
+
for (const signal of signals) {
|
|
47
|
+
process.removeListener(signal, handleSignal);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export async function hideChromeWindow(chrome, logger) {
|
|
52
|
+
if (process.platform !== 'darwin') {
|
|
53
|
+
logger('Window hiding is only supported on macOS');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!chrome.pid) {
|
|
57
|
+
logger('Unable to hide window: missing Chrome PID');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const script = `tell application "System Events"
|
|
61
|
+
try
|
|
62
|
+
set visible of (first process whose unix id is ${chrome.pid}) to false
|
|
63
|
+
end try
|
|
64
|
+
end tell`;
|
|
65
|
+
try {
|
|
66
|
+
await execFileAsync('osascript', ['-e', script]);
|
|
67
|
+
logger('Chrome window hidden (Cmd-H)');
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
+
logger(`Failed to hide Chrome window: ${message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function connectToChrome(port, logger) {
|
|
75
|
+
const client = await CDP({ port });
|
|
76
|
+
logger('Connected to Chrome DevTools protocol');
|
|
77
|
+
return client;
|
|
78
|
+
}
|
|
79
|
+
export async function connectToRemoteChrome(host, port, logger) {
|
|
80
|
+
const client = await CDP({ host, port });
|
|
81
|
+
logger(`Connected to remote Chrome DevTools protocol at ${host}:${port}`);
|
|
82
|
+
return client;
|
|
83
|
+
}
|
|
84
|
+
function buildChromeFlags(_headless) {
|
|
85
|
+
const flags = [
|
|
86
|
+
'--disable-background-networking',
|
|
87
|
+
'--disable-background-timer-throttling',
|
|
88
|
+
'--disable-breakpad',
|
|
89
|
+
'--disable-client-side-phishing-detection',
|
|
90
|
+
'--disable-default-apps',
|
|
91
|
+
'--disable-hang-monitor',
|
|
92
|
+
'--disable-popup-blocking',
|
|
93
|
+
'--disable-prompt-on-repost',
|
|
94
|
+
'--disable-sync',
|
|
95
|
+
'--disable-translate',
|
|
96
|
+
'--metrics-recording-only',
|
|
97
|
+
'--no-first-run',
|
|
98
|
+
'--safebrowsing-disable-auto-update',
|
|
99
|
+
'--disable-features=TranslateUI,AutomationControlled',
|
|
100
|
+
'--mute-audio',
|
|
101
|
+
'--window-size=1280,720',
|
|
102
|
+
'--password-store=basic',
|
|
103
|
+
'--use-mock-keychain',
|
|
104
|
+
];
|
|
105
|
+
// Headless/new is blocked by Cloudflare; always run headful.
|
|
106
|
+
return flags;
|
|
107
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CHATGPT_URL, DEFAULT_MODEL_TARGET } from './constants.js';
|
|
2
|
+
import { normalizeChatgptUrl } from './utils.js';
|
|
3
|
+
export const DEFAULT_BROWSER_CONFIG = {
|
|
4
|
+
chromeProfile: null,
|
|
5
|
+
chromePath: null,
|
|
6
|
+
chromeCookiePath: null,
|
|
7
|
+
url: CHATGPT_URL,
|
|
8
|
+
chatgptUrl: CHATGPT_URL,
|
|
9
|
+
timeoutMs: 1_200_000,
|
|
10
|
+
inputTimeoutMs: 30_000,
|
|
11
|
+
cookieSync: true,
|
|
12
|
+
cookieNames: null,
|
|
13
|
+
inlineCookies: null,
|
|
14
|
+
inlineCookiesSource: null,
|
|
15
|
+
headless: false,
|
|
16
|
+
keepBrowser: false,
|
|
17
|
+
hideWindow: false,
|
|
18
|
+
desiredModel: DEFAULT_MODEL_TARGET,
|
|
19
|
+
debug: false,
|
|
20
|
+
allowCookieErrors: false,
|
|
21
|
+
remoteChrome: null,
|
|
22
|
+
};
|
|
23
|
+
export function resolveBrowserConfig(config) {
|
|
24
|
+
const envAllowCookieErrors = (process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? '').trim().toLowerCase() === 'true' ||
|
|
25
|
+
(process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? '').trim() === '1';
|
|
26
|
+
const rawUrl = config?.chatgptUrl ?? config?.url ?? DEFAULT_BROWSER_CONFIG.url;
|
|
27
|
+
const normalizedUrl = normalizeChatgptUrl(rawUrl ?? DEFAULT_BROWSER_CONFIG.url, DEFAULT_BROWSER_CONFIG.url);
|
|
28
|
+
return {
|
|
29
|
+
...DEFAULT_BROWSER_CONFIG,
|
|
30
|
+
...(config ?? {}),
|
|
31
|
+
url: normalizedUrl,
|
|
32
|
+
chatgptUrl: normalizedUrl,
|
|
33
|
+
timeoutMs: config?.timeoutMs ?? DEFAULT_BROWSER_CONFIG.timeoutMs,
|
|
34
|
+
inputTimeoutMs: config?.inputTimeoutMs ?? DEFAULT_BROWSER_CONFIG.inputTimeoutMs,
|
|
35
|
+
cookieSync: config?.cookieSync ?? DEFAULT_BROWSER_CONFIG.cookieSync,
|
|
36
|
+
cookieNames: config?.cookieNames ?? DEFAULT_BROWSER_CONFIG.cookieNames,
|
|
37
|
+
inlineCookies: config?.inlineCookies ?? DEFAULT_BROWSER_CONFIG.inlineCookies,
|
|
38
|
+
inlineCookiesSource: config?.inlineCookiesSource ?? DEFAULT_BROWSER_CONFIG.inlineCookiesSource,
|
|
39
|
+
headless: config?.headless ?? DEFAULT_BROWSER_CONFIG.headless,
|
|
40
|
+
keepBrowser: config?.keepBrowser ?? DEFAULT_BROWSER_CONFIG.keepBrowser,
|
|
41
|
+
hideWindow: config?.hideWindow ?? DEFAULT_BROWSER_CONFIG.hideWindow,
|
|
42
|
+
desiredModel: config?.desiredModel ?? DEFAULT_BROWSER_CONFIG.desiredModel,
|
|
43
|
+
chromeProfile: config?.chromeProfile ?? DEFAULT_BROWSER_CONFIG.chromeProfile,
|
|
44
|
+
chromePath: config?.chromePath ?? DEFAULT_BROWSER_CONFIG.chromePath,
|
|
45
|
+
chromeCookiePath: config?.chromeCookiePath ?? DEFAULT_BROWSER_CONFIG.chromeCookiePath,
|
|
46
|
+
debug: config?.debug ?? DEFAULT_BROWSER_CONFIG.debug,
|
|
47
|
+
allowCookieErrors: config?.allowCookieErrors ?? envAllowCookieErrors ?? DEFAULT_BROWSER_CONFIG.allowCookieErrors,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const CHATGPT_URL = 'https://chatgpt.com/';
|
|
2
|
+
export const DEFAULT_MODEL_TARGET = 'ChatGPT 5.1';
|
|
3
|
+
export const COOKIE_URLS = ['https://chatgpt.com', 'https://chat.openai.com', 'https://atlas.openai.com'];
|
|
4
|
+
export const INPUT_SELECTORS = [
|
|
5
|
+
'textarea[data-id="prompt-textarea"]',
|
|
6
|
+
'textarea[placeholder*="Send a message"]',
|
|
7
|
+
'textarea[aria-label="Message ChatGPT"]',
|
|
8
|
+
'textarea:not([disabled])',
|
|
9
|
+
'textarea[name="prompt-textarea"]',
|
|
10
|
+
'#prompt-textarea',
|
|
11
|
+
'.ProseMirror',
|
|
12
|
+
'[contenteditable="true"][data-virtualkeyboard="true"]',
|
|
13
|
+
];
|
|
14
|
+
export const ANSWER_SELECTORS = [
|
|
15
|
+
'article[data-testid^="conversation-turn"][data-message-author-role="assistant"]',
|
|
16
|
+
'article[data-testid^="conversation-turn"] [data-message-author-role="assistant"]',
|
|
17
|
+
'article[data-testid^="conversation-turn"] .markdown',
|
|
18
|
+
'[data-message-author-role="assistant"] .markdown',
|
|
19
|
+
'[data-message-author-role="assistant"]',
|
|
20
|
+
];
|
|
21
|
+
export const CONVERSATION_TURN_SELECTOR = 'article[data-testid^="conversation-turn"]';
|
|
22
|
+
export const ASSISTANT_ROLE_SELECTOR = '[data-message-author-role="assistant"]';
|
|
23
|
+
export const CLOUDFLARE_SCRIPT_SELECTOR = 'script[src*="/challenge-platform/"]';
|
|
24
|
+
export const CLOUDFLARE_TITLE = 'just a moment';
|
|
25
|
+
export const PROMPT_PRIMARY_SELECTOR = '#prompt-textarea';
|
|
26
|
+
export const PROMPT_FALLBACK_SELECTOR = 'textarea[name="prompt-textarea"]';
|
|
27
|
+
export const FILE_INPUT_SELECTOR = 'form input[type="file"]:not([accept])';
|
|
28
|
+
export const GENERIC_FILE_INPUT_SELECTOR = 'input[type="file"]:not([accept])';
|
|
29
|
+
export const MENU_CONTAINER_SELECTOR = '[role="menu"], [data-radix-collection-root]';
|
|
30
|
+
export const MENU_ITEM_SELECTOR = 'button, [role="menuitem"], [role="menuitemradio"], [data-testid*="model-switcher-"]';
|
|
31
|
+
export const UPLOAD_STATUS_SELECTORS = [
|
|
32
|
+
'[data-testid*="upload"]',
|
|
33
|
+
'[data-testid*="attachment"]',
|
|
34
|
+
'[data-state="loading"]',
|
|
35
|
+
'[aria-live="polite"]',
|
|
36
|
+
];
|
|
37
|
+
export const STOP_BUTTON_SELECTOR = '[data-testid="stop-button"]';
|
|
38
|
+
export const SEND_BUTTON_SELECTOR = '[data-testid="send-button"]';
|
|
39
|
+
export const MODEL_BUTTON_SELECTOR = '[data-testid="model-switcher-dropdown-button"]';
|
|
40
|
+
export const COPY_BUTTON_SELECTOR = 'button[data-testid="copy-turn-action-button"]';
|
|
41
|
+
// Action buttons that only appear once a turn has finished rendering.
|
|
42
|
+
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"]';
|