@steipete/oracle 0.9.0 → 0.10.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 (177) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +61 -48
  3. package/dist/bin/oracle-cli.js +455 -402
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/assistantResponse.js +149 -101
  18. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  19. package/dist/src/browser/actions/attachments.js +246 -150
  20. package/dist/src/browser/actions/domEvents.js +2 -2
  21. package/dist/src/browser/actions/modelSelection.js +275 -117
  22. package/dist/src/browser/actions/navigation.js +161 -137
  23. package/dist/src/browser/actions/promptComposer.js +100 -64
  24. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  25. package/dist/src/browser/actions/thinkingTime.js +207 -110
  26. package/dist/src/browser/chromeLifecycle.js +62 -60
  27. package/dist/src/browser/config.js +34 -15
  28. package/dist/src/browser/constants.js +17 -12
  29. package/dist/src/browser/cookies.js +19 -19
  30. package/dist/src/browser/detect.js +62 -62
  31. package/dist/src/browser/domDebug.js +1 -1
  32. package/dist/src/browser/index.js +390 -295
  33. package/dist/src/browser/modelStrategy.js +1 -1
  34. package/dist/src/browser/pageActions.js +5 -5
  35. package/dist/src/browser/policies.js +16 -13
  36. package/dist/src/browser/profileState.js +44 -39
  37. package/dist/src/browser/prompt.js +72 -42
  38. package/dist/src/browser/promptSummary.js +5 -5
  39. package/dist/src/browser/providerDomFlow.js +1 -1
  40. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  41. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  42. package/dist/src/browser/providers/index.js +2 -2
  43. package/dist/src/browser/reattach.js +67 -34
  44. package/dist/src/browser/reattachHelpers.js +31 -26
  45. package/dist/src/browser/sessionRunner.js +37 -25
  46. package/dist/src/browser/utils.js +9 -9
  47. package/dist/src/browserMode.js +1 -1
  48. package/dist/src/cli/bridge/claudeConfig.js +16 -16
  49. package/dist/src/cli/bridge/client.js +28 -20
  50. package/dist/src/cli/bridge/codexConfig.js +16 -16
  51. package/dist/src/cli/bridge/doctor.js +47 -39
  52. package/dist/src/cli/bridge/host.js +58 -56
  53. package/dist/src/cli/browserConfig.js +62 -48
  54. package/dist/src/cli/browserDefaults.js +27 -26
  55. package/dist/src/cli/bundleWarnings.js +1 -1
  56. package/dist/src/cli/clipboard.js +11 -2
  57. package/dist/src/cli/detach.js +2 -2
  58. package/dist/src/cli/dryRun.js +29 -25
  59. package/dist/src/cli/duplicatePromptGuard.js +3 -3
  60. package/dist/src/cli/engine.js +9 -9
  61. package/dist/src/cli/errorUtils.js +1 -1
  62. package/dist/src/cli/fileSize.js +3 -3
  63. package/dist/src/cli/format.js +2 -2
  64. package/dist/src/cli/help.js +28 -28
  65. package/dist/src/cli/hiddenAliases.js +3 -3
  66. package/dist/src/cli/markdownBundle.js +7 -7
  67. package/dist/src/cli/markdownRenderer.js +15 -15
  68. package/dist/src/cli/notifier.js +77 -67
  69. package/dist/src/cli/options.js +127 -106
  70. package/dist/src/cli/oscUtils.js +1 -1
  71. package/dist/src/cli/promptRequirement.js +2 -2
  72. package/dist/src/cli/renderOutput.js +1 -1
  73. package/dist/src/cli/rootAlias.js +1 -1
  74. package/dist/src/cli/runOptions.js +32 -28
  75. package/dist/src/cli/sessionCommand.js +31 -21
  76. package/dist/src/cli/sessionDisplay.js +95 -81
  77. package/dist/src/cli/sessionLineage.js +6 -2
  78. package/dist/src/cli/sessionRunner.js +103 -93
  79. package/dist/src/cli/sessionTable.js +26 -23
  80. package/dist/src/cli/stdin.js +22 -0
  81. package/dist/src/cli/tagline.js +121 -124
  82. package/dist/src/cli/tui/index.js +139 -128
  83. package/dist/src/cli/writeOutputPath.js +5 -5
  84. package/dist/src/config.js +7 -7
  85. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  86. package/dist/src/gemini-web/client.js +76 -70
  87. package/dist/src/gemini-web/executionMode.js +6 -8
  88. package/dist/src/gemini-web/executor.js +98 -93
  89. package/dist/src/gemini-web/index.js +1 -1
  90. package/dist/src/mcp/server.js +16 -12
  91. package/dist/src/mcp/tools/consult.js +51 -47
  92. package/dist/src/mcp/tools/sessionResources.js +12 -12
  93. package/dist/src/mcp/tools/sessions.js +26 -17
  94. package/dist/src/mcp/types.js +5 -5
  95. package/dist/src/mcp/utils.js +15 -7
  96. package/dist/src/oracle/background.js +15 -15
  97. package/dist/src/oracle/claude.js +53 -25
  98. package/dist/src/oracle/client.js +50 -41
  99. package/dist/src/oracle/config.js +96 -66
  100. package/dist/src/oracle/errors.js +38 -38
  101. package/dist/src/oracle/files.js +55 -46
  102. package/dist/src/oracle/finishLine.js +10 -8
  103. package/dist/src/oracle/format.js +3 -3
  104. package/dist/src/oracle/gemini.js +37 -33
  105. package/dist/src/oracle/logging.js +7 -7
  106. package/dist/src/oracle/markdown.js +28 -28
  107. package/dist/src/oracle/modelResolver.js +16 -16
  108. package/dist/src/oracle/multiModelRunner.js +12 -12
  109. package/dist/src/oracle/oscProgress.js +8 -8
  110. package/dist/src/oracle/promptAssembly.js +6 -3
  111. package/dist/src/oracle/request.js +16 -13
  112. package/dist/src/oracle/run.js +156 -134
  113. package/dist/src/oracle/runUtils.js +8 -5
  114. package/dist/src/oracle/tokenEstimate.js +6 -6
  115. package/dist/src/oracle/tokenStats.js +5 -5
  116. package/dist/src/oracle/tokenStringifier.js +5 -5
  117. package/dist/src/oracle.js +12 -12
  118. package/dist/src/oracleHome.js +3 -3
  119. package/dist/src/remote/client.js +25 -25
  120. package/dist/src/remote/health.js +20 -20
  121. package/dist/src/remote/remoteServiceConfig.js +9 -9
  122. package/dist/src/remote/server.js +129 -118
  123. package/dist/src/sessionManager.js +77 -75
  124. package/dist/src/sessionStore.js +3 -3
  125. package/dist/src/version.js +10 -10
  126. package/dist/vendor/oracle-notifier/README.md +2 -0
  127. package/package.json +66 -62
  128. package/vendor/oracle-notifier/README.md +2 -0
  129. package/dist/markdansi/types/index.js +0 -4
  130. package/dist/oracle/bin/oracle-cli.js +0 -472
  131. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  132. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  133. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  134. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  135. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  136. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  137. package/dist/oracle/src/browser/config.js +0 -33
  138. package/dist/oracle/src/browser/constants.js +0 -40
  139. package/dist/oracle/src/browser/cookies.js +0 -210
  140. package/dist/oracle/src/browser/domDebug.js +0 -36
  141. package/dist/oracle/src/browser/index.js +0 -331
  142. package/dist/oracle/src/browser/pageActions.js +0 -5
  143. package/dist/oracle/src/browser/prompt.js +0 -88
  144. package/dist/oracle/src/browser/promptSummary.js +0 -20
  145. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  146. package/dist/oracle/src/browser/types.js +0 -1
  147. package/dist/oracle/src/browser/utils.js +0 -62
  148. package/dist/oracle/src/browserMode.js +0 -1
  149. package/dist/oracle/src/cli/browserConfig.js +0 -44
  150. package/dist/oracle/src/cli/dryRun.js +0 -59
  151. package/dist/oracle/src/cli/engine.js +0 -17
  152. package/dist/oracle/src/cli/errorUtils.js +0 -9
  153. package/dist/oracle/src/cli/help.js +0 -70
  154. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  155. package/dist/oracle/src/cli/options.js +0 -103
  156. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  157. package/dist/oracle/src/cli/rootAlias.js +0 -30
  158. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  159. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  160. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  161. package/dist/oracle/src/heartbeat.js +0 -43
  162. package/dist/oracle/src/oracle/client.js +0 -48
  163. package/dist/oracle/src/oracle/config.js +0 -29
  164. package/dist/oracle/src/oracle/errors.js +0 -101
  165. package/dist/oracle/src/oracle/files.js +0 -220
  166. package/dist/oracle/src/oracle/format.js +0 -33
  167. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  168. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  169. package/dist/oracle/src/oracle/request.js +0 -48
  170. package/dist/oracle/src/oracle/run.js +0 -444
  171. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  172. package/dist/oracle/src/oracle/types.js +0 -1
  173. package/dist/oracle/src/oracle.js +0 -9
  174. package/dist/oracle/src/sessionManager.js +0 -205
  175. package/dist/oracle/src/version.js +0 -39
  176. package/dist/scripts/chrome/browser-tools.js +0 -295
  177. package/dist/src/browser/profileSync.js +0 -141
@@ -1,32 +1,32 @@
1
- import { CLOUDFLARE_SCRIPT_SELECTOR, CLOUDFLARE_TITLE, INPUT_SELECTORS, } from '../constants.js';
2
- import { delay } from '../utils.js';
3
- import { logDomFailure } from '../domDebug.js';
4
- import { BrowserAutomationError } from '../../oracle/errors.js';
1
+ import { CLOUDFLARE_SCRIPT_SELECTOR, CLOUDFLARE_TITLE, INPUT_SELECTORS } from "../constants.js";
2
+ import { delay } from "../utils.js";
3
+ import { logDomFailure } from "../domDebug.js";
4
+ import { BrowserAutomationError } from "../../oracle/errors.js";
5
5
  export function installJavaScriptDialogAutoDismissal(Page, logger) {
6
6
  const pageAny = Page;
7
- if (typeof pageAny.on !== 'function' || typeof pageAny.handleJavaScriptDialog !== 'function') {
7
+ if (typeof pageAny.on !== "function" || typeof pageAny.handleJavaScriptDialog !== "function") {
8
8
  return () => { };
9
9
  }
10
10
  const handler = async (params) => {
11
- const type = typeof params?.type === 'string' ? params.type : 'unknown';
12
- const message = typeof params?.message === 'string' ? params.message : '';
13
- logger(`[nav] dismissing JS dialog (${type})${message ? `: ${message.slice(0, 140)}` : ''}`);
11
+ const type = typeof params?.type === "string" ? params.type : "unknown";
12
+ const message = typeof params?.message === "string" ? params.message : "";
13
+ logger(`[nav] dismissing JS dialog (${type})${message ? `: ${message.slice(0, 140)}` : ""}`);
14
14
  try {
15
- await pageAny.handleJavaScriptDialog?.({ accept: true, promptText: '' });
15
+ await pageAny.handleJavaScriptDialog?.({ accept: true, promptText: "" });
16
16
  }
17
17
  catch (error) {
18
18
  const msg = error instanceof Error ? error.message : String(error);
19
19
  logger(`[nav] failed to dismiss JS dialog: ${msg}`);
20
20
  }
21
21
  };
22
- pageAny.on('javascriptDialogOpening', handler);
22
+ pageAny.on("javascriptDialogOpening", handler);
23
23
  return () => {
24
24
  try {
25
- pageAny.off?.('javascriptDialogOpening', handler);
25
+ pageAny.off?.("javascriptDialogOpening", handler);
26
26
  }
27
27
  catch {
28
28
  try {
29
- pageAny.removeListener?.('javascriptDialogOpening', handler);
29
+ pageAny.removeListener?.("javascriptDialogOpening", handler);
30
30
  }
31
31
  catch {
32
32
  // ignore
@@ -92,13 +92,13 @@ async function dismissBlockingUi(Runtime, logger) {
92
92
  }).catch(() => null);
93
93
  const value = outcome?.result?.value;
94
94
  if (value?.dismissed) {
95
- logger(`[nav] dismissed blocking UI (${value.action ?? 'unknown'})`);
95
+ logger(`[nav] dismissed blocking UI (${value.action ?? "unknown"})`);
96
96
  return true;
97
97
  }
98
98
  return false;
99
99
  }
100
100
  export async function navigateToPromptReadyWithFallback(Page, Runtime, options, deps = {}) {
101
- const { url, fallbackUrl, timeoutMs, fallbackTimeoutMs, headless, logger, } = options;
101
+ const { url, fallbackUrl, timeoutMs, fallbackTimeoutMs, headless, logger } = options;
102
102
  const navigate = deps.navigateToChatGPT ?? navigateToChatGPT;
103
103
  const ensureBlocked = deps.ensureNotBlocked ?? ensureNotBlocked;
104
104
  const ensureReady = deps.ensurePromptReady ?? ensurePromptReady;
@@ -115,6 +115,8 @@ export async function navigateToPromptReadyWithFallback(Page, Runtime, options,
115
115
  }
116
116
  const fallbackTimeout = fallbackTimeoutMs ?? Math.max(timeoutMs * 2, 120_000);
117
117
  logger(`Prompt not ready after ${Math.round(timeoutMs / 1000)}s on ${url}; retrying ${fallbackUrl} with ${Math.round(fallbackTimeout / 1000)}s timeout.`);
118
+ await navigate(Page, Runtime, "about:blank", logger);
119
+ await delay(250);
118
120
  await navigate(Page, Runtime, fallbackUrl, logger);
119
121
  await ensureBlocked(Runtime, headless, logger);
120
122
  await dismissBlockingUi(Runtime, logger).catch(() => false);
@@ -125,10 +127,10 @@ export async function navigateToPromptReadyWithFallback(Page, Runtime, options,
125
127
  export async function ensureNotBlocked(Runtime, headless, logger) {
126
128
  if (await isCloudflareInterstitial(Runtime)) {
127
129
  const message = headless
128
- ? 'Cloudflare challenge detected in headless mode. Re-run with --headful so you can solve the challenge.'
129
- : 'Cloudflare challenge detected. Complete the “Just a moment…” check in the open browser, then rerun.';
130
- logger('Cloudflare anti-bot page detected');
131
- throw new BrowserAutomationError(message, { stage: 'cloudflare-challenge', headless });
130
+ ? "Cloudflare challenge detected in headless mode. Re-run with --headful so you can solve the challenge."
131
+ : "Cloudflare challenge detected. Complete the “Just a moment…” check in the open browser, then rerun.";
132
+ logger("Cloudflare anti-bot page detected");
133
+ throw new BrowserAutomationError(message, { stage: "cloudflare-challenge", headless });
132
134
  }
133
135
  }
134
136
  const LOGIN_CHECK_TIMEOUT_MS = 5_000;
@@ -157,98 +159,92 @@ export async function ensureLoggedIn(Runtime, logger, options = {}) {
157
159
  });
158
160
  const retryProbe = normalizeLoginProbe(retryOutcome.result?.value);
159
161
  if (retryProbe.ok) {
160
- logger('Login restored via Welcome back account picker');
162
+ logger("Login restored via Welcome back account picker");
161
163
  return;
162
164
  }
163
165
  logger(`Login retry after Welcome back failed (status=${retryProbe.status}, domLoginCta=${Boolean(retryProbe.domLoginCta)})`);
164
166
  }
165
- logger(`Login probe failed (status=${probe.status}, domLoginCta=${Boolean(probe.domLoginCta)}, onAuthPage=${Boolean(probe.onAuthPage)}, url=${probe.pageUrl ?? 'n/a'}, error=${probe.error ?? 'none'})`);
166
- const domLabel = probe.domLoginCta ? ' Login button detected on page.' : '';
167
+ logger(`Login probe failed (status=${probe.status}, domLoginCta=${Boolean(probe.domLoginCta)}, onAuthPage=${Boolean(probe.onAuthPage)}, url=${probe.pageUrl ?? "n/a"}, error=${probe.error ?? "none"})`);
168
+ const domLabel = probe.domLoginCta ? " Login button detected on page." : "";
167
169
  const cookieHint = options.remoteSession
168
- ? 'The remote Chrome session is not signed into ChatGPT. Sign in there, then rerun.'
170
+ ? "The remote Chrome session is not signed into ChatGPT. Sign in there, then rerun."
169
171
  : (options.appliedCookies ?? 0) === 0
170
- ? 'No ChatGPT cookies were applied; sign in to chatgpt.com in Chrome or pass inline cookies (--browser-inline-cookies[(-file)] / ORACLE_BROWSER_COOKIES_JSON).'
171
- : 'ChatGPT login appears missing; open chatgpt.com in Chrome to refresh the session or provide inline cookies (--browser-inline-cookies[(-file)] / ORACLE_BROWSER_COOKIES_JSON).';
172
+ ? "No ChatGPT cookies were applied; sign in to chatgpt.com in Chrome or pass inline cookies (--browser-inline-cookies[(-file)] / ORACLE_BROWSER_COOKIES_JSON)."
173
+ : "ChatGPT login appears missing; open chatgpt.com in Chrome to refresh the session or provide inline cookies (--browser-inline-cookies[(-file)] / ORACLE_BROWSER_COOKIES_JSON).";
172
174
  throw new Error(`ChatGPT session not detected.${domLabel} ${cookieHint}`);
173
175
  }
174
176
  async function attemptWelcomeBackLogin(Runtime, logger) {
175
- const outcome = await Runtime.evaluate({
176
- expression: `(() => {
177
- // Learned: "Welcome back" shows as a modal with account chips; click the email chip.
178
- const TIMEOUT_MS = 30000;
179
- const getLabel = (node) =>
180
- (node?.textContent || node?.getAttribute?.('aria-label') || '').trim();
181
- const isAccount = (label) =>
182
- Boolean(label) &&
183
- label.includes('@') &&
184
- !/log in|sign up|create account|another account/i.test(label);
185
- const findAccount = () => {
186
- const candidates = Array.from(document.querySelectorAll('[role="button"],button,a'));
187
- return candidates.find((node) => isAccount(getLabel(node))) || null;
188
- };
189
- const clickAccount = () => {
190
- const account = findAccount();
191
- if (!account) return null;
177
+ const deadline = Date.now() + 30_000;
178
+ while (Date.now() < deadline) {
179
+ let outcome;
192
180
  try {
193
- (account).click();
194
- } catch (_error) {
195
- return { clicked: false, reason: 'click-failed' };
196
- }
197
- return { clicked: true, label: getLabel(account) };
198
- };
199
- const immediate = clickAccount();
200
- if (immediate) {
201
- return immediate;
202
- }
203
- const root = document.documentElement || document.body;
204
- if (!root) {
205
- return { clicked: false, reason: 'no-root' };
206
- }
207
- return new Promise((resolve) => {
208
- const timer = setTimeout(() => {
209
- observer.disconnect();
210
- resolve({ clicked: false, reason: 'timeout' });
211
- }, TIMEOUT_MS);
212
- const observer = new MutationObserver(() => {
213
- const result = clickAccount();
214
- if (result) {
215
- clearTimeout(timer);
216
- observer.disconnect();
217
- resolve(result);
181
+ outcome = await Runtime.evaluate({
182
+ expression: `(() => {
183
+ // Learned: "Welcome back" shows as a modal with account chips; click the email chip.
184
+ const getLabel = (node) =>
185
+ (node?.textContent || node?.getAttribute?.('aria-label') || '').trim();
186
+ const isAccount = (label) =>
187
+ Boolean(label) &&
188
+ label.includes('@') &&
189
+ !/log in|sign up|create account|another account/i.test(label);
190
+ const candidates = Array.from(document.querySelectorAll('[role="button"],button,a'));
191
+ const account = candidates.find((node) => isAccount(getLabel(node))) || null;
192
+ if (!account) {
193
+ return { clicked: false, reason: 'not-found' };
218
194
  }
219
- });
220
- observer.observe(root, {
221
- subtree: true,
222
- childList: true,
223
- characterData: true,
224
- });
225
- });
226
- })()`,
227
- awaitPromise: true,
228
- returnByValue: true,
229
- });
230
- if (outcome.exceptionDetails) {
231
- const details = outcome.exceptionDetails;
232
- const description = (details.exception && typeof details.exception.description === 'string' && details.exception.description) ||
233
- details.text ||
234
- 'unknown error';
235
- logger(`Welcome back auto-select probe failed: ${description}`);
236
- }
237
- const result = outcome.result?.value;
238
- if (!result) {
239
- logger('Welcome back auto-select probe returned no result.');
240
- return false;
241
- }
242
- if (result?.clicked) {
243
- logger(`Welcome back modal detected; selected account ${result.label ?? '(unknown)'}`);
244
- return true;
245
- }
246
- if (result?.reason && result.reason !== 'timeout') {
247
- logger(`Welcome back modal present but auto-select failed (${result.reason}).`);
248
- }
249
- if (result?.reason === 'timeout') {
250
- logger('Welcome back modal not detected after login probe failure.');
195
+ const label = getLabel(account);
196
+ setTimeout(() => {
197
+ try {
198
+ account.click();
199
+ } catch {
200
+ // ignore; caller will re-probe login state
201
+ }
202
+ }, 0);
203
+ return { clicked: true, label };
204
+ })()`,
205
+ awaitPromise: false,
206
+ returnByValue: true,
207
+ });
208
+ }
209
+ catch (error) {
210
+ const message = error instanceof Error ? error.message : String(error);
211
+ if (/navigated or closed|context was destroyed|target closed/i.test(message)) {
212
+ logger("Welcome back account click triggered navigation.");
213
+ return true;
214
+ }
215
+ logger(`Welcome back auto-select probe failed: ${message}`);
216
+ return false;
217
+ }
218
+ if (outcome.exceptionDetails) {
219
+ const details = outcome.exceptionDetails;
220
+ const description = (details.exception &&
221
+ typeof details.exception.description === "string" &&
222
+ details.exception.description) ||
223
+ details.text ||
224
+ "unknown error";
225
+ logger(`Welcome back auto-select probe failed: ${description}`);
226
+ return false;
227
+ }
228
+ const result = outcome.result?.value;
229
+ if (!result) {
230
+ logger("Welcome back auto-select probe returned no result.");
231
+ return false;
232
+ }
233
+ if (!("clicked" in result) && !("reason" in result)) {
234
+ logger("Welcome back auto-select probe returned an unexpected result.");
235
+ return false;
236
+ }
237
+ if (result.clicked) {
238
+ logger(`Welcome back modal detected; selected account ${result.label ?? "(unknown)"}`);
239
+ return true;
240
+ }
241
+ if (result.reason && result.reason !== "not-found") {
242
+ logger(`Welcome back modal present but auto-select failed (${result.reason}).`);
243
+ return false;
244
+ }
245
+ await delay(500);
251
246
  }
247
+ logger("Welcome back modal not detected after login probe failure.");
252
248
  return false;
253
249
  }
254
250
  export async function ensurePromptReady(Runtime, timeoutMs, logger) {
@@ -257,15 +253,15 @@ export async function ensurePromptReady(Runtime, timeoutMs, logger) {
257
253
  const authUrl = await currentUrl(Runtime);
258
254
  if (authUrl && isAuthLoginUrl(authUrl)) {
259
255
  // Learned: auth.openai.com/login can appear after cookies are copied; allow manual login window.
260
- logger('Auth login page detected; waiting for manual login to complete...');
256
+ logger("Auth login page detected; waiting for manual login to complete...");
261
257
  const extended = Math.min(Math.max(timeoutMs, 60_000), 20 * 60_000);
262
258
  const loggedIn = await waitForPrompt(Runtime, extended);
263
259
  if (loggedIn) {
264
260
  return;
265
261
  }
266
262
  }
267
- await logDomFailure(Runtime, logger, 'prompt-textarea');
268
- throw new Error('Prompt textarea did not appear before timeout');
263
+ await logDomFailure(Runtime, logger, "prompt-textarea");
264
+ throw new Error("Prompt textarea did not appear before timeout");
269
265
  }
270
266
  }
271
267
  async function waitForDocumentReady(Runtime, timeoutMs) {
@@ -275,24 +271,24 @@ async function waitForDocumentReady(Runtime, timeoutMs) {
275
271
  expression: `document.readyState`,
276
272
  returnByValue: true,
277
273
  });
278
- if (result?.value === 'complete' || result?.value === 'interactive') {
274
+ if (result?.value === "complete" || result?.value === "interactive") {
279
275
  return;
280
276
  }
281
277
  await delay(100);
282
278
  }
283
- throw new Error('Page did not reach ready state in time');
279
+ throw new Error("Page did not reach ready state in time");
284
280
  }
285
281
  async function currentUrl(Runtime) {
286
282
  const { result } = await Runtime.evaluate({
287
283
  expression: 'typeof location === "object" && location.href ? location.href : null',
288
284
  returnByValue: true,
289
285
  });
290
- return typeof result?.value === 'string' ? result.value : null;
286
+ return typeof result?.value === "string" ? result.value : null;
291
287
  }
292
288
  function isAuthLoginUrl(url) {
293
289
  try {
294
290
  const parsed = new URL(url);
295
- if (parsed.hostname.includes('auth.openai.com')) {
291
+ if (parsed.hostname.includes("auth.openai.com")) {
296
292
  return true;
297
293
  }
298
294
  return /^\/log-?in/i.test(parsed.pathname);
@@ -325,8 +321,11 @@ async function waitForPrompt(Runtime, timeoutMs) {
325
321
  return false;
326
322
  }
327
323
  async function isCloudflareInterstitial(Runtime) {
328
- const { result: titleResult } = await Runtime.evaluate({ expression: 'document.title', returnByValue: true });
329
- const title = typeof titleResult.value === 'string' ? titleResult.value : '';
324
+ const { result: titleResult } = await Runtime.evaluate({
325
+ expression: "document.title",
326
+ returnByValue: true,
327
+ });
328
+ const title = typeof titleResult.value === "string" ? titleResult.value : "";
330
329
  const challengeTitle = CLOUDFLARE_TITLE.toLowerCase();
331
330
  if (title.toLowerCase().includes(challengeTitle)) {
332
331
  return true;
@@ -341,7 +340,6 @@ function buildLoginProbeExpression(timeoutMs) {
341
340
  return `(async () => {
342
341
  // Learned: /backend-api/me is the most reliable "am I logged in" signal.
343
342
  // Some UIs render without a session; use DOM + network for a robust answer.
344
- const timer = setTimeout(() => {}, ${timeoutMs});
345
343
  const pageUrl = typeof location === 'object' && location?.href ? location.href : null;
346
344
  const onAuthPage =
347
345
  typeof location === 'object' &&
@@ -367,12 +365,26 @@ function buildLoginProbeExpression(timeoutMs) {
367
365
  const textMatches = (text) => {
368
366
  if (!text) return false;
369
367
  const normalized = text.toLowerCase().trim();
370
- return ['log in', 'login', 'sign in', 'signin', 'continue with'].some((needle) =>
371
- normalized.startsWith(needle),
368
+ return (
369
+ ['log in', 'login', 'sign in', 'signin', 'continue with', 'sign up for free'].some(
370
+ (needle) => normalized.startsWith(needle),
371
+ ) ||
372
+ normalized.includes('get responses tailored to you') ||
373
+ normalized.includes('log in to get answers')
372
374
  );
373
375
  };
374
376
  for (const node of candidates) {
375
377
  if (!(node instanceof HTMLElement)) continue;
378
+ const rect = node.getBoundingClientRect();
379
+ const style = window.getComputedStyle(node);
380
+ if (
381
+ rect.width <= 0 ||
382
+ rect.height <= 0 ||
383
+ style.display === 'none' ||
384
+ style.visibility === 'hidden'
385
+ ) {
386
+ continue;
387
+ }
376
388
  const label =
377
389
  node.textContent?.trim() ||
378
390
  node.getAttribute('aria-label') ||
@@ -385,33 +397,45 @@ function buildLoginProbeExpression(timeoutMs) {
385
397
  return false;
386
398
  };
387
399
 
388
- let status = 0;
389
- let error = null;
390
- try {
391
- if (typeof fetch === 'function') {
392
- const controller = new AbortController();
393
- const timeout = setTimeout(() => controller.abort(), ${timeoutMs});
394
- try {
395
- // Credentials included so we see a 200 only when cookies are valid.
396
- const response = await fetch('/backend-api/me', {
397
- cache: 'no-store',
398
- credentials: 'include',
399
- signal: controller.signal,
400
- });
401
- status = response.status || 0;
402
- } finally {
403
- clearTimeout(timeout);
400
+ const readBackendStatus = async () => {
401
+ try {
402
+ if (typeof fetch === 'function') {
403
+ const controller = new AbortController();
404
+ const timeout = setTimeout(() => controller.abort(), ${timeoutMs});
405
+ try {
406
+ // Credentials included so we see a 200 only when cookies are valid.
407
+ const response = await fetch('/backend-api/me', {
408
+ cache: 'no-store',
409
+ credentials: 'include',
410
+ signal: controller.signal,
411
+ });
412
+ return { status: response.status || 0, error: null };
413
+ } finally {
414
+ clearTimeout(timeout);
415
+ }
404
416
  }
417
+ } catch (err) {
418
+ return { status: 0, error: err ? String(err) : 'unknown' };
419
+ }
420
+ return { status: 0, error: null };
421
+ };
422
+
423
+ let { status, error } = await readBackendStatus();
424
+ let domLoginCta = hasLoginCta();
425
+ const settleDeadline = Date.now() + Math.min(${timeoutMs}, 2500);
426
+ while (!domLoginCta && Date.now() < settleDeadline) {
427
+ await new Promise((resolve) => setTimeout(resolve, 100));
428
+ domLoginCta = hasLoginCta();
429
+ if (status === 0 || status === 401 || status === 403) {
430
+ const next = await readBackendStatus();
431
+ status = next.status;
432
+ error = next.error;
405
433
  }
406
- } catch (err) {
407
- error = err ? String(err) : 'unknown';
408
434
  }
409
435
 
410
- const domLoginCta = hasLoginCta();
411
436
  const loginSignals = domLoginCta || onAuthPage;
412
- clearTimeout(timer);
413
437
  return {
414
- ok: !loginSignals && (status === 0 || status === 200),
438
+ ok: !loginSignals && status === 200,
415
439
  status,
416
440
  redirected: false,
417
441
  url: pageUrl,
@@ -423,23 +447,23 @@ function buildLoginProbeExpression(timeoutMs) {
423
447
  })()`;
424
448
  }
425
449
  function normalizeLoginProbe(raw) {
426
- if (!raw || typeof raw !== 'object') {
450
+ if (!raw || typeof raw !== "object") {
427
451
  return { ok: false, status: 0 };
428
452
  }
429
453
  const value = raw;
430
454
  const statusRaw = value.status;
431
- const status = typeof statusRaw === 'number'
455
+ const status = typeof statusRaw === "number"
432
456
  ? statusRaw
433
- : typeof statusRaw === 'string' && !Number.isNaN(Number(statusRaw))
457
+ : typeof statusRaw === "string" && !Number.isNaN(Number(statusRaw))
434
458
  ? Number(statusRaw)
435
459
  : 0;
436
460
  return {
437
461
  ok: Boolean(value.ok),
438
462
  status: Number.isFinite(status) ? status : 0,
439
- url: typeof value.url === 'string' ? value.url : null,
463
+ url: typeof value.url === "string" ? value.url : null,
440
464
  redirected: Boolean(value.redirected),
441
- error: typeof value.error === 'string' ? value.error : null,
442
- pageUrl: typeof value.pageUrl === 'string' ? value.pageUrl : null,
465
+ error: typeof value.error === "string" ? value.error : null,
466
+ pageUrl: typeof value.pageUrl === "string" ? value.pageUrl : null,
443
467
  domLoginCta: Boolean(value.domLoginCta),
444
468
  onAuthPage: Boolean(value.onAuthPage),
445
469
  };