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,62 @@
1
+ import { ExternalServiceError } from "../../../../../shims/errors.js";
2
+ import { logger } from "../../../../../shims/utils.js";
3
+ import { navigateWithRetry } from "../../../../lib/browser/navigate.js";
4
+ const GOOGLE_CONSENT_SELECTOR = "button#L2AGLb, button#W0wltc, form[action*='consent.google.com'] button";
5
+ const SEARCH_RESULTS_WAIT_MS = 8e3;
6
+ const warmedPages = /* @__PURE__ */ new WeakSet();
7
+ async function dismissGoogleConsentDialog(page) {
8
+ const consentBtn = page.locator(GOOGLE_CONSENT_SELECTOR).first();
9
+ const visible = await consentBtn.isVisible({ timeout: 2500 }).catch(() => false);
10
+ if (!visible) return;
11
+ await consentBtn.click({ timeout: 4e3 }).catch(() => {
12
+ });
13
+ await page.waitForTimeout(1e3);
14
+ }
15
+ function assertAIOverviewPageNotBlocked(page) {
16
+ const url = page.url();
17
+ if (url.includes("/sorry/")) {
18
+ throw new ExternalServiceError(
19
+ "ai-overview",
20
+ "Google bot detection triggered (sorry page) \u2014 proxy IP blocked",
21
+ 429
22
+ );
23
+ }
24
+ if (url.includes("accounts.google.com")) {
25
+ throw new ExternalServiceError(
26
+ "ai-overview",
27
+ "Google redirected to login page \u2014 session cookie missing or expired",
28
+ 401
29
+ );
30
+ }
31
+ }
32
+ async function ensureAIOverviewGoogleSession(page) {
33
+ if (warmedPages.has(page)) return;
34
+ logger.log("[ai-overview] warming up Google cookies");
35
+ await navigateWithRetry(page, "https://www.google.com/", {
36
+ waitUntil: "domcontentloaded",
37
+ timeout: 3e4
38
+ });
39
+ assertAIOverviewPageNotBlocked(page);
40
+ await dismissGoogleConsentDialog(page);
41
+ warmedPages.add(page);
42
+ }
43
+ async function waitForAIOverviewSearchResults(page) {
44
+ const deadline = Date.now() + SEARCH_RESULTS_WAIT_MS;
45
+ while (Date.now() < deadline) {
46
+ if (page.url().includes("/search?")) {
47
+ return;
48
+ }
49
+ assertAIOverviewPageNotBlocked(page);
50
+ await page.waitForTimeout(150);
51
+ }
52
+ throw new ExternalServiceError(
53
+ "ai-overview",
54
+ `Not on search results page after submission (url: ${page.url()})`
55
+ );
56
+ }
57
+ export {
58
+ assertAIOverviewPageNotBlocked,
59
+ dismissGoogleConsentDialog,
60
+ ensureAIOverviewGoogleSession,
61
+ waitForAIOverviewSearchResults
62
+ };
@@ -0,0 +1,28 @@
1
+ import { extractSourcesFromChatgpt } from "./lib/extractSources.js";
2
+ import { dismissChatgptAuthModal } from "./lib/dismissAuthModal.js";
3
+ import { extractAssistantMarkdown } from "../../../lib/input/markdown/toMarkdown.js";
4
+ import { openSourcesPanel } from "../../../lib/input/sources/openPanel.js";
5
+ import { findSourcesButton } from "../../../lib/input/sources/findButton.js";
6
+ import { waitForAssistantToFinish } from "../../../lib/input/response/waitForFinish.js";
7
+ import { CHATGPT_URL, resetChatgptPage } from "./lib/pageLifecycle.js";
8
+ const chatgptConfig = {
9
+ url: CHATGPT_URL,
10
+ label: "ChatGPT",
11
+ displayName: "ChatGPT",
12
+ waitForResponse: (page) => waitForAssistantToFinish(page, "chatgpt"),
13
+ extractResponse: (page) => extractAssistantMarkdown(page, "chatgpt"),
14
+ beforePromptHook: (page) => dismissChatgptAuthModal(page, { waitForAppearanceMs: 500 }),
15
+ afterTypingHook: (page) => dismissChatgptAuthModal(page, { waitForAppearanceMs: 1500 }),
16
+ beforeSubmitHook: (page) => dismissChatgptAuthModal(page, { waitForAppearanceMs: 1500 }),
17
+ afterSubmitHook: (page) => dismissChatgptAuthModal(page, { waitForAppearanceMs: 1500 }),
18
+ beforeRetryHook: resetChatgptPage,
19
+ extractSources: async (page) => {
20
+ const btn = await findSourcesButton(page);
21
+ if (!btn) return [];
22
+ await openSourcesPanel(page, btn);
23
+ return extractSourcesFromChatgpt(page, btn);
24
+ }
25
+ };
26
+ export {
27
+ chatgptConfig
28
+ };
@@ -0,0 +1,39 @@
1
+ import { logger } from "../../../../../shims/utils.js";
2
+ import {
3
+ canUseOsLevelInput,
4
+ clickLocatorLikeUser
5
+ } from "../../../../lib/browser/humanBehavior.js";
6
+ const AUTH_DIALOG_SELECTOR = '[role="dialog"][data-state="open"]';
7
+ const AUTH_DIALOG_TEXT_RE = /Thanks for trying ChatGPT|Log in or sign up/i;
8
+ const DISMISS_MODAL_TEXT_RE = /^Stay logged out$/i;
9
+ const MODAL_POLL_INTERVAL_MS = 200;
10
+ async function dismissChatgptAuthModal(page, options = {}) {
11
+ const waitForAppearanceMs = options.waitForAppearanceMs ?? 0;
12
+ const deadline = Date.now() + waitForAppearanceMs;
13
+ do {
14
+ const dialog = page.locator(AUTH_DIALOG_SELECTOR).filter({ hasText: AUTH_DIALOG_TEXT_RE }).first();
15
+ const dialogVisible = await dialog.isVisible().catch(() => false);
16
+ if (!dialogVisible) {
17
+ if (Date.now() >= deadline) return;
18
+ await page.waitForTimeout(MODAL_POLL_INTERVAL_MS);
19
+ continue;
20
+ }
21
+ const dismissTarget = dialog.getByText(DISMISS_MODAL_TEXT_RE).first();
22
+ const dismissVisible = await dismissTarget.isVisible().catch(() => false);
23
+ if (!dismissVisible) return;
24
+ logger.log("[chatgpt] dismissing auth modal via Stay logged out");
25
+ const clicked = await clickLocatorLikeUser(page, dismissTarget, {
26
+ timeout: 5e3
27
+ }).catch(() => false);
28
+ if (!clicked && canUseOsLevelInput(page)) {
29
+ return;
30
+ }
31
+ await dialog.waitFor({ state: "hidden", timeout: 5e3 }).catch(() => {
32
+ });
33
+ await page.waitForTimeout(300);
34
+ return;
35
+ } while (Date.now() <= deadline);
36
+ }
37
+ export {
38
+ dismissChatgptAuthModal
39
+ };
@@ -0,0 +1,66 @@
1
+ import {
2
+ buildSources,
3
+ clickButtonViaDispatch
4
+ } from "../../_shared/sourceUtils.js";
5
+ const CHATGPT_RAW_SOURCES_DOM_EXTRACTOR = String.raw`(_helpers) => {
6
+ const results = [];
7
+
8
+ const getLeafTexts = (anchor) => {
9
+ const texts = [];
10
+
11
+ for (const el of anchor.querySelectorAll("*")) {
12
+ const text = (el.textContent || "").trim();
13
+ if (!text) continue;
14
+
15
+ if (
16
+ Array.from(el.children).some(
17
+ (child) => (child.textContent || "").trim().length > 0,
18
+ )
19
+ ) {
20
+ continue;
21
+ }
22
+
23
+ texts.push(text);
24
+ }
25
+
26
+ return Array.from(new Set(texts));
27
+ };
28
+
29
+ for (const anchor of Array.from(
30
+ document.querySelectorAll(
31
+ 'ul li > a[target="_blank"][rel*="noopener"][href^="http"]',
32
+ ),
33
+ )) {
34
+ if (!(anchor instanceof HTMLAnchorElement)) continue;
35
+
36
+ const rawHref = anchor.href.replace(/#.*$/, "");
37
+ if (!rawHref) continue;
38
+
39
+ const texts = getLeafTexts(anchor);
40
+ if (texts.length === 0) continue;
41
+ const sorted = [...texts].sort((a, b) => a.length - b.length);
42
+ const citedText = sorted[sorted.length - 1] || "";
43
+ const title =
44
+ sorted.length >= 2 ? sorted[sorted.length - 2] || "" : sorted[0] || "";
45
+
46
+ results.push({
47
+ rawHref,
48
+ title,
49
+ citedText,
50
+ });
51
+ }
52
+
53
+ return results;
54
+ }`;
55
+ async function extractSourcesFromChatgpt(page, sourcesButton) {
56
+ const rawSources = await page.runDomOp("raw-sources", {
57
+ provider: "chatgpt"
58
+ });
59
+ if (!await clickButtonViaDispatch(page, sourcesButton)) return [];
60
+ await page.waitForTimeout(300);
61
+ return buildSources(rawSources, { provider: "chatgpt" });
62
+ }
63
+ export {
64
+ CHATGPT_RAW_SOURCES_DOM_EXTRACTOR,
65
+ extractSourcesFromChatgpt
66
+ };
@@ -0,0 +1,11 @@
1
+ import { resetProviderPage } from "../../_shared/resetProviderPage.js";
2
+ import { dismissChatgptAuthModal } from "./dismissAuthModal.js";
3
+ const CHATGPT_URL = "https://chatgpt.com/";
4
+ async function resetChatgptPage(page) {
5
+ await resetProviderPage(page, "chatgpt", CHATGPT_URL);
6
+ await dismissChatgptAuthModal(page, { waitForAppearanceMs: 1e3 });
7
+ }
8
+ export {
9
+ CHATGPT_URL,
10
+ resetChatgptPage
11
+ };
@@ -0,0 +1,16 @@
1
+ import { extractAssistantMarkdown } from "../../../lib/input/markdown/toMarkdown.js";
2
+ import { waitForAssistantToFinish } from "../../../lib/input/response/waitForFinish.js";
3
+ import { extractSourcesFromClaude } from "./lib/extractSources.js";
4
+ import { CLAUDE_URL, resetClaudePage } from "./lib/pageLifecycle.js";
5
+ const claudeConfig = {
6
+ url: CLAUDE_URL,
7
+ label: "Claude",
8
+ displayName: "Claude",
9
+ beforeRetryHook: resetClaudePage,
10
+ waitForResponse: (page) => waitForAssistantToFinish(page, "claude"),
11
+ extractResponse: (page) => extractAssistantMarkdown(page, "claude"),
12
+ extractSources: (page) => extractSourcesFromClaude(page)
13
+ };
14
+ export {
15
+ claudeConfig
16
+ };
@@ -0,0 +1,54 @@
1
+ import { toErrorMessage } from "../../../../../shims/errors.js";
2
+ import { logger, PROVIDER_MODEL_RESPONSE_SELECTORS } from "../../../../../shims/utils.js";
3
+ import { buildSources } from "../../_shared/sourceUtils.js";
4
+ const CLAUDE_RAW_SOURCES_DOM_EXTRACTOR = String.raw`(
5
+ {
6
+ getCachedRawSources,
7
+ setCachedRawSources,
8
+ findLatestResponseElement,
9
+ extractClaudeRawSourcesFromResponseElement,
10
+ },
11
+ selectors,
12
+ ) => {
13
+ const cached = getCachedRawSources("claude");
14
+ if (cached) return cached;
15
+
16
+ const responseEl = findLatestResponseElement(selectors)?.element;
17
+ if (!responseEl) return [];
18
+
19
+ const rawSources = extractClaudeRawSourcesFromResponseElement(responseEl);
20
+ setCachedRawSources("claude", rawSources);
21
+ return rawSources;
22
+ }`;
23
+ async function fetchTitle(url) {
24
+ try {
25
+ const res = await fetch(url);
26
+ const html = await res.text();
27
+ const match = html.match(/<title>(.*?)<\/title>/i);
28
+ return match?.[1]?.trim() || null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ async function extractSourcesFromClaude(page) {
34
+ try {
35
+ const rawSources = await page.runDomOp("raw-sources", {
36
+ provider: "claude",
37
+ selectors: PROVIDER_MODEL_RESPONSE_SELECTORS.claude || []
38
+ });
39
+ const rawSourcesWithFetchedTitles = await Promise.all(
40
+ rawSources.map(async (source) => ({
41
+ ...source,
42
+ title: await fetchTitle(source.rawHref) || source.title
43
+ }))
44
+ );
45
+ return buildSources(rawSourcesWithFetchedTitles, { provider: "claude" });
46
+ } catch (error) {
47
+ logger.error(`Failed to extract Claude sources: ${toErrorMessage(error)}`);
48
+ return [];
49
+ }
50
+ }
51
+ export {
52
+ CLAUDE_RAW_SOURCES_DOM_EXTRACTOR,
53
+ extractSourcesFromClaude
54
+ };
@@ -0,0 +1,9 @@
1
+ import { resetProviderPage } from "../../_shared/resetProviderPage.js";
2
+ const CLAUDE_URL = "https://claude.ai/new";
3
+ async function resetClaudePage(page) {
4
+ await resetProviderPage(page, "claude", CLAUDE_URL);
5
+ }
6
+ export {
7
+ CLAUDE_URL,
8
+ resetClaudePage
9
+ };
@@ -0,0 +1,40 @@
1
+ import { extractAssistantMarkdown } from "../../../lib/input/markdown/toMarkdown.js";
2
+ import { openSourcesPanel } from "../../../lib/input/sources/openPanel.js";
3
+ import { findSourcesButton } from "../../../lib/input/sources/findButton.js";
4
+ import { waitForAssistantToFinish } from "../../../lib/input/response/waitForFinish.js";
5
+ import { extractSourcesFromGemini } from "./lib/extractSources.js";
6
+ import { GEMINI_URL, resetGeminiPage } from "./lib/pageLifecycle.js";
7
+ import {
8
+ handleGeminiConsentPage,
9
+ waitForGeminiConversationUrl
10
+ } from "./lib/session.js";
11
+ const geminiConfig = {
12
+ url: GEMINI_URL,
13
+ label: "Gemini",
14
+ displayName: "Gemini",
15
+ // Detect consent pages before attempting to locate the editor.
16
+ // consent.google.com has no Gemini composer, so without this check
17
+ // waitForEditorReady times out and misclassifies it as "no_editor".
18
+ postNavigationHook: async (page) => {
19
+ await handleGeminiConsentPage(page);
20
+ },
21
+ beforePromptHook: async (page) => {
22
+ await handleGeminiConsentPage(page);
23
+ },
24
+ checkSubmitSuccess: async (page, { preSubmitUrl }) => waitForGeminiConversationUrl(page, preSubmitUrl),
25
+ waitForResponse: (page) => waitForAssistantToFinish(page, "gemini"),
26
+ extractResponse: (page) => extractAssistantMarkdown(page, "gemini"),
27
+ beforeRetryHook: resetGeminiPage,
28
+ // No reset between prompts — session is reused in the same conversation.
29
+ // Navigating back to gemini.google.com on each prompt adds unnecessary
30
+ // round-trips and increases detection surface.
31
+ extractSources: async (page) => {
32
+ const btn = await findSourcesButton(page);
33
+ if (!btn) return [];
34
+ await openSourcesPanel(page, btn);
35
+ return extractSourcesFromGemini(page, btn);
36
+ }
37
+ };
38
+ export {
39
+ geminiConfig
40
+ };
@@ -0,0 +1,65 @@
1
+ import {
2
+ buildSources,
3
+ clickButtonViaDispatch
4
+ } from "../../_shared/sourceUtils.js";
5
+ const GEMINI_RAW_SOURCES_DOM_EXTRACTOR = String.raw`(_helpers) => {
6
+ const results = [];
7
+
8
+ const getLeafTexts = (anchor) => {
9
+ const texts = [];
10
+
11
+ for (const el of anchor.querySelectorAll("*")) {
12
+ const text = (el.textContent || "").trim();
13
+ if (!text) continue;
14
+
15
+ if (
16
+ Array.from(el.children).some(
17
+ (child) => (child.textContent || "").trim().length > 0,
18
+ )
19
+ ) {
20
+ continue;
21
+ }
22
+
23
+ texts.push(text);
24
+ }
25
+
26
+ return Array.from(new Set(texts));
27
+ };
28
+
29
+ for (const anchor of Array.from(
30
+ document.querySelectorAll(
31
+ 'inline-source-card a[href^="http"]',
32
+ ),
33
+ )) {
34
+ if (!(anchor instanceof HTMLAnchorElement)) continue;
35
+
36
+ const rawHref = anchor.href.replace(/#.*$/, "");
37
+ if (!rawHref) continue;
38
+ const texts = getLeafTexts(anchor);
39
+ if (texts.length === 0) continue;
40
+ const sorted = [...texts].sort((a, b) => a.length - b.length);
41
+ const citedText = sorted[sorted.length - 1] || "";
42
+ const title =
43
+ sorted.length >= 2 ? sorted[sorted.length - 2] || "" : sorted[0] || "";
44
+
45
+ results.push({
46
+ rawHref,
47
+ title,
48
+ citedText,
49
+ });
50
+ }
51
+
52
+ return results;
53
+ }`;
54
+ async function extractSourcesFromGemini(page, sourcesButton) {
55
+ const rawSources = await page.runDomOp("raw-sources", {
56
+ provider: "gemini"
57
+ });
58
+ if (!await clickButtonViaDispatch(page, sourcesButton)) return [];
59
+ await page.waitForTimeout(300);
60
+ return buildSources(rawSources, { provider: "gemini" });
61
+ }
62
+ export {
63
+ GEMINI_RAW_SOURCES_DOM_EXTRACTOR,
64
+ extractSourcesFromGemini
65
+ };
@@ -0,0 +1,14 @@
1
+ import { resetProviderPage } from "../../_shared/resetProviderPage.js";
2
+ import { handleGeminiConsentPage } from "./session.js";
3
+ const GEMINI_URL = "https://gemini.google.com/";
4
+ async function resetGeminiPage(page) {
5
+ await resetProviderPage(page, "gemini", GEMINI_URL, {
6
+ postNavigationHook: async (currentPage) => {
7
+ await handleGeminiConsentPage(currentPage);
8
+ }
9
+ });
10
+ }
11
+ export {
12
+ GEMINI_URL,
13
+ resetGeminiPage
14
+ };
@@ -0,0 +1,45 @@
1
+ import { ExternalServiceError } from "../../../../../shims/errors.js";
2
+ import { clickLocatorLikeUser } from "../../../../lib/browser/humanBehavior.js";
3
+ const GEMINI_CONSENT_SELECTOR = "button#L2AGLb, button#W0wltc, form[action*='consent.google.com'] button";
4
+ async function handleGeminiConsentPage(page) {
5
+ const url = page.url();
6
+ if (!url.includes("consent.google.com")) return;
7
+ const consentBtn = page.locator(GEMINI_CONSENT_SELECTOR).first();
8
+ const visible = await consentBtn.isVisible({ timeout: 3e3 }).catch(() => false);
9
+ if (visible) {
10
+ await clickLocatorLikeUser(page, consentBtn, { timeout: 4e3 }).catch(() => {
11
+ });
12
+ await page.waitForTimeout(1e3);
13
+ if (!page.url().includes("consent.google.com")) return;
14
+ }
15
+ throw new ExternalServiceError(
16
+ "gemini",
17
+ "Google consent page not dismissible \u2014 proxy IP requires Google consent",
18
+ 429
19
+ );
20
+ }
21
+ function isGeminiAppUrl(rawUrl) {
22
+ try {
23
+ const url = new URL(rawUrl);
24
+ return url.hostname === "gemini.google.com" && url.pathname.startsWith("/app/") && url.pathname.length > "/app/".length;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ async function waitForGeminiConversationUrl(page, preSubmitUrl) {
30
+ if (isGeminiAppUrl(preSubmitUrl)) {
31
+ return void 0;
32
+ }
33
+ const deadline = Date.now() + 4e3;
34
+ while (Date.now() < deadline) {
35
+ if (isGeminiAppUrl(await page.getUrl().catch(() => page.url()))) {
36
+ return true;
37
+ }
38
+ await page.waitForTimeout(100);
39
+ }
40
+ return false;
41
+ }
42
+ export {
43
+ handleGeminiConsentPage,
44
+ waitForGeminiConversationUrl
45
+ };
@@ -0,0 +1,15 @@
1
+ import { aiOverviewConfig } from "./ai-overview/index.js";
2
+ import { chatgptConfig } from "./chatgpt/index.js";
3
+ import { claudeConfig } from "./claude/index.js";
4
+ import { geminiConfig } from "./gemini/index.js";
5
+ import { perplexityConfig } from "./perplexity/index.js";
6
+ const PROVIDER_CONFIGS = {
7
+ gemini: geminiConfig,
8
+ chatgpt: chatgptConfig,
9
+ perplexity: perplexityConfig,
10
+ claude: claudeConfig,
11
+ "ai-overview": aiOverviewConfig
12
+ };
13
+ export {
14
+ PROVIDER_CONFIGS
15
+ };
@@ -0,0 +1,31 @@
1
+ import { extractSourcesFromPerplexity } from "./lib/extractSources.js";
2
+ import { extractAssistantMarkdown } from "../../../lib/input/markdown/toMarkdown.js";
3
+ import { openSourcesPanel } from "../../../lib/input/sources/openPanel.js";
4
+ import { findSourcesButton } from "../../../lib/input/sources/findButton.js";
5
+ import { waitForAssistantToFinish } from "../../../lib/input/response/waitForFinish.js";
6
+ import {
7
+ PERPLEXITY_URL,
8
+ perplexityPostNavigationHook,
9
+ resetPerplexityPage,
10
+ waitForPerplexitySearchUrl
11
+ } from "./lib/navigation.js";
12
+ const perplexityConfig = {
13
+ url: PERPLEXITY_URL,
14
+ label: "Perplexity",
15
+ displayName: "Perplexity",
16
+ beforeRetryHook: resetPerplexityPage,
17
+ betweenPromptsHook: resetPerplexityPage,
18
+ checkSubmitSuccess: async (page, { preSubmitUrl }) => waitForPerplexitySearchUrl(page, preSubmitUrl),
19
+ waitForResponse: (page) => waitForAssistantToFinish(page, "perplexity"),
20
+ extractResponse: (page) => extractAssistantMarkdown(page, "perplexity"),
21
+ postNavigationHook: perplexityPostNavigationHook,
22
+ extractSources: async (page) => {
23
+ const btn = await findSourcesButton(page);
24
+ if (!btn) return [];
25
+ await openSourcesPanel(page, btn);
26
+ return extractSourcesFromPerplexity(page, btn);
27
+ }
28
+ };
29
+ export {
30
+ perplexityConfig
31
+ };
@@ -0,0 +1,84 @@
1
+ import {
2
+ canUseOsLevelInput,
3
+ pressKeyLikeUser
4
+ } from "../../../../lib/browser/humanBehavior.js";
5
+ import {
6
+ buildSources,
7
+ clickButtonViaDispatch
8
+ } from "../../_shared/sourceUtils.js";
9
+ const PERPLEXITY_RAW_SOURCES_DOM_EXTRACTOR = String.raw`(_helpers) => {
10
+ const results = [];
11
+ const panel = document.querySelector(
12
+ '[role="tabpanel"][aria-labelledby*="citations"]',
13
+ );
14
+ if (!panel) return results;
15
+
16
+ const getCleanTexts = (anchor) => {
17
+ const texts = [];
18
+
19
+ for (const el of anchor.querySelectorAll("*")) {
20
+ const text = (el.textContent || "").trim();
21
+ if (!text) continue;
22
+
23
+ if (
24
+ Array.from(el.children).some(
25
+ (child) => (child.textContent || "").trim().length > 0,
26
+ )
27
+ ) {
28
+ continue;
29
+ }
30
+
31
+ texts.push(text);
32
+ }
33
+
34
+ return Array.from(new Set(texts));
35
+ };
36
+
37
+ for (const anchor of Array.from(panel.querySelectorAll('a[href^="http"]'))) {
38
+ if (!(anchor instanceof HTMLAnchorElement)) continue;
39
+
40
+ const rawHref = anchor.href.replace(/#.*$/, "");
41
+ if (!rawHref) continue;
42
+
43
+ const texts = getCleanTexts(anchor);
44
+ if (texts.length === 0) continue;
45
+
46
+ const sorted = [...texts].sort((a, b) => a.length - b.length);
47
+ const citedText = sorted[sorted.length - 1] || "";
48
+ const title =
49
+ sorted.length >= 2 ? sorted[sorted.length - 2] || "" : sorted[0] || "";
50
+
51
+ results.push({
52
+ rawHref,
53
+ title,
54
+ citedText,
55
+ });
56
+ }
57
+
58
+ return results;
59
+ }`;
60
+ async function extractSourcesFromPerplexity(page, sourcesButton) {
61
+ const rawSources = await page.runDomOp("raw-sources", {
62
+ provider: "perplexity"
63
+ });
64
+ const clickedToClose = await clickButtonViaDispatch(page, sourcesButton).catch(
65
+ () => false
66
+ );
67
+ if (!clickedToClose) {
68
+ const escaped2 = await pressKeyLikeUser(page, "Escape").catch(() => false);
69
+ if (!escaped2 && canUseOsLevelInput(page)) {
70
+ return buildSources(rawSources, { provider: "perplexity" });
71
+ }
72
+ }
73
+ await page.waitForTimeout(300);
74
+ const escaped = await pressKeyLikeUser(page, "Escape").catch(() => false);
75
+ if (!escaped && canUseOsLevelInput(page)) {
76
+ return buildSources(rawSources, { provider: "perplexity" });
77
+ }
78
+ await page.waitForTimeout(300);
79
+ return buildSources(rawSources, { provider: "perplexity" });
80
+ }
81
+ export {
82
+ PERPLEXITY_RAW_SOURCES_DOM_EXTRACTOR,
83
+ extractSourcesFromPerplexity
84
+ };
@@ -0,0 +1,38 @@
1
+ import { resetProviderPage } from "../../_shared/resetProviderPage.js";
2
+ const PERPLEXITY_URL = "https://www.perplexity.ai/";
3
+ function isPerplexitySearchUrl(rawUrl) {
4
+ try {
5
+ const url = new URL(rawUrl);
6
+ return url.hostname.endsWith("perplexity.ai") && url.pathname.startsWith("/search/") && url.pathname.length > "/search/".length;
7
+ } catch {
8
+ return false;
9
+ }
10
+ }
11
+ async function waitForPerplexitySearchUrl(page, preSubmitUrl) {
12
+ if (isPerplexitySearchUrl(preSubmitUrl)) {
13
+ return void 0;
14
+ }
15
+ const deadline = Date.now() + 4e3;
16
+ while (Date.now() < deadline) {
17
+ if (isPerplexitySearchUrl(await page.getUrl().catch(() => page.url()))) {
18
+ return true;
19
+ }
20
+ await page.waitForTimeout(100);
21
+ }
22
+ return false;
23
+ }
24
+ async function perplexityPostNavigationHook(page) {
25
+ const delay = 1e3 + Math.floor(Math.random() * 1e3);
26
+ await page.waitForTimeout(delay);
27
+ }
28
+ async function resetPerplexityPage(page) {
29
+ await resetProviderPage(page, "perplexity", PERPLEXITY_URL, {
30
+ postNavigationHook: perplexityPostNavigationHook
31
+ });
32
+ }
33
+ export {
34
+ PERPLEXITY_URL,
35
+ perplexityPostNavigationHook,
36
+ resetPerplexityPage,
37
+ waitForPerplexitySearchUrl
38
+ };