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