@steipete/oracle 0.18.0 → 0.19.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.
Files changed (159) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +65 -63
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/archiveConversation.js +31 -7
  5. package/dist/src/browser/actions/assistantResponse.js +43 -1
  6. package/dist/src/browser/actions/attachmentContext.js +239 -0
  7. package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
  8. package/dist/src/browser/actions/attachmentEvidence.js +78 -0
  9. package/dist/src/browser/actions/attachmentProgress.js +41 -0
  10. package/dist/src/browser/actions/attachmentPrompt.js +130 -0
  11. package/dist/src/browser/actions/attachments.js +306 -355
  12. package/dist/src/browser/actions/deepResearch.js +2 -1
  13. package/dist/src/browser/actions/modelSelection.js +13 -3
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +361 -186
  16. package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
  17. package/dist/src/browser/actions/thinkingStatus.js +2 -5
  18. package/dist/src/browser/actions/thinkingTime.js +119 -16
  19. package/dist/src/browser/attachRunning.js +46 -2
  20. package/dist/src/browser/attachmentValidation.js +53 -0
  21. package/dist/src/browser/cancellation.js +105 -0
  22. package/dist/src/browser/chatgptImages.js +2 -0
  23. package/dist/src/browser/chatgptThrottle.js +104 -0
  24. package/dist/src/browser/chromeLifecycle.js +267 -43
  25. package/dist/src/browser/config.js +13 -0
  26. package/dist/src/browser/detect.js +1 -1
  27. package/dist/src/browser/index.js +444 -261
  28. package/dist/src/browser/modelDisplay.js +9 -0
  29. package/dist/src/browser/pageActions.js +2 -2
  30. package/dist/src/browser/profileState.js +114 -9
  31. package/dist/src/browser/projectSourcesRunner.js +83 -15
  32. package/dist/src/browser/prompt.js +272 -113
  33. package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
  34. package/dist/src/browser/reattach.js +10 -3
  35. package/dist/src/browser/sessionRunner.js +71 -11
  36. package/dist/src/browser/tabLeaseRegistry.js +414 -38
  37. package/dist/src/browser/utils.js +20 -2
  38. package/dist/src/cli/browserConfig.js +3 -1
  39. package/dist/src/cli/browserDefaults.js +10 -0
  40. package/dist/src/cli/browserTabs.js +12 -29
  41. package/dist/src/cli/detachedSession.js +69 -0
  42. package/dist/src/cli/dryRun.js +58 -48
  43. package/dist/src/cli/harvestIntegrity.js +140 -0
  44. package/dist/src/cli/serveBrowserConfig.js +24 -0
  45. package/dist/src/cli/sessionDisplay.js +9 -2
  46. package/dist/src/cli/sessionRunner.js +91 -7
  47. package/dist/src/config.js +1 -0
  48. package/dist/src/gemini-web/client.js +67 -21
  49. package/dist/src/gemini-web/executor.js +4 -0
  50. package/dist/src/gemini-web/http.js +15 -0
  51. package/dist/src/gemini-web/models.js +15 -11
  52. package/dist/src/mcp/server.js +11 -14
  53. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  54. package/dist/src/mcp/tools/consult.js +84 -22
  55. package/dist/src/mcp/tools/projectSources.js +4 -6
  56. package/dist/src/mcp/tools/sessionResources.js +1 -1
  57. package/dist/src/mcp/tools/sessions.js +2 -2
  58. package/dist/src/mcp/tools/wait.js +182 -0
  59. package/dist/src/mcp/types.js +7 -0
  60. package/dist/src/oracle/errors.js +11 -0
  61. package/dist/src/oracle/providerRoutePlan.js +3 -2
  62. package/dist/src/remote/client.js +70 -6
  63. package/dist/src/remote/health.js +6 -2
  64. package/dist/src/remote/runSlots.js +83 -0
  65. package/dist/src/remote/server.js +345 -71
  66. package/dist/src/sessionManager.js +36 -1
  67. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  68. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  69. package/package.json +25 -23
  70. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  71. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  72. package/dist/bin/oracle.js +0 -683
  73. package/dist/docs-site/.nojekyll +0 -0
  74. package/dist/docs-site/CNAME +0 -1
  75. package/dist/docs-site/RELEASING.html +0 -410
  76. package/dist/docs-site/agents.html +0 -374
  77. package/dist/docs-site/anthropic.html +0 -368
  78. package/dist/docs-site/bridge.html +0 -400
  79. package/dist/docs-site/browser-mode.html +0 -588
  80. package/dist/docs-site/chromium-forks.html +0 -347
  81. package/dist/docs-site/cli-reference.html +0 -346
  82. package/dist/docs-site/configuration.html +0 -436
  83. package/dist/docs-site/favicon.svg +0 -14
  84. package/dist/docs-site/followup.html +0 -368
  85. package/dist/docs-site/gemini.html +0 -379
  86. package/dist/docs-site/grok.html +0 -325
  87. package/dist/docs-site/index.html +0 -360
  88. package/dist/docs-site/install.html +0 -335
  89. package/dist/docs-site/linux.html +0 -321
  90. package/dist/docs-site/llms.txt +0 -43
  91. package/dist/docs-site/manual-tests.html +0 -595
  92. package/dist/docs-site/mcp.html +0 -368
  93. package/dist/docs-site/multimodel.html +0 -364
  94. package/dist/docs-site/mythical-pro-agents.html +0 -360
  95. package/dist/docs-site/notifier.html +0 -338
  96. package/dist/docs-site/openai-endpoints.html +0 -387
  97. package/dist/docs-site/openrouter.html +0 -344
  98. package/dist/docs-site/quickstart.html +0 -369
  99. package/dist/docs-site/refactor/ux.html +0 -532
  100. package/dist/docs-site/sessions.html +0 -388
  101. package/dist/docs-site/social-card.png +0 -0
  102. package/dist/docs-site/social-card.svg +0 -79
  103. package/dist/docs-site/spec.html +0 -363
  104. package/dist/docs-site/testing.html +0 -320
  105. package/dist/docs-site/tui-debug.html +0 -326
  106. package/dist/docs-site/windows-work.html +0 -323
  107. package/dist/docs-site/windows.html +0 -320
  108. package/dist/markdansi/types/index.js +0 -4
  109. package/dist/oracle/bin/oracle-cli.js +0 -472
  110. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  111. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  112. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  113. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  114. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  115. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  116. package/dist/oracle/src/browser/config.js +0 -33
  117. package/dist/oracle/src/browser/constants.js +0 -40
  118. package/dist/oracle/src/browser/cookies.js +0 -210
  119. package/dist/oracle/src/browser/domDebug.js +0 -36
  120. package/dist/oracle/src/browser/index.js +0 -331
  121. package/dist/oracle/src/browser/pageActions.js +0 -5
  122. package/dist/oracle/src/browser/prompt.js +0 -88
  123. package/dist/oracle/src/browser/promptSummary.js +0 -20
  124. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  125. package/dist/oracle/src/browser/types.js +0 -1
  126. package/dist/oracle/src/browser/utils.js +0 -62
  127. package/dist/oracle/src/browserMode.js +0 -1
  128. package/dist/oracle/src/cli/browserConfig.js +0 -44
  129. package/dist/oracle/src/cli/dryRun.js +0 -59
  130. package/dist/oracle/src/cli/engine.js +0 -17
  131. package/dist/oracle/src/cli/errorUtils.js +0 -9
  132. package/dist/oracle/src/cli/help.js +0 -70
  133. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  134. package/dist/oracle/src/cli/options.js +0 -103
  135. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  136. package/dist/oracle/src/cli/rootAlias.js +0 -30
  137. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  138. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  139. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  140. package/dist/oracle/src/heartbeat.js +0 -43
  141. package/dist/oracle/src/oracle/client.js +0 -48
  142. package/dist/oracle/src/oracle/config.js +0 -29
  143. package/dist/oracle/src/oracle/errors.js +0 -101
  144. package/dist/oracle/src/oracle/files.js +0 -220
  145. package/dist/oracle/src/oracle/format.js +0 -33
  146. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  147. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  148. package/dist/oracle/src/oracle/request.js +0 -48
  149. package/dist/oracle/src/oracle/run.js +0 -444
  150. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  151. package/dist/oracle/src/oracle/types.js +0 -1
  152. package/dist/oracle/src/oracle.js +0 -9
  153. package/dist/oracle/src/sessionManager.js +0 -205
  154. package/dist/oracle/src/version.js +0 -39
  155. package/dist/scripts/chrome/browser-tools.js +0 -295
  156. package/dist/src/browser/chromeCookies.js +0 -312
  157. package/dist/src/browser/keytarShim.js +0 -56
  158. package/dist/src/browser/profileSync.js +0 -141
  159. package/dist/src/browser/windowsCookies.js +0 -219
@@ -1,210 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
3
- import { fileURLToPath } from 'node:url';
4
- import { COOKIE_URLS } from './constants.js';
5
- export class ChromeCookieSyncError extends Error {
6
- }
7
- export async function syncCookies(Network, url, profile, logger, allowErrors = false) {
8
- try {
9
- const cookies = await readChromeCookies(url, profile);
10
- if (!cookies.length) {
11
- return 0;
12
- }
13
- let applied = 0;
14
- for (const cookie of cookies) {
15
- const cookieWithUrl = { ...cookie };
16
- if (!cookieWithUrl.domain || cookieWithUrl.domain === 'localhost') {
17
- cookieWithUrl.url = url;
18
- }
19
- else if (!cookieWithUrl.domain.startsWith('.')) {
20
- cookieWithUrl.url = `https://${cookieWithUrl.domain}`;
21
- }
22
- try {
23
- const result = await Network.setCookie(cookieWithUrl);
24
- if (result?.success) {
25
- applied += 1;
26
- }
27
- }
28
- catch (error) {
29
- const message = error instanceof Error ? error.message : String(error);
30
- logger(`Failed to set cookie ${cookie.name}: ${message}`);
31
- }
32
- }
33
- return applied;
34
- }
35
- catch (error) {
36
- const message = error instanceof Error ? error.message : String(error);
37
- if (allowErrors) {
38
- logger(`Cookie sync failed (continuing with override): ${message}`);
39
- return 0;
40
- }
41
- throw error instanceof ChromeCookieSyncError ? error : new ChromeCookieSyncError(message);
42
- }
43
- }
44
- async function readChromeCookies(url, profile) {
45
- const chromeModule = await loadChromeCookiesModule();
46
- const urlsToCheck = Array.from(new Set([stripQuery(url), ...COOKIE_URLS]));
47
- const merged = new Map();
48
- for (const candidateUrl of urlsToCheck) {
49
- let rawCookies;
50
- rawCookies = await chromeModule.getCookiesPromised(candidateUrl, 'puppeteer', profile ?? undefined);
51
- if (!Array.isArray(rawCookies)) {
52
- continue;
53
- }
54
- const fallbackHostname = new URL(candidateUrl).hostname;
55
- for (const cookie of rawCookies) {
56
- const normalized = normalizeCookie(cookie, fallbackHostname);
57
- if (!normalized) {
58
- continue;
59
- }
60
- const key = `${normalized.domain ?? fallbackHostname}:${normalized.name}`;
61
- if (!merged.has(key)) {
62
- merged.set(key, normalized);
63
- }
64
- }
65
- }
66
- return Array.from(merged.values());
67
- }
68
- function normalizeCookie(cookie, fallbackHost) {
69
- if (!cookie?.name) {
70
- return null;
71
- }
72
- const domain = cookie.domain?.startsWith('.') ? cookie.domain : cookie.domain ?? fallbackHost;
73
- const expires = normalizeExpiration(cookie.expires);
74
- const secure = typeof cookie.Secure === 'boolean' ? cookie.Secure : true;
75
- const httpOnly = typeof cookie.HttpOnly === 'boolean' ? cookie.HttpOnly : false;
76
- return {
77
- name: cookie.name,
78
- value: cookie.value ?? '',
79
- domain,
80
- path: cookie.path ?? '/',
81
- expires,
82
- secure,
83
- httpOnly,
84
- };
85
- }
86
- function stripQuery(url) {
87
- try {
88
- const parsed = new URL(url);
89
- parsed.hash = '';
90
- parsed.search = '';
91
- return parsed.toString();
92
- }
93
- catch {
94
- return url;
95
- }
96
- }
97
- function normalizeExpiration(expires) {
98
- if (!expires || Number.isNaN(expires)) {
99
- return undefined;
100
- }
101
- const value = Number(expires);
102
- if (value <= 0) {
103
- return undefined;
104
- }
105
- if (value > 1_000_000_000_000) {
106
- return Math.round(value / 1_000_000 - 11644473600);
107
- }
108
- if (value > 1_000_000_000) {
109
- return Math.round(value / 1000);
110
- }
111
- return Math.round(value);
112
- }
113
- const WORKSPACE_MANIFEST_PATH = fileURLToPath(new URL('../../pnpm-workspace.yaml', import.meta.url));
114
- const HAS_PNPM_WORKSPACE = existsSync(WORKSPACE_MANIFEST_PATH);
115
- const SQLITE_NODE_PATTERN = /node_sqlite3\.node/i;
116
- const SQLITE_BINDINGS_PATTERN = /bindings file/i;
117
- const SQLITE_SELF_REGISTER_PATTERN = /Module did not self-register/i;
118
- const SQLITE_BINDING_HINT = [
119
- 'Chrome cookie sync needs sqlite3 bindings for Node 25.',
120
- 'If the automatic rebuild fails, run:',
121
- ' PYTHON=/usr/bin/python3 npm_config_build_from_source=1 pnpm rebuild chrome-cookies-secure sqlite3 keytar --workspace-root',
122
- ].join('\n');
123
- let attemptedSqliteRebuild = false;
124
- async function loadChromeCookiesModule() {
125
- let imported;
126
- try {
127
- imported = await import('chrome-cookies-secure');
128
- }
129
- catch (error) {
130
- console.warn('Failed to load chrome-cookies-secure to copy cookies:', error);
131
- if (isSqliteBindingError(error)) {
132
- const rebuilt = await attemptSqliteRebuild();
133
- if (rebuilt) {
134
- return loadChromeCookiesModule();
135
- }
136
- console.warn(SQLITE_BINDING_HINT);
137
- }
138
- else {
139
- console.warn('If this persists, run `pnpm rebuild chrome-cookies-secure sqlite3 keytar --workspace-root`.');
140
- }
141
- throw new ChromeCookieSyncError('Unable to load chrome-cookies-secure. Cookie copy is required.');
142
- }
143
- const secureModule = resolveChromeCookieModule(imported);
144
- if (!secureModule) {
145
- console.warn('chrome-cookies-secure does not expose getCookiesPromised(); skipping cookie copy.');
146
- throw new ChromeCookieSyncError('chrome-cookies-secure did not expose getCookiesPromised');
147
- }
148
- return secureModule;
149
- }
150
- function resolveChromeCookieModule(candidate) {
151
- if (hasGetCookiesPromised(candidate)) {
152
- return candidate;
153
- }
154
- if (typeof candidate === 'object' && candidate !== null) {
155
- const defaultExport = Reflect.get(candidate, 'default');
156
- if (hasGetCookiesPromised(defaultExport)) {
157
- return defaultExport;
158
- }
159
- }
160
- return null;
161
- }
162
- function hasGetCookiesPromised(value) {
163
- return Boolean(value && typeof value.getCookiesPromised === 'function');
164
- }
165
- function isSqliteBindingError(error) {
166
- if (!(error instanceof Error)) {
167
- return false;
168
- }
169
- const message = error.message ?? '';
170
- return (SQLITE_NODE_PATTERN.test(message) ||
171
- SQLITE_BINDINGS_PATTERN.test(message) ||
172
- SQLITE_SELF_REGISTER_PATTERN.test(message));
173
- }
174
- async function attemptSqliteRebuild() {
175
- if (attemptedSqliteRebuild) {
176
- return false;
177
- }
178
- attemptedSqliteRebuild = true;
179
- if (process.env.ORACLE_ALLOW_SQLITE_REBUILD !== '1') {
180
- console.warn('[oracle] sqlite3 bindings missing. Set ORACLE_ALLOW_SQLITE_REBUILD=1 if you want Oracle to attempt an automatic rebuild, or run `pnpm rebuild chrome-cookies-secure sqlite3 keytar --workspace-root` manually.');
181
- return false;
182
- }
183
- const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
184
- const args = ['rebuild', 'chrome-cookies-secure', 'sqlite3', 'keytar'];
185
- if (HAS_PNPM_WORKSPACE) {
186
- args.push('--workspace-root');
187
- }
188
- const childEnv = { ...process.env };
189
- childEnv.npm_config_build_from_source = '1';
190
- childEnv.PYTHON = childEnv.PYTHON ?? '/usr/bin/python3';
191
- console.warn('[oracle] Attempting to rebuild sqlite3 bindings automatically…');
192
- console.warn(`[oracle] Running: npm_config_build_from_source=1 PYTHON=${childEnv.PYTHON} ${pnpmCommand} ${args.join(' ')}`);
193
- return new Promise((resolve) => {
194
- const child = spawn(pnpmCommand, args, { stdio: 'inherit', env: childEnv });
195
- child.on('exit', (code) => {
196
- if (code === 0) {
197
- console.warn('[oracle] sqlite3 rebuild completed successfully.');
198
- resolve(true);
199
- }
200
- else {
201
- console.warn('[oracle] sqlite3 rebuild failed with exit code', code ?? 0);
202
- resolve(false);
203
- }
204
- });
205
- child.on('error', (error) => {
206
- console.warn('[oracle] Unable to spawn pnpm to rebuild sqlite3:', error);
207
- resolve(false);
208
- });
209
- });
210
- }
@@ -1,36 +0,0 @@
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
- }
@@ -1,331 +0,0 @@
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 } from './chromeLifecycle.js';
6
- import { syncCookies } from './cookies.js';
7
- import { navigateToChatGPT, ensureNotBlocked, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, readAssistantSnapshot, } from './pageActions.js';
8
- import { estimateTokenCount, withRetries } from './utils.js';
9
- import { formatElapsed } from '../oracle/format.js';
10
- export { CHATGPT_URL, DEFAULT_MODEL_TARGET } from './constants.js';
11
- export { parseDuration, delay } from './utils.js';
12
- export async function runBrowserMode(options) {
13
- const promptText = options.prompt?.trim();
14
- if (!promptText) {
15
- throw new Error('Prompt text is required when using browser mode.');
16
- }
17
- const attachments = options.attachments ?? [];
18
- const config = resolveBrowserConfig(options.config);
19
- const logger = options.log ?? ((_message) => { });
20
- if (logger.verbose === undefined) {
21
- logger.verbose = Boolean(config.debug);
22
- }
23
- if (logger.sessionLog === undefined && options.log?.sessionLog) {
24
- logger.sessionLog = options.log.sessionLog;
25
- }
26
- if (config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === '1') {
27
- logger(`[browser-mode] config: ${JSON.stringify({
28
- ...config,
29
- promptLength: promptText.length,
30
- })}`);
31
- }
32
- const userDataDir = await mkdtemp(path.join(os.tmpdir(), 'oracle-browser-'));
33
- logger(`Created temporary Chrome profile at ${userDataDir}`);
34
- const chrome = await launchChrome(config, userDataDir, logger);
35
- let removeTerminationHooks = null;
36
- try {
37
- removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, config.keepBrowser, logger);
38
- }
39
- catch {
40
- // ignore failure; cleanup still happens below
41
- }
42
- let client = null;
43
- const startedAt = Date.now();
44
- let answerText = '';
45
- let answerMarkdown = '';
46
- let answerHtml = '';
47
- let runStatus = 'attempted';
48
- let connectionClosedUnexpectedly = false;
49
- let stopThinkingMonitor = null;
50
- try {
51
- client = await connectToChrome(chrome.port, logger);
52
- const markConnectionLost = () => {
53
- connectionClosedUnexpectedly = true;
54
- };
55
- client.on('disconnect', markConnectionLost);
56
- const { Network, Page, Runtime, Input, DOM } = client;
57
- if (!config.headless && config.hideWindow) {
58
- await hideChromeWindow(chrome, logger);
59
- }
60
- const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
61
- if (DOM && typeof DOM.enable === 'function') {
62
- domainEnablers.push(DOM.enable());
63
- }
64
- await Promise.all(domainEnablers);
65
- await Network.clearBrowserCookies();
66
- if (config.cookieSync) {
67
- const cookieCount = await syncCookies(Network, config.url, config.chromeProfile, logger, config.allowCookieErrors ?? false);
68
- logger(cookieCount > 0
69
- ? `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ?? 'Default'}`
70
- : 'No Chrome cookies found; continuing without session reuse');
71
- }
72
- else {
73
- logger('Skipping Chrome cookie sync (--browser-no-cookie-sync)');
74
- }
75
- await navigateToChatGPT(Page, Runtime, config.url, logger);
76
- await ensureNotBlocked(Runtime, config.headless, logger);
77
- await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
78
- logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
79
- if (config.desiredModel) {
80
- await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger), {
81
- retries: 2,
82
- delayMs: 300,
83
- onRetry: (attempt, error) => {
84
- if (options.verbose) {
85
- logger(`[retry] Model picker attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
86
- }
87
- },
88
- });
89
- await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
90
- logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
91
- }
92
- if (attachments.length > 0) {
93
- if (!DOM) {
94
- throw new Error('Chrome DOM domain unavailable while uploading attachments.');
95
- }
96
- for (const attachment of attachments) {
97
- logger(`Uploading attachment: ${attachment.displayPath}`);
98
- await uploadAttachmentFile({ runtime: Runtime, dom: DOM }, attachment, logger);
99
- }
100
- const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
101
- await waitForAttachmentCompletion(Runtime, waitBudget, logger);
102
- logger('All attachments uploaded');
103
- }
104
- await submitPrompt({ runtime: Runtime, input: Input }, promptText, logger);
105
- stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
106
- const answer = await waitForAssistantResponse(Runtime, config.timeoutMs, logger);
107
- answerText = answer.text;
108
- answerHtml = answer.html ?? '';
109
- const copiedMarkdown = await withRetries(async () => {
110
- const attempt = await captureAssistantMarkdown(Runtime, answer.meta, logger);
111
- if (!attempt) {
112
- throw new Error('copy-missing');
113
- }
114
- return attempt;
115
- }, {
116
- retries: 2,
117
- delayMs: 350,
118
- onRetry: (attempt, error) => {
119
- if (options.verbose) {
120
- logger(`[retry] Markdown capture attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
121
- }
122
- },
123
- }).catch(() => null);
124
- answerMarkdown = copiedMarkdown ?? answerText;
125
- stopThinkingMonitor?.();
126
- runStatus = 'complete';
127
- const durationMs = Date.now() - startedAt;
128
- const answerChars = answerText.length;
129
- const answerTokens = estimateTokenCount(answerMarkdown);
130
- return {
131
- answerText,
132
- answerMarkdown,
133
- answerHtml: answerHtml.length > 0 ? answerHtml : undefined,
134
- tookMs: durationMs,
135
- answerTokens,
136
- answerChars,
137
- chromePid: chrome.pid,
138
- chromePort: chrome.port,
139
- userDataDir,
140
- };
141
- }
142
- catch (error) {
143
- const normalizedError = error instanceof Error ? error : new Error(String(error));
144
- stopThinkingMonitor?.();
145
- const socketClosed = connectionClosedUnexpectedly || isWebSocketClosureError(normalizedError);
146
- connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
147
- if (!socketClosed) {
148
- logger(`Failed to complete ChatGPT run: ${normalizedError.message}`);
149
- if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === '1') && normalizedError.stack) {
150
- logger(normalizedError.stack);
151
- }
152
- throw normalizedError;
153
- }
154
- if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === '1') && normalizedError.stack) {
155
- logger(`Chrome window closed before completion: ${normalizedError.message}`);
156
- logger(normalizedError.stack);
157
- }
158
- throw new Error('Chrome window closed before Oracle finished. Please keep it open until completion.', {
159
- cause: normalizedError,
160
- });
161
- }
162
- finally {
163
- try {
164
- if (!connectionClosedUnexpectedly) {
165
- await client?.close();
166
- }
167
- }
168
- catch {
169
- // ignore
170
- }
171
- removeTerminationHooks?.();
172
- if (!config.keepBrowser) {
173
- if (!connectionClosedUnexpectedly) {
174
- try {
175
- await chrome.kill();
176
- }
177
- catch {
178
- // ignore kill failures
179
- }
180
- }
181
- await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
182
- if (!connectionClosedUnexpectedly) {
183
- const totalSeconds = (Date.now() - startedAt) / 1000;
184
- logger(`Cleanup ${runStatus} • ${totalSeconds.toFixed(1)}s total`);
185
- }
186
- }
187
- else if (!connectionClosedUnexpectedly) {
188
- logger(`Chrome left running on port ${chrome.port} with profile ${userDataDir}`);
189
- }
190
- }
191
- }
192
- export { estimateTokenCount } from './utils.js';
193
- export { resolveBrowserConfig, DEFAULT_BROWSER_CONFIG } from './config.js';
194
- export { syncCookies } from './cookies.js';
195
- export { navigateToChatGPT, ensureNotBlocked, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, } from './pageActions.js';
196
- function isWebSocketClosureError(error) {
197
- const message = error.message.toLowerCase();
198
- return (message.includes('websocket connection closed') ||
199
- message.includes('websocket is closed') ||
200
- message.includes('websocket error') ||
201
- message.includes('target closed'));
202
- }
203
- export function formatThinkingLog(startedAt, now, message, locatorSuffix) {
204
- const elapsedMs = now - startedAt;
205
- const elapsedText = formatElapsed(elapsedMs);
206
- const progress = Math.min(1, elapsedMs / 600_000); // soft target: 10 minutes
207
- const barSegments = 10;
208
- const filled = Math.round(progress * barSegments);
209
- const bar = `${'█'.repeat(filled).padEnd(barSegments, '░')}`;
210
- const pct = Math.round(progress * 100)
211
- .toString()
212
- .padStart(3, ' ');
213
- const statusLabel = message ? ` — ${message}` : '';
214
- return `[${elapsedText} / ~10m] ${bar} ${pct}%${statusLabel}${locatorSuffix}`;
215
- }
216
- function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false) {
217
- let stopped = false;
218
- let pending = false;
219
- let lastMessage = null;
220
- const startedAt = Date.now();
221
- const interval = setInterval(async () => {
222
- // biome-ignore lint/nursery/noUnnecessaryConditions: stop flag flips asynchronously
223
- if (stopped || pending) {
224
- return;
225
- }
226
- pending = true;
227
- try {
228
- const nextMessage = await readThinkingStatus(Runtime);
229
- if (nextMessage && nextMessage !== lastMessage) {
230
- lastMessage = nextMessage;
231
- let locatorSuffix = '';
232
- if (includeDiagnostics) {
233
- try {
234
- const snapshot = await readAssistantSnapshot(Runtime);
235
- locatorSuffix = ` | assistant-turn=${snapshot ? 'present' : 'missing'}`;
236
- }
237
- catch {
238
- locatorSuffix = ' | assistant-turn=error';
239
- }
240
- }
241
- logger(formatThinkingLog(startedAt, Date.now(), nextMessage, locatorSuffix));
242
- }
243
- }
244
- catch {
245
- // ignore DOM polling errors
246
- }
247
- finally {
248
- pending = false;
249
- }
250
- }, 1500);
251
- interval.unref?.();
252
- return () => {
253
- // biome-ignore lint/nursery/noUnnecessaryConditions: multiple callers may race to stop
254
- if (stopped) {
255
- return;
256
- }
257
- stopped = true;
258
- clearInterval(interval);
259
- };
260
- }
261
- async function readThinkingStatus(Runtime) {
262
- const expression = buildThinkingStatusExpression();
263
- try {
264
- const { result } = await Runtime.evaluate({ expression, returnByValue: true });
265
- const value = typeof result.value === 'string' ? result.value.trim() : '';
266
- const sanitized = sanitizeThinkingText(value);
267
- return sanitized || null;
268
- }
269
- catch {
270
- return null;
271
- }
272
- }
273
- function sanitizeThinkingText(raw) {
274
- if (!raw) {
275
- return '';
276
- }
277
- const trimmed = raw.trim();
278
- const prefixPattern = /^(pro thinking)\s*[•:\-–—]*\s*/i;
279
- if (prefixPattern.test(trimmed)) {
280
- return trimmed.replace(prefixPattern, '').trim();
281
- }
282
- return trimmed;
283
- }
284
- function buildThinkingStatusExpression() {
285
- const selectors = [
286
- 'span.loading-shimmer',
287
- 'span.flex.items-center.gap-1.truncate.text-start.align-middle.text-token-text-tertiary',
288
- '[data-testid*="thinking"]',
289
- '[data-testid*="reasoning"]',
290
- '[role="status"]',
291
- '[aria-live="polite"]',
292
- ];
293
- const keywords = ['pro thinking', 'thinking', 'reasoning', 'clarifying', 'planning', 'drafting', 'summarizing'];
294
- const selectorLiteral = JSON.stringify(selectors);
295
- const keywordsLiteral = JSON.stringify(keywords);
296
- return `(() => {
297
- const selectors = ${selectorLiteral};
298
- const keywords = ${keywordsLiteral};
299
- const nodes = new Set();
300
- for (const selector of selectors) {
301
- document.querySelectorAll(selector).forEach((node) => nodes.add(node));
302
- }
303
- document.querySelectorAll('[data-testid]').forEach((node) => nodes.add(node));
304
- for (const node of nodes) {
305
- if (!(node instanceof HTMLElement)) {
306
- continue;
307
- }
308
- const text = node.textContent?.trim();
309
- if (!text) {
310
- continue;
311
- }
312
- const classLabel = (node.className || '').toLowerCase();
313
- const dataLabel = ((node.getAttribute('data-testid') || '') + ' ' + (node.getAttribute('aria-label') || ''))
314
- .toLowerCase();
315
- const normalizedText = text.toLowerCase();
316
- const matches = keywords.some((keyword) =>
317
- normalizedText.includes(keyword) || classLabel.includes(keyword) || dataLabel.includes(keyword)
318
- );
319
- if (matches) {
320
- const shimmerChild = node.querySelector(
321
- 'span.flex.items-center.gap-1.truncate.text-start.align-middle.text-token-text-tertiary',
322
- );
323
- if (shimmerChild?.textContent?.trim()) {
324
- return shimmerChild.textContent.trim();
325
- }
326
- return text.trim();
327
- }
328
- }
329
- return null;
330
- })()`;
331
- }
@@ -1,5 +0,0 @@
1
- export { navigateToChatGPT, ensureNotBlocked, 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';
@@ -1,88 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { readFiles, createFileSections, DEFAULT_SYSTEM_PROMPT, MODEL_CONFIGS, TOKENIZER_OPTIONS } from '../oracle.js';
5
- export async function assembleBrowserPrompt(runOptions, deps = {}) {
6
- const cwd = deps.cwd ?? process.cwd();
7
- const readFilesFn = deps.readFilesImpl ?? readFiles;
8
- const files = await readFilesFn(runOptions.file ?? [], { cwd });
9
- const basePrompt = (runOptions.prompt ?? '').trim();
10
- const userPrompt = basePrompt;
11
- const systemPrompt = runOptions.system?.trim() || DEFAULT_SYSTEM_PROMPT;
12
- const sections = createFileSections(files, cwd);
13
- const lines = ['[SYSTEM]', systemPrompt, '', '[USER]', userPrompt, ''];
14
- sections.forEach((section) => {
15
- lines.push(`[FILE: ${section.displayPath}]`, section.content.trimEnd(), '');
16
- });
17
- const markdown = lines.join('\n').trimEnd();
18
- const inlineFiles = Boolean(runOptions.browserInlineFiles);
19
- const composerSections = [];
20
- if (systemPrompt) {
21
- composerSections.push(systemPrompt);
22
- }
23
- if (userPrompt) {
24
- composerSections.push(userPrompt);
25
- }
26
- let inlineBlock = '';
27
- if (inlineFiles && sections.length > 0) {
28
- const inlineLines = [];
29
- sections.forEach((section) => {
30
- inlineLines.push(`[FILE: ${section.displayPath}]`, section.content.trimEnd(), '');
31
- });
32
- inlineBlock = inlineLines.join('\n').trim();
33
- if (inlineBlock.length > 0) {
34
- composerSections.push(inlineBlock);
35
- }
36
- }
37
- const composerText = composerSections.join('\n\n').trim();
38
- const attachments = inlineFiles
39
- ? []
40
- : sections.map((section) => ({
41
- path: section.absolutePath,
42
- displayPath: section.displayPath,
43
- sizeBytes: Buffer.byteLength(section.content, 'utf8'),
44
- }));
45
- const MAX_BROWSER_ATTACHMENTS = 10;
46
- if (!inlineFiles && attachments.length > MAX_BROWSER_ATTACHMENTS) {
47
- const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
48
- const bundlePath = path.join(bundleDir, 'attachments-bundle.txt');
49
- const bundleLines = [];
50
- sections.forEach((section) => {
51
- bundleLines.push(`### File: ${section.displayPath}`);
52
- bundleLines.push(section.content.trimEnd());
53
- bundleLines.push('');
54
- });
55
- const bundleText = `${bundleLines.join('\n').trimEnd()}\n`;
56
- await fs.writeFile(bundlePath, bundleText, 'utf8');
57
- attachments.length = 0;
58
- attachments.push({
59
- path: bundlePath,
60
- displayPath: 'attachments-bundle.txt',
61
- sizeBytes: Buffer.byteLength(bundleText, 'utf8'),
62
- });
63
- }
64
- const inlineFileCount = inlineFiles ? sections.length : 0;
65
- const tokenizer = MODEL_CONFIGS[runOptions.model].tokenizer;
66
- const tokenizerUserContent = inlineFileCount > 0 && inlineBlock
67
- ? [userPrompt, inlineBlock].filter((value) => Boolean(value?.trim())).join('\n\n').trim()
68
- : userPrompt;
69
- const tokenizerMessages = [
70
- systemPrompt ? { role: 'system', content: systemPrompt } : null,
71
- tokenizerUserContent ? { role: 'user', content: tokenizerUserContent } : null,
72
- ].filter(Boolean);
73
- const estimatedInputTokens = tokenizer(tokenizerMessages.length > 0
74
- ? tokenizerMessages
75
- : [{ role: 'user', content: '' }], TOKENIZER_OPTIONS);
76
- const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(inlineBlock);
77
- return {
78
- markdown,
79
- composerText,
80
- estimatedInputTokens,
81
- attachments,
82
- inlineFileCount,
83
- tokenEstimateIncludesInlineFiles,
84
- bundled: !inlineFiles && attachments.length === 1 && sections.length > MAX_BROWSER_ATTACHMENTS && attachments[0]?.displayPath
85
- ? { originalCount: sections.length, bundlePath: attachments[0].displayPath }
86
- : null,
87
- };
88
- }