agentlinkops 0.5.0 → 0.6.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 (174) 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/LICENSE +0 -0
  6. package/NOTICE +0 -0
  7. package/README.md +2 -1
  8. package/bin/agentlinkops.mjs +0 -0
  9. package/bin/linktrail.mjs +0 -0
  10. package/cli/adapters/csv.js +0 -0
  11. package/cli/adapters/sqlite-crm.js +0 -0
  12. package/cli/adapters/suppliers.js +0 -0
  13. package/cli/agent-setup.js +0 -0
  14. package/cli/agentlinkops.mjs +0 -0
  15. package/cli/args.js +0 -0
  16. package/cli/catalog.js +0 -0
  17. package/cli/check.js +0 -0
  18. package/cli/citation.js +124 -2
  19. package/cli/client.js +1 -1
  20. package/cli/commands.js +0 -0
  21. package/cli/config.js +0 -0
  22. package/cli/connection.js +0 -0
  23. package/cli/context-mcp.js +0 -0
  24. package/cli/context.js +0 -0
  25. package/cli/delivery.js +0 -0
  26. package/cli/doctor.js +19 -0
  27. package/cli/env.js +0 -0
  28. package/cli/fleet.js +0 -0
  29. package/cli/gsc-browser.js +0 -0
  30. package/cli/import.js +0 -0
  31. package/cli/ledger.js +0 -0
  32. package/cli/linktrail.mjs +0 -0
  33. package/cli/local-result.js +0 -0
  34. package/cli/main.js +0 -0
  35. package/cli/mirror.js +0 -0
  36. package/cli/mix.js +0 -0
  37. package/cli/platforms.js +0 -0
  38. package/cli/receipt-performance.js +0 -0
  39. package/cli/receipts.js +0 -0
  40. package/cli/report.js +0 -0
  41. package/cli/setup-plan.js +0 -0
  42. package/cli/skill.js +0 -0
  43. package/cli/state.js +0 -0
  44. package/cli/status.js +0 -0
  45. package/cli/sync.js +0 -0
  46. package/package.json +14 -3
  47. package/references/cli.md +0 -0
  48. package/references/disavow.md +0 -0
  49. package/references/reports.md +0 -0
  50. package/scripts/agentlinkops.py +0 -0
  51. package/scripts/disavow.py +0 -0
  52. package/scripts/linktrail.py +0 -0
  53. package/scripts/migrations/001_crm.sql +0 -0
  54. package/scripts/migrations/002_cloud_events.sql +0 -0
  55. package/scripts/migrations/003_disavow.sql +0 -0
  56. package/scripts/migrations/004_placement_costs.sql +0 -0
  57. package/scripts/report.py +0 -0
  58. package/shared/agent-clients.js +0 -0
  59. package/shared/observation-event.js +0 -0
  60. package/shared/response-contract.js +0 -0
  61. package/shared/search-connection-contract.js +0 -0
  62. package/skills/agentlinkops-assets/SKILL.md +0 -0
  63. package/skills/agentlinkops-assets/agents/openai.yaml +0 -0
  64. package/skills/agentlinkops-campaigns/SKILL.md +0 -0
  65. package/skills/agentlinkops-campaigns/agents/openai.yaml +0 -0
  66. package/skills/agentlinkops-connect/SKILL.md +0 -0
  67. package/skills/agentlinkops-connect/agents/openai.yaml +0 -0
  68. package/skills/agentlinkops-connect/references/agentlinkops.md +0 -0
  69. package/skills/agentlinkops-crm/SKILL.md +0 -0
  70. package/skills/agentlinkops-crm/agents/openai.yaml +0 -0
  71. package/skills/content-defingerprinting/SKILL.md +0 -0
  72. package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -0
  73. package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -0
  74. package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -0
  75. package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -0
  76. package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -0
  77. package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -0
  78. package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -0
  79. package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -0
  80. package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -0
  81. package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -0
  82. package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -0
  83. package/skills/internal-linking-optimizer/SKILL.md +0 -0
  84. package/skills/internal-linking-optimizer/references/source-prompt.md +0 -0
  85. package/skills/pre-publish-review/SKILL.md +0 -0
  86. package/skills/source-cited-content-builder/SKILL.md +0 -0
  87. package/skills/source-cited-content-builder/references/source-prompt.md +0 -0
  88. package/skills/source-cited-humanizer/SKILL.md +0 -0
  89. package/src/citations/adapter.js +173 -0
  90. package/src/citations/alert.js +53 -0
  91. package/src/citations/browser/engine.js +267 -0
  92. package/src/citations/browser/gen/core/providers/_shared/google.js +4 -0
  93. package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +15 -0
  94. package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +22 -0
  95. package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +8 -0
  96. package/src/citations/browser/gen/core/providers/ai-overview/index.js +70 -0
  97. package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +59 -0
  98. package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +60 -0
  99. package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +18 -0
  100. package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +62 -0
  101. package/src/citations/browser/gen/core/providers/chatgpt/index.js +28 -0
  102. package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +39 -0
  103. package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +66 -0
  104. package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +11 -0
  105. package/src/citations/browser/gen/core/providers/claude/index.js +16 -0
  106. package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +54 -0
  107. package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +9 -0
  108. package/src/citations/browser/gen/core/providers/gemini/index.js +40 -0
  109. package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +65 -0
  110. package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +14 -0
  111. package/src/citations/browser/gen/core/providers/gemini/lib/session.js +45 -0
  112. package/src/citations/browser/gen/core/providers/index.js +15 -0
  113. package/src/citations/browser/gen/core/providers/perplexity/index.js +31 -0
  114. package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +84 -0
  115. package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +38 -0
  116. package/src/citations/browser/gen/core/providers/types.js +0 -0
  117. package/src/citations/browser/gen/lib/browser/camoufox.js +587 -0
  118. package/src/citations/browser/gen/lib/browser/display.js +147 -0
  119. package/src/citations/browser/gen/lib/browser/domOps.js +339 -0
  120. package/src/citations/browser/gen/lib/browser/humanBehavior.js +121 -0
  121. package/src/citations/browser/gen/lib/browser/navigate.js +46 -0
  122. package/src/citations/browser/gen/lib/browser/playwrightCompat.js +321 -0
  123. package/src/citations/browser/gen/lib/browser/proxy/forwarder.js +251 -0
  124. package/src/citations/browser/gen/lib/browser/runtimeTypes.js +0 -0
  125. package/src/citations/browser/gen/lib/extraction/sourceUtils.js +71 -0
  126. package/src/citations/browser/gen/lib/input/editor/assertNotBlocked.js +54 -0
  127. package/src/citations/browser/gen/lib/input/editor/clearInput.js +52 -0
  128. package/src/citations/browser/gen/lib/input/editor/findEditor.js +54 -0
  129. package/src/citations/browser/gen/lib/input/editor/findSendButton.js +21 -0
  130. package/src/citations/browser/gen/lib/input/editor/promptInput.js +117 -0
  131. package/src/citations/browser/gen/lib/input/markdown/converter.js +101 -0
  132. package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +14 -0
  133. package/src/citations/browser/gen/lib/input/response/detectBotPage.js +13 -0
  134. package/src/citations/browser/gen/lib/input/response/getText.js +10 -0
  135. package/src/citations/browser/gen/lib/input/response/isGenerating.js +59 -0
  136. package/src/citations/browser/gen/lib/input/response/waitForFinish.js +105 -0
  137. package/src/citations/browser/gen/lib/input/sources/findButton.js +41 -0
  138. package/src/citations/browser/gen/lib/input/sources/openPanel.js +11 -0
  139. package/src/citations/browser/gen/utils/agent-constants.js +87 -0
  140. package/src/citations/browser/providers.js +166 -0
  141. package/src/citations/browser/shims/errors.js +26 -0
  142. package/src/citations/browser/shims/types.js +6 -0
  143. package/src/citations/browser/shims/utils.js +32 -0
  144. package/src/citations/contract.js +19 -3
  145. package/src/citations/fixtures/dataforseo-aio.json +82 -0
  146. package/src/citations/match.js +0 -0
  147. package/src/citations/panel-builder.js +44 -0
  148. package/src/citations/report.js +165 -0
  149. package/src/citations/runner.js +43 -16
  150. package/src/citations/stats.js +63 -0
  151. package/src/citations/watch.js +85 -0
  152. package/src/competitors/contract.js +0 -0
  153. package/src/competitors/domain-mix.js +0 -0
  154. package/src/context/ga4.js +0 -0
  155. package/src/context/gsc.js +0 -0
  156. package/src/context/site-profile.js +0 -0
  157. package/src/context/util.js +0 -0
  158. package/src/context-tool-registry.js +0 -0
  159. package/src/corpus/acquire.js +0 -0
  160. package/src/corpus/seeds.js +0 -0
  161. package/src/discovery/contract.js +0 -0
  162. package/src/discovery/import.js +0 -0
  163. package/src/tool-views.js +0 -0
  164. package/src/verifier/fetch.js +0 -0
  165. package/src/verifier/index.js +0 -0
  166. package/src/verifier/observation-kind.js +0 -0
  167. package/src/verifier/parser.js +0 -0
  168. package/src/verifier/readiness.js +0 -0
  169. package/src/verifier/render-evidence.js +0 -0
  170. package/src/verifier/robots.js +0 -0
  171. package/src/verifier/transition.js +0 -0
  172. package/src/verifier/url.js +0 -0
  173. package/templates/SITE.template.md +0 -0
  174. package/templates/campaigns.v1.json +0 -0
@@ -0,0 +1,59 @@
1
+ import { logger } from "../../../../../shims/utils.js";
2
+ const AI_OVERVIEW_SOURCE_ROOT_SELECTOR = '[data-container-id="rhs-col"]';
3
+ const AI_OVERVIEW_BUTTON_SELECTOR = '[role="button"], button';
4
+ function shouldClickSourceExpansionButton(text, aria) {
5
+ const t = `${text} ${aria}`.toLowerCase();
6
+ if (t.includes("videos")) return false;
7
+ if (t.includes("more")) return true;
8
+ if (t.includes("all")) return true;
9
+ if (t.includes("expand")) return true;
10
+ return false;
11
+ }
12
+ async function expandAIOverviewSources(page) {
13
+ const matches = await page.evaluate(
14
+ ({
15
+ rootSelector,
16
+ buttonSelector
17
+ }) => {
18
+ const root = document.querySelector(rootSelector);
19
+ if (!(root instanceof HTMLElement)) return [];
20
+ return Array.from(root.querySelectorAll(buttonSelector)).map((button, index) => {
21
+ if (!(button instanceof HTMLElement)) {
22
+ return null;
23
+ }
24
+ const style = window.getComputedStyle(button);
25
+ const rect = button.getBoundingClientRect();
26
+ const visible = button.isConnected && style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" && rect.width > 0 && rect.height > 0;
27
+ if (!visible) return null;
28
+ return {
29
+ index,
30
+ text: (button.textContent || "").trim(),
31
+ aria: button.getAttribute("aria-label")?.trim() || ""
32
+ };
33
+ }).filter(
34
+ (match) => match !== null
35
+ );
36
+ },
37
+ {
38
+ rootSelector: AI_OVERVIEW_SOURCE_ROOT_SELECTOR,
39
+ buttonSelector: AI_OVERVIEW_BUTTON_SELECTOR
40
+ }
41
+ );
42
+ for (const match of matches) {
43
+ if (!shouldClickSourceExpansionButton(match.text, match.aria)) {
44
+ continue;
45
+ }
46
+ const button = page.locator(`${AI_OVERVIEW_SOURCE_ROOT_SELECTOR} ${AI_OVERVIEW_BUTTON_SELECTOR}`).nth(match.index);
47
+ const visible = await button.isVisible().catch(() => false);
48
+ if (!visible) continue;
49
+ logger.debug(
50
+ `[ai-overview] expanding sources text="${match.text}" aria="${match.aria}"`
51
+ );
52
+ await button.click({ timeout: 5e3 }).catch(() => {
53
+ });
54
+ await page.waitForTimeout(700);
55
+ }
56
+ }
57
+ export {
58
+ expandAIOverviewSources
59
+ };
@@ -0,0 +1,60 @@
1
+ import { toErrorMessage } from "../../../../../shims/errors.js";
2
+ import { logger } from "../../../../../shims/utils.js";
3
+ import { buildSources } from "../../_shared/sourceUtils.js";
4
+ const AI_OVERVIEW_RAW_SOURCES_DOM_EXTRACTOR = String.raw`(_helpers) => {
5
+ const results = [];
6
+ const rhsCol = document.querySelector('[data-container-id="rhs-col"]');
7
+ if (!rhsCol) {
8
+ return { rawSources: results, containerFound: false };
9
+ }
10
+
11
+ for (const card of Array.from(rhsCol.querySelectorAll("div[data-src-id]"))) {
12
+ if (!(card instanceof HTMLElement)) continue;
13
+
14
+ const link = card.querySelector('a[href^="http"]');
15
+ if (!(link instanceof HTMLAnchorElement)) continue;
16
+
17
+ const title =
18
+ link
19
+ .getAttribute("aria-label")
20
+ ?.replace(/\.\s*Opens in new tab\.?$/i, "")
21
+ .trim() || link.href;
22
+ const citedText =
23
+ card.querySelector("[data-crb-snippet-text]")?.textContent?.trim() || "";
24
+
25
+ results.push({
26
+ rawHref: link.href,
27
+ title,
28
+ citedText,
29
+ });
30
+ }
31
+
32
+ return { rawSources: results, containerFound: true };
33
+ }`;
34
+ function normalizeAIOverviewTitle(title) {
35
+ return title.replace(/\s*\.?\s*opens in new tab\.?\s*$/i, "").trim();
36
+ }
37
+ async function extractAIOverviewSources(page) {
38
+ try {
39
+ const { rawSources, containerFound } = await page.runDomOp("raw-sources", {
40
+ provider: "ai-overview"
41
+ });
42
+ if (!containerFound) {
43
+ logger.warn("AI Overview container not found \u2014 no sources extracted");
44
+ }
45
+ const normalizedSources = rawSources.map((source) => ({
46
+ ...source,
47
+ title: normalizeAIOverviewTitle(source.title ?? "") || source.rawHref
48
+ }));
49
+ return buildSources(normalizedSources, { provider: "ai-overview" });
50
+ } catch (err) {
51
+ logger.error(
52
+ `Failed to extract AI Overview sources: ${toErrorMessage(err)}`
53
+ );
54
+ return [];
55
+ }
56
+ }
57
+ export {
58
+ AI_OVERVIEW_RAW_SOURCES_DOM_EXTRACTOR,
59
+ extractAIOverviewSources
60
+ };
@@ -0,0 +1,18 @@
1
+ import { resetProviderPage } from "../../_shared/resetProviderPage.js";
2
+ import {
3
+ assertAIOverviewPageNotBlocked,
4
+ dismissGoogleConsentDialog
5
+ } from "./session.js";
6
+ const AI_OVERVIEW_URL = "https://www.google.com/";
7
+ async function resetAIOverviewPage(page) {
8
+ await resetProviderPage(page, "ai-overview", AI_OVERVIEW_URL, {
9
+ postNavigationHook: async (currentPage) => {
10
+ assertAIOverviewPageNotBlocked(currentPage);
11
+ await dismissGoogleConsentDialog(currentPage);
12
+ }
13
+ });
14
+ }
15
+ export {
16
+ AI_OVERVIEW_URL,
17
+ resetAIOverviewPage
18
+ };
@@ -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
+ };