@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,541 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { resolveBrowserConfig } from './config.js';
|
|
5
|
+
import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToChrome, connectToRemoteChrome, } from './chromeLifecycle.js';
|
|
6
|
+
import { syncCookies } from './cookies.js';
|
|
7
|
+
import { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, readAssistantSnapshot, } from './pageActions.js';
|
|
8
|
+
import { uploadAttachmentViaDataTransfer } from './actions/remoteFileTransfer.js';
|
|
9
|
+
import { estimateTokenCount, withRetries } from './utils.js';
|
|
10
|
+
import { formatElapsed } from '../oracle/format.js';
|
|
11
|
+
import { CHATGPT_URL } from './constants.js';
|
|
12
|
+
export { CHATGPT_URL, DEFAULT_MODEL_TARGET } from './constants.js';
|
|
13
|
+
export { parseDuration, delay, normalizeChatgptUrl } from './utils.js';
|
|
14
|
+
export async function runBrowserMode(options) {
|
|
15
|
+
const promptText = options.prompt?.trim();
|
|
16
|
+
if (!promptText) {
|
|
17
|
+
throw new Error('Prompt text is required when using browser mode.');
|
|
18
|
+
}
|
|
19
|
+
const attachments = options.attachments ?? [];
|
|
20
|
+
const config = resolveBrowserConfig(options.config);
|
|
21
|
+
const logger = options.log ?? ((_message) => { });
|
|
22
|
+
if (logger.verbose === undefined) {
|
|
23
|
+
logger.verbose = Boolean(config.debug);
|
|
24
|
+
}
|
|
25
|
+
if (logger.sessionLog === undefined && options.log?.sessionLog) {
|
|
26
|
+
logger.sessionLog = options.log.sessionLog;
|
|
27
|
+
}
|
|
28
|
+
if (config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === '1') {
|
|
29
|
+
logger(`[browser-mode] config: ${JSON.stringify({
|
|
30
|
+
...config,
|
|
31
|
+
promptLength: promptText.length,
|
|
32
|
+
})}`);
|
|
33
|
+
}
|
|
34
|
+
// Remote Chrome mode - connect to existing browser
|
|
35
|
+
if (config.remoteChrome) {
|
|
36
|
+
// Warn about ignored local-only options
|
|
37
|
+
if (config.headless || config.hideWindow || config.keepBrowser || config.chromePath) {
|
|
38
|
+
logger('Note: --remote-chrome ignores local Chrome flags ' +
|
|
39
|
+
'(--browser-headless, --browser-hide-window, --browser-keep-browser, --browser-chrome-path).');
|
|
40
|
+
}
|
|
41
|
+
return runRemoteBrowserMode(promptText, attachments, config, logger, options);
|
|
42
|
+
}
|
|
43
|
+
const userDataDir = await mkdtemp(path.join(os.tmpdir(), 'oracle-browser-'));
|
|
44
|
+
logger(`Created temporary Chrome profile at ${userDataDir}`);
|
|
45
|
+
const chrome = await launchChrome(config, userDataDir, logger);
|
|
46
|
+
let removeTerminationHooks = null;
|
|
47
|
+
try {
|
|
48
|
+
removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, config.keepBrowser, logger);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// ignore failure; cleanup still happens below
|
|
52
|
+
}
|
|
53
|
+
let client = null;
|
|
54
|
+
const startedAt = Date.now();
|
|
55
|
+
let answerText = '';
|
|
56
|
+
let answerMarkdown = '';
|
|
57
|
+
let answerHtml = '';
|
|
58
|
+
let runStatus = 'attempted';
|
|
59
|
+
let connectionClosedUnexpectedly = false;
|
|
60
|
+
let stopThinkingMonitor = null;
|
|
61
|
+
let appliedCookies = 0;
|
|
62
|
+
try {
|
|
63
|
+
client = await connectToChrome(chrome.port, logger);
|
|
64
|
+
const disconnectPromise = new Promise((_, reject) => {
|
|
65
|
+
client?.on('disconnect', () => {
|
|
66
|
+
connectionClosedUnexpectedly = true;
|
|
67
|
+
logger('Chrome window closed; attempting to abort run.');
|
|
68
|
+
reject(new Error('Chrome window closed before oracle finished. Please keep it open until completion.'));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
72
|
+
const { Network, Page, Runtime, Input, DOM } = client;
|
|
73
|
+
if (!config.headless && config.hideWindow) {
|
|
74
|
+
await hideChromeWindow(chrome, logger);
|
|
75
|
+
}
|
|
76
|
+
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
77
|
+
if (DOM && typeof DOM.enable === 'function') {
|
|
78
|
+
domainEnablers.push(DOM.enable());
|
|
79
|
+
}
|
|
80
|
+
await Promise.all(domainEnablers);
|
|
81
|
+
await Network.clearBrowserCookies();
|
|
82
|
+
if (config.cookieSync) {
|
|
83
|
+
if (!config.inlineCookies) {
|
|
84
|
+
logger('Heads-up: macOS may prompt for your Keychain password to read Chrome cookies; use --copy or --render for manual flow.');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
logger('Applying inline cookies (skipping Chrome profile read and Keychain prompt)');
|
|
88
|
+
}
|
|
89
|
+
const cookieCount = await syncCookies(Network, config.url, config.chromeProfile, logger, {
|
|
90
|
+
allowErrors: config.allowCookieErrors ?? false,
|
|
91
|
+
filterNames: config.cookieNames ?? undefined,
|
|
92
|
+
inlineCookies: config.inlineCookies ?? undefined,
|
|
93
|
+
cookiePath: config.chromeCookiePath ?? undefined,
|
|
94
|
+
});
|
|
95
|
+
appliedCookies = cookieCount;
|
|
96
|
+
if (config.inlineCookies && cookieCount === 0) {
|
|
97
|
+
throw new Error('No inline cookies were applied; aborting before navigation.');
|
|
98
|
+
}
|
|
99
|
+
logger(cookieCount > 0
|
|
100
|
+
? config.inlineCookies
|
|
101
|
+
? `Applied ${cookieCount} inline cookies`
|
|
102
|
+
: `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ?? 'Default'}`
|
|
103
|
+
: config.inlineCookies
|
|
104
|
+
? 'No inline cookies applied; continuing without session reuse'
|
|
105
|
+
: 'No Chrome cookies found; continuing without session reuse');
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
logger('Skipping Chrome cookie sync (--browser-no-cookie-sync)');
|
|
109
|
+
}
|
|
110
|
+
const baseUrl = CHATGPT_URL;
|
|
111
|
+
// First load the base ChatGPT homepage to satisfy potential interstitials,
|
|
112
|
+
// then hop to the requested URL if it differs.
|
|
113
|
+
await raceWithDisconnect(navigateToChatGPT(Page, Runtime, baseUrl, logger));
|
|
114
|
+
await raceWithDisconnect(ensureNotBlocked(Runtime, config.headless, logger));
|
|
115
|
+
await raceWithDisconnect(ensureLoggedIn(Runtime, logger, { appliedCookies }));
|
|
116
|
+
if (config.url !== baseUrl) {
|
|
117
|
+
await raceWithDisconnect(navigateToChatGPT(Page, Runtime, config.url, logger));
|
|
118
|
+
await raceWithDisconnect(ensureNotBlocked(Runtime, config.headless, logger));
|
|
119
|
+
}
|
|
120
|
+
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
121
|
+
logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
|
|
122
|
+
if (config.desiredModel) {
|
|
123
|
+
await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger), {
|
|
124
|
+
retries: 2,
|
|
125
|
+
delayMs: 300,
|
|
126
|
+
onRetry: (attempt, error) => {
|
|
127
|
+
if (options.verbose) {
|
|
128
|
+
logger(`[retry] Model picker attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
})).catch((error) => {
|
|
132
|
+
const base = error instanceof Error ? error.message : String(error);
|
|
133
|
+
const hint = appliedCookies === 0
|
|
134
|
+
? ' No cookies were applied; log in to ChatGPT in Chrome or provide inline cookies (--browser-inline-cookies[(-file)] or ORACLE_BROWSER_COOKIES_JSON).'
|
|
135
|
+
: '';
|
|
136
|
+
throw new Error(`${base}${hint}`);
|
|
137
|
+
});
|
|
138
|
+
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
139
|
+
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
140
|
+
}
|
|
141
|
+
if (attachments.length > 0) {
|
|
142
|
+
if (!DOM) {
|
|
143
|
+
throw new Error('Chrome DOM domain unavailable while uploading attachments.');
|
|
144
|
+
}
|
|
145
|
+
for (const attachment of attachments) {
|
|
146
|
+
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
147
|
+
await uploadAttachmentFile({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
148
|
+
}
|
|
149
|
+
const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
|
|
150
|
+
await raceWithDisconnect(waitForAttachmentCompletion(Runtime, waitBudget, logger));
|
|
151
|
+
logger('All attachments uploaded');
|
|
152
|
+
}
|
|
153
|
+
await raceWithDisconnect(submitPrompt({ runtime: Runtime, input: Input }, promptText, logger));
|
|
154
|
+
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
155
|
+
const answer = await raceWithDisconnect(waitForAssistantResponse(Runtime, config.timeoutMs, logger));
|
|
156
|
+
answerText = answer.text;
|
|
157
|
+
answerHtml = answer.html ?? '';
|
|
158
|
+
const copiedMarkdown = await raceWithDisconnect(withRetries(async () => {
|
|
159
|
+
const attempt = await captureAssistantMarkdown(Runtime, answer.meta, logger);
|
|
160
|
+
if (!attempt) {
|
|
161
|
+
throw new Error('copy-missing');
|
|
162
|
+
}
|
|
163
|
+
return attempt;
|
|
164
|
+
}, {
|
|
165
|
+
retries: 2,
|
|
166
|
+
delayMs: 350,
|
|
167
|
+
onRetry: (attempt, error) => {
|
|
168
|
+
if (options.verbose) {
|
|
169
|
+
logger(`[retry] Markdown capture attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
})).catch(() => null);
|
|
173
|
+
answerMarkdown = copiedMarkdown ?? answerText;
|
|
174
|
+
// Final sanity check: ensure we didn't accidentally capture the user prompt instead of the assistant turn.
|
|
175
|
+
const finalSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
176
|
+
const finalText = typeof finalSnapshot?.text === 'string' ? finalSnapshot.text.trim() : '';
|
|
177
|
+
if (finalText &&
|
|
178
|
+
finalText !== answerMarkdown.trim() &&
|
|
179
|
+
finalText !== promptText.trim() &&
|
|
180
|
+
finalText.length >= answerMarkdown.trim().length) {
|
|
181
|
+
logger('Refreshed assistant response via final DOM snapshot');
|
|
182
|
+
answerText = finalText;
|
|
183
|
+
answerMarkdown = finalText;
|
|
184
|
+
}
|
|
185
|
+
if (answerMarkdown.trim() === promptText.trim()) {
|
|
186
|
+
const deadline = Date.now() + 8_000;
|
|
187
|
+
let bestText = null;
|
|
188
|
+
let stableCount = 0;
|
|
189
|
+
while (Date.now() < deadline) {
|
|
190
|
+
const snapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
191
|
+
const text = typeof snapshot?.text === 'string' ? snapshot.text.trim() : '';
|
|
192
|
+
if (text && text !== promptText.trim()) {
|
|
193
|
+
if (!bestText || text.length > bestText.length) {
|
|
194
|
+
bestText = text;
|
|
195
|
+
stableCount = 0;
|
|
196
|
+
}
|
|
197
|
+
else if (text === bestText) {
|
|
198
|
+
stableCount += 1;
|
|
199
|
+
}
|
|
200
|
+
if (stableCount >= 2) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
205
|
+
}
|
|
206
|
+
if (bestText) {
|
|
207
|
+
logger('Recovered assistant response after detecting prompt echo');
|
|
208
|
+
answerText = bestText;
|
|
209
|
+
answerMarkdown = bestText;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
stopThinkingMonitor?.();
|
|
213
|
+
runStatus = 'complete';
|
|
214
|
+
const durationMs = Date.now() - startedAt;
|
|
215
|
+
const answerChars = answerText.length;
|
|
216
|
+
const answerTokens = estimateTokenCount(answerMarkdown);
|
|
217
|
+
return {
|
|
218
|
+
answerText,
|
|
219
|
+
answerMarkdown,
|
|
220
|
+
answerHtml: answerHtml.length > 0 ? answerHtml : undefined,
|
|
221
|
+
tookMs: durationMs,
|
|
222
|
+
answerTokens,
|
|
223
|
+
answerChars,
|
|
224
|
+
chromePid: chrome.pid,
|
|
225
|
+
chromePort: chrome.port,
|
|
226
|
+
userDataDir,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
231
|
+
stopThinkingMonitor?.();
|
|
232
|
+
const socketClosed = connectionClosedUnexpectedly || isWebSocketClosureError(normalizedError);
|
|
233
|
+
connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
|
|
234
|
+
if (!socketClosed) {
|
|
235
|
+
logger(`Failed to complete ChatGPT run: ${normalizedError.message}`);
|
|
236
|
+
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === '1') && normalizedError.stack) {
|
|
237
|
+
logger(normalizedError.stack);
|
|
238
|
+
}
|
|
239
|
+
throw normalizedError;
|
|
240
|
+
}
|
|
241
|
+
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === '1') && normalizedError.stack) {
|
|
242
|
+
logger(`Chrome window closed before completion: ${normalizedError.message}`);
|
|
243
|
+
logger(normalizedError.stack);
|
|
244
|
+
}
|
|
245
|
+
throw new Error('Chrome window closed before oracle finished. Please keep it open until completion.', {
|
|
246
|
+
cause: normalizedError,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
try {
|
|
251
|
+
if (!connectionClosedUnexpectedly) {
|
|
252
|
+
await client?.close();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// ignore
|
|
257
|
+
}
|
|
258
|
+
removeTerminationHooks?.();
|
|
259
|
+
if (!config.keepBrowser) {
|
|
260
|
+
if (!connectionClosedUnexpectedly) {
|
|
261
|
+
try {
|
|
262
|
+
await chrome.kill();
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// ignore kill failures
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
269
|
+
if (!connectionClosedUnexpectedly) {
|
|
270
|
+
const totalSeconds = (Date.now() - startedAt) / 1000;
|
|
271
|
+
logger(`Cleanup ${runStatus} • ${totalSeconds.toFixed(1)}s total`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else if (!connectionClosedUnexpectedly) {
|
|
275
|
+
logger(`Chrome left running on port ${chrome.port} with profile ${userDataDir}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function runRemoteBrowserMode(promptText, attachments, config, logger, options) {
|
|
280
|
+
const remoteChromeConfig = config.remoteChrome;
|
|
281
|
+
if (!remoteChromeConfig) {
|
|
282
|
+
throw new Error('Remote Chrome configuration missing. Pass --remote-chrome <host:port> to use this mode.');
|
|
283
|
+
}
|
|
284
|
+
const { host, port } = remoteChromeConfig;
|
|
285
|
+
logger(`Connecting to remote Chrome at ${host}:${port}`);
|
|
286
|
+
let client = null;
|
|
287
|
+
const startedAt = Date.now();
|
|
288
|
+
let answerText = '';
|
|
289
|
+
let answerMarkdown = '';
|
|
290
|
+
let answerHtml = '';
|
|
291
|
+
let connectionClosedUnexpectedly = false;
|
|
292
|
+
let stopThinkingMonitor = null;
|
|
293
|
+
try {
|
|
294
|
+
client = await connectToRemoteChrome(host, port, logger);
|
|
295
|
+
const markConnectionLost = () => {
|
|
296
|
+
connectionClosedUnexpectedly = true;
|
|
297
|
+
};
|
|
298
|
+
client.on('disconnect', markConnectionLost);
|
|
299
|
+
const { Network, Page, Runtime, Input, DOM } = client;
|
|
300
|
+
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
301
|
+
if (DOM && typeof DOM.enable === 'function') {
|
|
302
|
+
domainEnablers.push(DOM.enable());
|
|
303
|
+
}
|
|
304
|
+
await Promise.all(domainEnablers);
|
|
305
|
+
// Skip cookie sync for remote Chrome - it already has cookies
|
|
306
|
+
logger('Skipping cookie sync for remote Chrome (using existing session)');
|
|
307
|
+
await navigateToChatGPT(Page, Runtime, config.url, logger);
|
|
308
|
+
await ensureNotBlocked(Runtime, config.headless, logger);
|
|
309
|
+
await ensureLoggedIn(Runtime, logger, { remoteSession: true });
|
|
310
|
+
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
311
|
+
logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
|
|
312
|
+
if (config.desiredModel) {
|
|
313
|
+
await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger), {
|
|
314
|
+
retries: 2,
|
|
315
|
+
delayMs: 300,
|
|
316
|
+
onRetry: (attempt, error) => {
|
|
317
|
+
if (options.verbose) {
|
|
318
|
+
logger(`[retry] Model picker attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
323
|
+
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
324
|
+
}
|
|
325
|
+
if (attachments.length > 0) {
|
|
326
|
+
if (!DOM) {
|
|
327
|
+
throw new Error('Chrome DOM domain unavailable while uploading attachments.');
|
|
328
|
+
}
|
|
329
|
+
// Use remote file transfer for remote Chrome (reads local files and injects via CDP)
|
|
330
|
+
for (const attachment of attachments) {
|
|
331
|
+
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
332
|
+
await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
333
|
+
}
|
|
334
|
+
const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
|
|
335
|
+
await waitForAttachmentCompletion(Runtime, waitBudget, logger);
|
|
336
|
+
logger('All attachments uploaded');
|
|
337
|
+
}
|
|
338
|
+
await submitPrompt({ runtime: Runtime, input: Input }, promptText, logger);
|
|
339
|
+
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
340
|
+
const answer = await waitForAssistantResponse(Runtime, config.timeoutMs, logger);
|
|
341
|
+
answerText = answer.text;
|
|
342
|
+
answerHtml = answer.html ?? '';
|
|
343
|
+
const copiedMarkdown = await withRetries(async () => {
|
|
344
|
+
const attempt = await captureAssistantMarkdown(Runtime, answer.meta, logger);
|
|
345
|
+
if (!attempt) {
|
|
346
|
+
throw new Error('copy-missing');
|
|
347
|
+
}
|
|
348
|
+
return attempt;
|
|
349
|
+
}, {
|
|
350
|
+
retries: 2,
|
|
351
|
+
delayMs: 350,
|
|
352
|
+
onRetry: (attempt, error) => {
|
|
353
|
+
if (options.verbose) {
|
|
354
|
+
logger(`[retry] Markdown capture attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
}).catch(() => null);
|
|
358
|
+
answerMarkdown = copiedMarkdown ?? answerText;
|
|
359
|
+
stopThinkingMonitor?.();
|
|
360
|
+
const durationMs = Date.now() - startedAt;
|
|
361
|
+
const answerChars = answerText.length;
|
|
362
|
+
const answerTokens = estimateTokenCount(answerMarkdown);
|
|
363
|
+
return {
|
|
364
|
+
answerText,
|
|
365
|
+
answerMarkdown,
|
|
366
|
+
answerHtml: answerHtml.length > 0 ? answerHtml : undefined,
|
|
367
|
+
tookMs: durationMs,
|
|
368
|
+
answerTokens,
|
|
369
|
+
answerChars,
|
|
370
|
+
chromePid: undefined,
|
|
371
|
+
chromePort: port,
|
|
372
|
+
userDataDir: undefined,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
377
|
+
stopThinkingMonitor?.();
|
|
378
|
+
const socketClosed = connectionClosedUnexpectedly || isWebSocketClosureError(normalizedError);
|
|
379
|
+
connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
|
|
380
|
+
if (!socketClosed) {
|
|
381
|
+
logger(`Failed to complete ChatGPT run: ${normalizedError.message}`);
|
|
382
|
+
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === '1') && normalizedError.stack) {
|
|
383
|
+
logger(normalizedError.stack);
|
|
384
|
+
}
|
|
385
|
+
throw normalizedError;
|
|
386
|
+
}
|
|
387
|
+
throw new Error('Remote Chrome connection lost before Oracle finished.', {
|
|
388
|
+
cause: normalizedError,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
try {
|
|
393
|
+
if (!connectionClosedUnexpectedly && client) {
|
|
394
|
+
await client.close();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// ignore
|
|
399
|
+
}
|
|
400
|
+
// Don't kill remote Chrome - it's not ours to manage
|
|
401
|
+
const totalSeconds = (Date.now() - startedAt) / 1000;
|
|
402
|
+
logger(`Remote session complete • ${totalSeconds.toFixed(1)}s total`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
export { estimateTokenCount } from './utils.js';
|
|
406
|
+
export { resolveBrowserConfig, DEFAULT_BROWSER_CONFIG } from './config.js';
|
|
407
|
+
export { syncCookies } from './cookies.js';
|
|
408
|
+
export { navigateToChatGPT, ensureNotBlocked, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, } from './pageActions.js';
|
|
409
|
+
function isWebSocketClosureError(error) {
|
|
410
|
+
const message = error.message.toLowerCase();
|
|
411
|
+
return (message.includes('websocket connection closed') ||
|
|
412
|
+
message.includes('websocket is closed') ||
|
|
413
|
+
message.includes('websocket error') ||
|
|
414
|
+
message.includes('target closed'));
|
|
415
|
+
}
|
|
416
|
+
export function formatThinkingLog(startedAt, now, message, locatorSuffix) {
|
|
417
|
+
const elapsedMs = now - startedAt;
|
|
418
|
+
const elapsedText = formatElapsed(elapsedMs);
|
|
419
|
+
const progress = Math.min(1, elapsedMs / 600_000); // soft target: 10 minutes
|
|
420
|
+
const pct = Math.round(progress * 100)
|
|
421
|
+
.toString()
|
|
422
|
+
.padStart(3, ' ');
|
|
423
|
+
const statusLabel = message ? ` — ${message}` : '';
|
|
424
|
+
return `${pct}% [${elapsedText} / ~10m]${statusLabel}${locatorSuffix}`;
|
|
425
|
+
}
|
|
426
|
+
function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false) {
|
|
427
|
+
let stopped = false;
|
|
428
|
+
let pending = false;
|
|
429
|
+
let lastMessage = null;
|
|
430
|
+
const startedAt = Date.now();
|
|
431
|
+
const interval = setInterval(async () => {
|
|
432
|
+
// biome-ignore lint/nursery/noUnnecessaryConditions: stop flag flips asynchronously
|
|
433
|
+
if (stopped || pending) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
pending = true;
|
|
437
|
+
try {
|
|
438
|
+
const nextMessage = await readThinkingStatus(Runtime);
|
|
439
|
+
if (nextMessage && nextMessage !== lastMessage) {
|
|
440
|
+
lastMessage = nextMessage;
|
|
441
|
+
let locatorSuffix = '';
|
|
442
|
+
if (includeDiagnostics) {
|
|
443
|
+
try {
|
|
444
|
+
const snapshot = await readAssistantSnapshot(Runtime);
|
|
445
|
+
locatorSuffix = ` | assistant-turn=${snapshot ? 'present' : 'missing'}`;
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
locatorSuffix = ' | assistant-turn=error';
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
logger(formatThinkingLog(startedAt, Date.now(), nextMessage, locatorSuffix));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
// ignore DOM polling errors
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
pending = false;
|
|
459
|
+
}
|
|
460
|
+
}, 1500);
|
|
461
|
+
interval.unref?.();
|
|
462
|
+
return () => {
|
|
463
|
+
// biome-ignore lint/nursery/noUnnecessaryConditions: multiple callers may race to stop
|
|
464
|
+
if (stopped) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
stopped = true;
|
|
468
|
+
clearInterval(interval);
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
async function readThinkingStatus(Runtime) {
|
|
472
|
+
const expression = buildThinkingStatusExpression();
|
|
473
|
+
try {
|
|
474
|
+
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
475
|
+
const value = typeof result.value === 'string' ? result.value.trim() : '';
|
|
476
|
+
const sanitized = sanitizeThinkingText(value);
|
|
477
|
+
return sanitized || null;
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function sanitizeThinkingText(raw) {
|
|
484
|
+
if (!raw) {
|
|
485
|
+
return '';
|
|
486
|
+
}
|
|
487
|
+
const trimmed = raw.trim();
|
|
488
|
+
const prefixPattern = /^(pro thinking)\s*[•:\-–—]*\s*/i;
|
|
489
|
+
if (prefixPattern.test(trimmed)) {
|
|
490
|
+
return trimmed.replace(prefixPattern, '').trim();
|
|
491
|
+
}
|
|
492
|
+
return trimmed;
|
|
493
|
+
}
|
|
494
|
+
function buildThinkingStatusExpression() {
|
|
495
|
+
const selectors = [
|
|
496
|
+
'span.loading-shimmer',
|
|
497
|
+
'span.flex.items-center.gap-1.truncate.text-start.align-middle.text-token-text-tertiary',
|
|
498
|
+
'[data-testid*="thinking"]',
|
|
499
|
+
'[data-testid*="reasoning"]',
|
|
500
|
+
'[role="status"]',
|
|
501
|
+
'[aria-live="polite"]',
|
|
502
|
+
];
|
|
503
|
+
const keywords = ['pro thinking', 'thinking', 'reasoning', 'clarifying', 'planning', 'drafting', 'summarizing'];
|
|
504
|
+
const selectorLiteral = JSON.stringify(selectors);
|
|
505
|
+
const keywordsLiteral = JSON.stringify(keywords);
|
|
506
|
+
return `(() => {
|
|
507
|
+
const selectors = ${selectorLiteral};
|
|
508
|
+
const keywords = ${keywordsLiteral};
|
|
509
|
+
const nodes = new Set();
|
|
510
|
+
for (const selector of selectors) {
|
|
511
|
+
document.querySelectorAll(selector).forEach((node) => nodes.add(node));
|
|
512
|
+
}
|
|
513
|
+
document.querySelectorAll('[data-testid]').forEach((node) => nodes.add(node));
|
|
514
|
+
for (const node of nodes) {
|
|
515
|
+
if (!(node instanceof HTMLElement)) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const text = node.textContent?.trim();
|
|
519
|
+
if (!text) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const classLabel = (node.className || '').toLowerCase();
|
|
523
|
+
const dataLabel = ((node.getAttribute('data-testid') || '') + ' ' + (node.getAttribute('aria-label') || ''))
|
|
524
|
+
.toLowerCase();
|
|
525
|
+
const normalizedText = text.toLowerCase();
|
|
526
|
+
const matches = keywords.some((keyword) =>
|
|
527
|
+
normalizedText.includes(keyword) || classLabel.includes(keyword) || dataLabel.includes(keyword)
|
|
528
|
+
);
|
|
529
|
+
if (matches) {
|
|
530
|
+
const shimmerChild = node.querySelector(
|
|
531
|
+
'span.flex.items-center.gap-1.truncate.text-start.align-middle.text-token-text-tertiary',
|
|
532
|
+
);
|
|
533
|
+
if (shimmerChild?.textContent?.trim()) {
|
|
534
|
+
return shimmerChild.textContent.trim();
|
|
535
|
+
}
|
|
536
|
+
return text.trim();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
})()`;
|
|
541
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const defaultLabels = [
|
|
2
|
+
{ service: 'Chrome Safe Storage', account: 'Chrome' },
|
|
3
|
+
{ service: 'Chromium Safe Storage', account: 'Chromium' },
|
|
4
|
+
{ service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' },
|
|
5
|
+
{ service: 'Brave Safe Storage', account: 'Brave' },
|
|
6
|
+
{ service: 'Vivaldi Safe Storage', account: 'Vivaldi' },
|
|
7
|
+
];
|
|
8
|
+
function loadEnvLabels() {
|
|
9
|
+
const raw = process.env.ORACLE_KEYCHAIN_LABELS;
|
|
10
|
+
if (!raw)
|
|
11
|
+
return [];
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
if (Array.isArray(parsed)) {
|
|
15
|
+
return parsed
|
|
16
|
+
.map((entry) => (entry && typeof entry === 'object' ? entry : null))
|
|
17
|
+
.filter((entry) => Boolean(entry?.service && entry?.account));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// ignore invalid env payload
|
|
22
|
+
}
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const fallbackLabels = [...loadEnvLabels(), ...defaultLabels];
|
|
26
|
+
const disableKeytar = process.env.ORACLE_DISABLE_KEYTAR === '1' || process.env.CI === 'true';
|
|
27
|
+
let keytar;
|
|
28
|
+
if (disableKeytar) {
|
|
29
|
+
keytar = {
|
|
30
|
+
getPassword: async () => null,
|
|
31
|
+
setPassword: async () => undefined,
|
|
32
|
+
deletePassword: async () => false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const keytarModule = await import('keytar');
|
|
37
|
+
keytar = (keytarModule.default ?? keytarModule);
|
|
38
|
+
const originalGetPassword = keytar.getPassword.bind(keytar);
|
|
39
|
+
keytar.getPassword = async (service, account) => {
|
|
40
|
+
const primary = await originalGetPassword(service, account);
|
|
41
|
+
if (primary) {
|
|
42
|
+
return primary;
|
|
43
|
+
}
|
|
44
|
+
for (const label of fallbackLabels) {
|
|
45
|
+
if (label.service === service && label.account === account) {
|
|
46
|
+
continue; // already tried
|
|
47
|
+
}
|
|
48
|
+
const value = await originalGetPassword(label.service, label.account);
|
|
49
|
+
if (value) {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export default keytar;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady } from './actions/navigation.js';
|
|
2
|
+
export { ensureModelSelection } from './actions/modelSelection.js';
|
|
3
|
+
export { submitPrompt } from './actions/promptComposer.js';
|
|
4
|
+
export { uploadAttachmentFile, waitForAttachmentCompletion } from './actions/attachments.js';
|
|
5
|
+
export { waitForAssistantResponse, readAssistantSnapshot, captureAssistantMarkdown, buildAssistantExtractorForTest, buildConversationDebugExpressionForTest, } from './actions/assistantResponse.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { formatFileSection } from '../oracle/markdown.js';
|
|
2
|
+
export function buildAttachmentPlan(sections, { inlineFiles, bundleRequested, maxAttachments = 10, }) {
|
|
3
|
+
if (inlineFiles) {
|
|
4
|
+
const inlineLines = [];
|
|
5
|
+
sections.forEach((section) => {
|
|
6
|
+
inlineLines.push(formatFileSection(section.displayPath, section.content).trimEnd(), '');
|
|
7
|
+
});
|
|
8
|
+
const inlineBlock = inlineLines.join('\n').trim();
|
|
9
|
+
return {
|
|
10
|
+
mode: 'inline',
|
|
11
|
+
inlineBlock,
|
|
12
|
+
inlineFileCount: sections.length,
|
|
13
|
+
attachments: [],
|
|
14
|
+
shouldBundle: false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const attachments = sections.map((section) => ({
|
|
18
|
+
path: section.absolutePath,
|
|
19
|
+
displayPath: section.displayPath,
|
|
20
|
+
sizeBytes: Buffer.byteLength(section.content, 'utf8'),
|
|
21
|
+
}));
|
|
22
|
+
const shouldBundle = bundleRequested || attachments.length > maxAttachments;
|
|
23
|
+
return {
|
|
24
|
+
mode: shouldBundle ? 'bundle' : 'upload',
|
|
25
|
+
inlineBlock: '',
|
|
26
|
+
inlineFileCount: 0,
|
|
27
|
+
attachments,
|
|
28
|
+
shouldBundle,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function buildCookiePlan(config) {
|
|
32
|
+
if (config?.inlineCookies && config.inlineCookies.length > 0) {
|
|
33
|
+
const source = config.inlineCookiesSource ?? 'inline';
|
|
34
|
+
return { type: 'inline', description: `Cookies: inline payload (${config.inlineCookies.length}) via ${source}.` };
|
|
35
|
+
}
|
|
36
|
+
if (config?.cookieSync === false) {
|
|
37
|
+
return { type: 'disabled', description: 'Cookies: sync disabled (--browser-no-cookie-sync).' };
|
|
38
|
+
}
|
|
39
|
+
const allowlist = config?.cookieNames && config.cookieNames.length > 0
|
|
40
|
+
? config.cookieNames.join(', ')
|
|
41
|
+
: 'all from Chrome profile';
|
|
42
|
+
return { type: 'copy', description: `Cookies: copy from Chrome (${allowlist}).` };
|
|
43
|
+
}
|