agentlinkops 0.5.0 → 0.5.1

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 (63) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.mcp.json +8 -0
  4. package/CHANGELOG.md +15 -1
  5. package/README.md +2 -1
  6. package/cli/citation.js +19 -1
  7. package/cli/client.js +1 -1
  8. package/cli/doctor.js +19 -0
  9. package/package.json +9 -1
  10. package/src/citations/browser/engine.js +196 -0
  11. package/src/citations/browser/gen/core/providers/_shared/google.js +4 -0
  12. package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +15 -0
  13. package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +22 -0
  14. package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +8 -0
  15. package/src/citations/browser/gen/core/providers/ai-overview/index.js +70 -0
  16. package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +59 -0
  17. package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +60 -0
  18. package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +18 -0
  19. package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +62 -0
  20. package/src/citations/browser/gen/core/providers/chatgpt/index.js +28 -0
  21. package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +39 -0
  22. package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +66 -0
  23. package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +11 -0
  24. package/src/citations/browser/gen/core/providers/claude/index.js +16 -0
  25. package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +54 -0
  26. package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +9 -0
  27. package/src/citations/browser/gen/core/providers/gemini/index.js +40 -0
  28. package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +65 -0
  29. package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +14 -0
  30. package/src/citations/browser/gen/core/providers/gemini/lib/session.js +45 -0
  31. package/src/citations/browser/gen/core/providers/index.js +15 -0
  32. package/src/citations/browser/gen/core/providers/perplexity/index.js +31 -0
  33. package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +84 -0
  34. package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +38 -0
  35. package/src/citations/browser/gen/core/providers/types.js +0 -0
  36. package/src/citations/browser/gen/lib/browser/camoufox.js +587 -0
  37. package/src/citations/browser/gen/lib/browser/display.js +147 -0
  38. package/src/citations/browser/gen/lib/browser/domOps.js +339 -0
  39. package/src/citations/browser/gen/lib/browser/humanBehavior.js +121 -0
  40. package/src/citations/browser/gen/lib/browser/navigate.js +46 -0
  41. package/src/citations/browser/gen/lib/browser/playwrightCompat.js +321 -0
  42. package/src/citations/browser/gen/lib/browser/proxy/forwarder.js +251 -0
  43. package/src/citations/browser/gen/lib/browser/runtimeTypes.js +0 -0
  44. package/src/citations/browser/gen/lib/extraction/sourceUtils.js +71 -0
  45. package/src/citations/browser/gen/lib/input/editor/assertNotBlocked.js +54 -0
  46. package/src/citations/browser/gen/lib/input/editor/clearInput.js +52 -0
  47. package/src/citations/browser/gen/lib/input/editor/findEditor.js +54 -0
  48. package/src/citations/browser/gen/lib/input/editor/findSendButton.js +21 -0
  49. package/src/citations/browser/gen/lib/input/editor/promptInput.js +117 -0
  50. package/src/citations/browser/gen/lib/input/markdown/converter.js +101 -0
  51. package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +14 -0
  52. package/src/citations/browser/gen/lib/input/response/detectBotPage.js +13 -0
  53. package/src/citations/browser/gen/lib/input/response/getText.js +10 -0
  54. package/src/citations/browser/gen/lib/input/response/isGenerating.js +59 -0
  55. package/src/citations/browser/gen/lib/input/response/waitForFinish.js +105 -0
  56. package/src/citations/browser/gen/lib/input/sources/findButton.js +41 -0
  57. package/src/citations/browser/gen/lib/input/sources/openPanel.js +11 -0
  58. package/src/citations/browser/gen/utils/agent-constants.js +87 -0
  59. package/src/citations/browser/shims/errors.js +26 -0
  60. package/src/citations/browser/shims/types.js +6 -0
  61. package/src/citations/browser/shims/utils.js +32 -0
  62. package/src/citations/contract.js +9 -2
  63. package/src/citations/runner.js +21 -5
@@ -0,0 +1,52 @@
1
+ import {
2
+ clickLocatorLikeUser,
3
+ getBrowserPrimaryModifier
4
+ } from "../../browser/humanBehavior.js";
5
+ async function clearEditorInput(page, input, options = {}) {
6
+ const {
7
+ clickTimeoutMs = 3e3,
8
+ dismissWithEscape = false,
9
+ waitAfterMs = 0
10
+ } = options;
11
+ const count = await input.count().catch(() => 0);
12
+ if (count === 0) return false;
13
+ try {
14
+ const existingValue = await input.readInputValue().catch(() => "");
15
+ if (existingValue.trim().length === 0) {
16
+ return true;
17
+ }
18
+ const clicked = await clickLocatorLikeUser(page, input, {
19
+ timeout: clickTimeoutMs
20
+ }).catch(() => false) || await clickLocatorLikeUser(page, input, {
21
+ force: true,
22
+ timeout: clickTimeoutMs
23
+ }).catch(() => false);
24
+ if (!clicked) {
25
+ return false;
26
+ }
27
+ const primaryModifier = await getBrowserPrimaryModifier(page);
28
+ const selectAllShortcuts = primaryModifier === "Control" ? ["Control+A"] : [`${primaryModifier}+A`, "Control+A"];
29
+ let clearedByKeyboard = false;
30
+ for (const shortcut of selectAllShortcuts) {
31
+ await input.press(shortcut).catch(() => null);
32
+ await input.press("Backspace").catch(() => null);
33
+ clearedByKeyboard = await input.readInputValue().then((value) => value.trim().length === 0).catch(() => false);
34
+ if (clearedByKeyboard) break;
35
+ }
36
+ if (!clearedByKeyboard) {
37
+ await input.setInputValue("");
38
+ }
39
+ if (dismissWithEscape) {
40
+ await input.press("Escape").catch(() => null);
41
+ }
42
+ if (waitAfterMs > 0) {
43
+ await page.waitForTimeout(waitAfterMs);
44
+ }
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+ export {
51
+ clearEditorInput
52
+ };
@@ -0,0 +1,54 @@
1
+ import { NotFoundError } from "../../../../shims/errors.js";
2
+ import { PROVIDER_EDITOR_SELECTORS } from "../../../../shims/utils.js";
3
+ async function findActiveEditorCandidateFromSelectors(page, selectors) {
4
+ for (const selector of selectors) {
5
+ const nodes = page.locator(selector);
6
+ const count = await nodes.count();
7
+ for (let i = 0; i < count; i++) {
8
+ const el = nodes.nth(i);
9
+ try {
10
+ const visible = await el.isVisible().catch(() => false);
11
+ if (!visible) {
12
+ continue;
13
+ }
14
+ const box = await el.boundingBox().catch(() => null);
15
+ if (!box || box.width < 8 || box.height < 8) {
16
+ continue;
17
+ }
18
+ await el.scrollIntoViewIfNeeded().catch(() => {
19
+ });
20
+ await el.focus().catch(() => {
21
+ });
22
+ const state = await el.getEditableState().catch(() => null);
23
+ if (!(state?.connected && state.visible && state.editable && state.enabled)) {
24
+ continue;
25
+ }
26
+ return { locator: el, selector };
27
+ } catch (_error) {
28
+ }
29
+ }
30
+ }
31
+ throw new NotFoundError("active prompt editor");
32
+ }
33
+ async function findActiveEditorFromSelectors(page, selectors) {
34
+ const candidate = await findActiveEditorCandidateFromSelectors(
35
+ page,
36
+ selectors
37
+ );
38
+ return candidate.locator;
39
+ }
40
+ async function findActiveEditor(page, provider) {
41
+ const candidate = await findActiveEditorCandidate(page, provider);
42
+ return candidate.locator;
43
+ }
44
+ async function findActiveEditorCandidate(page, provider) {
45
+ const fallbackSelectors = [
46
+ ...new Set(Object.values(PROVIDER_EDITOR_SELECTORS).flat())
47
+ ];
48
+ const selectors = provider ? PROVIDER_EDITOR_SELECTORS[provider] || fallbackSelectors : fallbackSelectors;
49
+ return findActiveEditorCandidateFromSelectors(page, selectors);
50
+ }
51
+ export {
52
+ findActiveEditorCandidate,
53
+ findActiveEditorCandidateFromSelectors
54
+ };
@@ -0,0 +1,21 @@
1
+ import { PROVIDER_SUBMIT_BTN_SELECTORS } from "../../../../shims/utils.js";
2
+ async function findEnabledSendButton(page, provider) {
3
+ const selectors = PROVIDER_SUBMIT_BTN_SELECTORS[provider] || [];
4
+ for (const selector of selectors) {
5
+ const buttons = page.locator(selector);
6
+ const count = await buttons.count();
7
+ for (let i = 0; i < count; i++) {
8
+ const btn = buttons.nth(i);
9
+ try {
10
+ if (await btn.isVisible() && await btn.isEnabled()) {
11
+ return btn;
12
+ }
13
+ } catch {
14
+ }
15
+ }
16
+ }
17
+ return null;
18
+ }
19
+ export {
20
+ findEnabledSendButton
21
+ };
@@ -0,0 +1,117 @@
1
+ import { ExternalServiceError } from "../../../../shims/errors.js";
2
+ import { logger } from "../../../../shims/utils.js";
3
+ import {
4
+ clickLocatorLikeUser,
5
+ pastePrompt,
6
+ randomBetween
7
+ } from "../../browser/humanBehavior.js";
8
+ import { clearEditorInput } from "./clearInput.js";
9
+ function normalizePromptValue(text) {
10
+ return text.replace(/\r\n/g, "\n").replace(/\u00a0/g, " ").replace(/[\u200b-\u200d\ufeff]/g, "").trim();
11
+ }
12
+ async function focusEditorTarget(page, input) {
13
+ await input.scrollIntoViewIfNeeded().catch(() => null);
14
+ await clickLocatorLikeUser(page, input, {
15
+ timeout: 3e3,
16
+ delay: randomBetween(25, 80)
17
+ }).catch(() => null);
18
+ await page.waitForTimeout(randomBetween(40, 120));
19
+ await input.focus().catch(() => null);
20
+ await page.waitForTimeout(randomBetween(30, 90));
21
+ }
22
+ async function prepareEditorForPrompt(page, input, provider) {
23
+ const count = await input.count().catch(() => 0);
24
+ if (count === 0) {
25
+ throw new ExternalServiceError(
26
+ provider,
27
+ `Editor not ready for ${provider}: input locator is missing`
28
+ );
29
+ }
30
+ const state = await input.getEditableState().catch(() => null);
31
+ if (!(state?.connected && state.editable && state.enabled && state.acceptsTextInput)) {
32
+ throw new ExternalServiceError(
33
+ provider,
34
+ `Editor not ready for ${provider}: input is not editable`
35
+ );
36
+ }
37
+ await focusEditorTarget(page, input);
38
+ const existingValue = await input.readInputValue().catch(() => "");
39
+ if (normalizePromptValue(existingValue).length === 0) {
40
+ await focusEditorTarget(page, input);
41
+ return;
42
+ }
43
+ const cleared = await clearEditorInput(page, input, {
44
+ clickTimeoutMs: 3e3,
45
+ waitAfterMs: randomBetween(40, 120)
46
+ });
47
+ if (!cleared) {
48
+ throw new ExternalServiceError(
49
+ provider,
50
+ `Editor not ready for ${provider}: could not clear existing input`
51
+ );
52
+ }
53
+ const remainingValue = await input.readInputValue().catch(() => "");
54
+ if (normalizePromptValue(remainingValue).length > 0) {
55
+ throw new ExternalServiceError(
56
+ provider,
57
+ `Editor not ready for ${provider}: input retained content after clear`
58
+ );
59
+ }
60
+ await focusEditorTarget(page, input);
61
+ }
62
+ async function insertPromptOnce(page, input, prompt, strategy) {
63
+ if (strategy === "directSet") {
64
+ await input.setInputValue(prompt);
65
+ await page.waitForTimeout(randomBetween(40, 120));
66
+ return;
67
+ }
68
+ await pastePrompt(page, prompt);
69
+ }
70
+ async function waitForPromptValue(page, input, expectedValue, timeoutMs) {
71
+ const deadline = Date.now() + timeoutMs;
72
+ let lastValue = await input.readInputValue().catch(() => "");
73
+ while (Date.now() < deadline) {
74
+ if (normalizePromptValue(lastValue) === expectedValue) {
75
+ return lastValue;
76
+ }
77
+ await page.waitForTimeout(randomBetween(80, 140));
78
+ lastValue = await input.readInputValue().catch(() => "");
79
+ }
80
+ return lastValue;
81
+ }
82
+ async function insertPromptIntoEditor(page, input, prompt, provider) {
83
+ const expectedValue = normalizePromptValue(prompt);
84
+ const strategies = [
85
+ ...provider === "perplexity" ? [] : ["directSet"],
86
+ "pacedPaste"
87
+ ];
88
+ for (const strategy of strategies) {
89
+ for (let attempt = 1; attempt <= 2; attempt++) {
90
+ await prepareEditorForPrompt(page, input, provider);
91
+ await insertPromptOnce(page, input, prompt, strategy);
92
+ const rawValue = await waitForPromptValue(
93
+ page,
94
+ input,
95
+ expectedValue,
96
+ strategy === "directSet" ? attempt === 1 ? 800 : 1400 : attempt === 1 ? 1800 : 2500
97
+ );
98
+ if (normalizePromptValue(rawValue) === expectedValue) {
99
+ return { rawValue, strategy };
100
+ }
101
+ if (attempt === 1) {
102
+ logger.warn(
103
+ `[${provider}] prompt verification mismatch after ${strategy} \u2014 retrying once`
104
+ );
105
+ }
106
+ }
107
+ }
108
+ const finalValue = await input.readInputValue().catch(() => "");
109
+ throw new ExternalServiceError(
110
+ provider,
111
+ `Typing failed: normalized input mismatch after local retry (expected ${expectedValue.length} chars, got ${normalizePromptValue(finalValue).length})`
112
+ );
113
+ }
114
+ export {
115
+ insertPromptIntoEditor,
116
+ normalizePromptValue
117
+ };
@@ -0,0 +1,101 @@
1
+ import TurndownService from "turndown";
2
+ const turndown = new TurndownService({
3
+ headingStyle: "atx",
4
+ codeBlockStyle: "fenced",
5
+ bulletListMarker: "-",
6
+ br: "\n"
7
+ });
8
+ turndown.addRule("paragraph", {
9
+ filter: "p",
10
+ replacement(content) {
11
+ return `
12
+
13
+ ${content}
14
+
15
+ `;
16
+ }
17
+ });
18
+ turndown.addRule("div", {
19
+ filter: "div",
20
+ replacement(content) {
21
+ return `
22
+
23
+ ${content.trim()}
24
+
25
+ `;
26
+ }
27
+ });
28
+ turndown.addRule("heading", {
29
+ filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
30
+ replacement(content, node) {
31
+ const level = Number(node.nodeName.charAt(1));
32
+ return `
33
+
34
+ ${"#".repeat(level)} ${content}
35
+
36
+ `;
37
+ }
38
+ });
39
+ turndown.addRule("list", {
40
+ filter: ["ul", "ol"],
41
+ replacement(content, node) {
42
+ const parent = node.parentNode;
43
+ if (parent && (parent.nodeName === "LI" || parent.nodeName === "UL" || parent.nodeName === "OL")) {
44
+ return `
45
+ ${content}`;
46
+ }
47
+ return `
48
+
49
+ ${content}
50
+
51
+ `;
52
+ }
53
+ });
54
+ turndown.addRule("table", {
55
+ filter: "table",
56
+ replacement(_content, node) {
57
+ const table = node;
58
+ const rows = Array.from(table.querySelectorAll("tr"));
59
+ if (rows.length === 0) return "";
60
+ const result = [];
61
+ for (let i = 0; i < rows.length; i++) {
62
+ const cells = Array.from(rows[i].querySelectorAll("th, td"));
63
+ const line = cells.map((c) => turndown.turndown(c.innerHTML ?? c.textContent ?? "").replace(/\n+/g, " ").trim()).join(" | ");
64
+ result.push(`| ${line} |`);
65
+ if (i === 0) {
66
+ result.push(`| ${cells.map(() => "---").join(" | ")} |`);
67
+ }
68
+ }
69
+ return `
70
+
71
+ ${result.join("\n")}
72
+
73
+ `;
74
+ }
75
+ });
76
+ turndown.addRule("link", {
77
+ filter: "a",
78
+ replacement(content) {
79
+ const trimmed = content.trim();
80
+ if (!trimmed) return "";
81
+ if (/^\[?\d+\]?$/.test(trimmed)) return "";
82
+ return content;
83
+ }
84
+ });
85
+ turndown.addRule("image", { filter: "img", replacement: () => "" });
86
+ turndown.addRule("figure", { filter: "figure", replacement: () => "" });
87
+ turndown.addRule("picture", { filter: "picture", replacement: () => "" });
88
+ turndown.addRule("video", { filter: "video", replacement: () => "" });
89
+ turndown.addRule("iframe", { filter: "iframe", replacement: () => "" });
90
+ turndown.addRule("sup", { filter: "sup", replacement: () => "" });
91
+ turndown.addRule("carousel", {
92
+ filter(node) {
93
+ const el = node;
94
+ const cn = el.className || "";
95
+ return el.nodeName === "DIV" && (cn.includes("carousel") || cn.includes("gallery") || cn.includes("slider") || cn.includes("swiper"));
96
+ },
97
+ replacement: () => ""
98
+ });
99
+ export {
100
+ turndown
101
+ };
@@ -0,0 +1,14 @@
1
+ import { PROVIDER_MODEL_RESPONSE_SELECTORS } from "../../../../shims/utils.js";
2
+ import { turndown } from "./converter.js";
3
+ async function extractAssistantMarkdown(page, provider) {
4
+ const html = await page.runDomOp("response-html", {
5
+ provider,
6
+ selectors: PROVIDER_MODEL_RESPONSE_SELECTORS[provider] || []
7
+ });
8
+ if (!html) return "";
9
+ const markdown = turndown.turndown(html);
10
+ return markdown.replace(/\n{3,}/g, "\n\n").trim();
11
+ }
12
+ export {
13
+ extractAssistantMarkdown
14
+ };
@@ -0,0 +1,13 @@
1
+ import { ExternalServiceError } from "../../../../shims/errors.js";
2
+ async function detectBotPage(page, provider) {
3
+ const state = await page.runDomOp("detect-bot-page").catch(() => ({ botDetected: false, reason: null }));
4
+ if (state.botDetected) {
5
+ throw new ExternalServiceError(
6
+ provider,
7
+ state.reason ?? "bot detection page detected"
8
+ );
9
+ }
10
+ }
11
+ export {
12
+ detectBotPage
13
+ };
@@ -0,0 +1,10 @@
1
+ import { PROVIDER_MODEL_RESPONSE_SELECTORS } from "../../../../shims/utils.js";
2
+ async function getText(page, provider) {
3
+ return await page.runDomOp("response-text", {
4
+ provider,
5
+ selectors: PROVIDER_MODEL_RESPONSE_SELECTORS[provider] || []
6
+ });
7
+ }
8
+ export {
9
+ getText
10
+ };
@@ -0,0 +1,59 @@
1
+ import {
2
+ PROVIDER_MODEL_RESPONSE_SELECTORS,
3
+ PROVIDER_RESPONSE_GENERATION_SELECTORS
4
+ } from "../../../../shims/utils.js";
5
+ async function getGenerationStateSignature(page, provider) {
6
+ return await page.evaluate(
7
+ (selectors) => (selectors || []).map((selector) => {
8
+ const parts = Array.from(document.querySelectorAll(selector)).map((node) => {
9
+ const element = node;
10
+ const style = window.getComputedStyle(element);
11
+ const visible = element.offsetParent !== null && style.visibility !== "hidden" && style.display !== "none";
12
+ const text = (element.textContent || "").trim();
13
+ const ariaLabel = element.getAttribute("aria-label") || "";
14
+ const disabled = element.getAttribute("disabled") ? "1" : "0";
15
+ return `${visible ? 1 : 0}:${text}:${ariaLabel}:${disabled}`;
16
+ });
17
+ return `${selector}=>${parts.join("|")}`;
18
+ }).join("||"),
19
+ PROVIDER_RESPONSE_GENERATION_SELECTORS[provider] || []
20
+ );
21
+ }
22
+ async function hasVisibleGenerationIndicator(page, provider) {
23
+ return await page.evaluate(
24
+ (selectors) => (selectors || []).some(
25
+ (selector) => Array.from(document.querySelectorAll(selector)).some((node) => {
26
+ const element = node;
27
+ const style = window.getComputedStyle(element);
28
+ return element.offsetParent !== null && style.visibility !== "hidden" && style.display !== "none";
29
+ })
30
+ ),
31
+ PROVIDER_RESPONSE_GENERATION_SELECTORS[provider] || []
32
+ );
33
+ }
34
+ async function getResponseStateSignature(page, provider) {
35
+ return await page.evaluate((selectors) => {
36
+ const visible = (element) => {
37
+ if (!(element instanceof HTMLElement)) return false;
38
+ const style = window.getComputedStyle(element);
39
+ return element.offsetParent !== null && style.visibility !== "hidden" && style.display !== "none";
40
+ };
41
+ const elements = (selectors || []).flatMap((selector) => Array.from(document.querySelectorAll(selector))).filter(
42
+ (el) => visible(el) && (el.innerText || "").trim().length > 0
43
+ );
44
+ const latest = elements.at(-1) ?? null;
45
+ if (!latest) {
46
+ return { signature: "", textLength: 0 };
47
+ }
48
+ const text = (latest.innerText || "").replace(/\s+/g, " ").trim();
49
+ return {
50
+ signature: `${text.length}:${latest.innerHTML.length}:${latest.childElementCount}:${text.slice(-120)}`,
51
+ textLength: text.length
52
+ };
53
+ }, PROVIDER_MODEL_RESPONSE_SELECTORS[provider] || []);
54
+ }
55
+ export {
56
+ getGenerationStateSignature,
57
+ getResponseStateSignature,
58
+ hasVisibleGenerationIndicator
59
+ };
@@ -0,0 +1,105 @@
1
+ import { ExternalServiceError } from "../../../../shims/errors.js";
2
+ import {
3
+ logger,
4
+ PROVIDER_FORCE_EXIT_STABLE_MS,
5
+ PROVIDER_NO_OUTPUT_TIMEOUT_MS
6
+ } from "../../../../shims/utils.js";
7
+ import {
8
+ getGenerationStateSignature,
9
+ getResponseStateSignature,
10
+ hasVisibleGenerationIndicator
11
+ } from "./isGenerating.js";
12
+ async function sleep(ms) {
13
+ let timer = null;
14
+ try {
15
+ await new Promise((resolve) => {
16
+ timer = setTimeout(resolve, ms);
17
+ });
18
+ } finally {
19
+ if (timer !== null) {
20
+ clearTimeout(timer);
21
+ }
22
+ }
23
+ }
24
+ async function pollUntilCondition(checkFn, pollInterval, maxWait, timeoutError) {
25
+ const start = Date.now();
26
+ while (Date.now() - start < maxWait) {
27
+ if (await checkFn()) return;
28
+ await sleep(pollInterval);
29
+ }
30
+ throw timeoutError;
31
+ }
32
+ async function waitForAssistantToFinish(page, provider) {
33
+ logger.debug(
34
+ provider === "ai-overview" ? "\u23F3 Waiting for AI Overview response container to stabilize\u2026" : "\u23F3 Waiting for assistant to finish\u2026"
35
+ );
36
+ const waitStart = Date.now();
37
+ let lastGenerationState = "";
38
+ let lastResponseState = "";
39
+ let lastChangeAt = Date.now();
40
+ let initialized = false;
41
+ let seenResponse = false;
42
+ await pollUntilCondition(
43
+ async () => {
44
+ const [currentGenerationState, currentResponseState, hasVisibleIndicator] = await Promise.all([
45
+ getGenerationStateSignature(page, provider),
46
+ getResponseStateSignature(page, provider),
47
+ hasVisibleGenerationIndicator(page, provider)
48
+ ]);
49
+ const waitedFor = Date.now() - waitStart;
50
+ const forceExitStableMs = PROVIDER_FORCE_EXIT_STABLE_MS[provider];
51
+ const responseStateChanged = currentResponseState.signature !== lastResponseState;
52
+ const generationStateChanged = currentGenerationState !== lastGenerationState;
53
+ const requiresContainerStabilityOnly = provider === "ai-overview";
54
+ if (!initialized) {
55
+ lastGenerationState = currentGenerationState;
56
+ lastResponseState = currentResponseState.signature;
57
+ seenResponse = currentResponseState.textLength > 0;
58
+ lastChangeAt = Date.now();
59
+ initialized = true;
60
+ return false;
61
+ }
62
+ if (currentResponseState.textLength > 0) {
63
+ seenResponse = true;
64
+ }
65
+ if (responseStateChanged || generationStateChanged) {
66
+ lastGenerationState = currentGenerationState;
67
+ lastResponseState = currentResponseState.signature;
68
+ lastChangeAt = Date.now();
69
+ return false;
70
+ }
71
+ const stableFor = Date.now() - lastChangeAt;
72
+ if (requiresContainerStabilityOnly) {
73
+ if (seenResponse && stableFor >= 2500) {
74
+ logger.debug("\u2705 AI Overview response container stabilized");
75
+ return true;
76
+ }
77
+ }
78
+ if (seenResponse && !hasVisibleIndicator && stableFor >= 1500) {
79
+ logger.debug("\u2705 Assistant finished");
80
+ return true;
81
+ }
82
+ const noOutputTimeoutMs = PROVIDER_NO_OUTPUT_TIMEOUT_MS[provider];
83
+ if (waitedFor >= noOutputTimeoutMs) {
84
+ logger.warn(
85
+ `Generation state did not stabilize within ${Math.round(noOutputTimeoutMs / 1e3)}s`
86
+ );
87
+ }
88
+ if (stableFor >= forceExitStableMs) {
89
+ logger.warn(
90
+ `${hasVisibleIndicator ? "Generation indicator still visible and " : ""}generation state stable for ${Math.round(forceExitStableMs / 1e3)}s \u2014 forcing exit`
91
+ );
92
+ return true;
93
+ }
94
+ return false;
95
+ },
96
+ 280 + Math.floor(Math.random() * 60),
97
+ // Poll ~300ms with ±50ms jitter
98
+ 5 * 60 * 1e3,
99
+ // 5 min max — if a response hasn't arrived by then, something is wrong
100
+ new ExternalServiceError(provider, "Assistant wait timed out")
101
+ );
102
+ }
103
+ export {
104
+ waitForAssistantToFinish
105
+ };
@@ -0,0 +1,41 @@
1
+ async function findSourcesButton(page, _provider) {
2
+ const buttonCount = await page.locator("button, [role='button']").count();
3
+ if (buttonCount === 0) return null;
4
+ const bestIndex = await page.evaluate((_unused) => {
5
+ const normalize = (text) => text?.toLowerCase().replace(/\s+/g, " ").trim() || "";
6
+ const getVisibleText = (el) => normalize(
7
+ Array.from(el.querySelectorAll("*")).map((child) => child.textContent || "").join(" ")
8
+ );
9
+ return Array.from(
10
+ document.querySelectorAll("button, [role='button'], [role='tab']")
11
+ ).map((el, index) => {
12
+ if (!(el instanceof HTMLElement)) {
13
+ return null;
14
+ }
15
+ const role = el.getAttribute("role");
16
+ if (role === "tab" || el.hasAttribute("aria-controls") || el.id?.includes("trigger")) {
17
+ return null;
18
+ }
19
+ const isAction = el.tagName === "BUTTON" || role === "button";
20
+ if (!isAction) return null;
21
+ const style = window.getComputedStyle(el);
22
+ if (style.display === "none" || style.visibility === "hidden") {
23
+ return null;
24
+ }
25
+ const text = getVisibleText(el);
26
+ const aria = normalize(el.getAttribute("aria-label"));
27
+ let score = 0;
28
+ if (/\b\d+\s*sources?\b/.test(text)) score += 120;
29
+ if (aria.includes("sources")) score += 90;
30
+ if (text === "sources") score += 80;
31
+ if (text.includes("sources")) score += 50;
32
+ if (score <= 0) return null;
33
+ score += index * 1e-3;
34
+ return { index, score };
35
+ }).filter((match) => match !== null).sort((a, b) => b.score - a.score || b.index - a.index)[0]?.index ?? -1;
36
+ }, null);
37
+ return bestIndex >= 0 ? page.locator("button, [role='button']").nth(bestIndex) : null;
38
+ }
39
+ export {
40
+ findSourcesButton
41
+ };
@@ -0,0 +1,11 @@
1
+ import { clickButtonViaDispatch } from "../../extraction/sourceUtils.js";
2
+ async function openSourcesPanel(page, btn) {
3
+ const clicked = await btn.scrollIntoViewIfNeeded().then(() => btn.click({ timeout: 3e3 })).then(() => true).catch(() => false);
4
+ if (!clicked) {
5
+ if (!await clickButtonViaDispatch(page, btn)) return;
6
+ }
7
+ await page.waitForTimeout(500);
8
+ }
9
+ export {
10
+ openSourcesPanel
11
+ };