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