mcp-scraper 0.6.0 → 0.8.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 (57) hide show
  1. package/dist/bin/api-server.cjs +3771 -3613
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +2 -2
  4. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +1 -1
  7. package/dist/bin/mcp-scraper-install.cjs +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-install.js +1 -1
  10. package/dist/bin/mcp-stdio-server.cjs +3 -3
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +4 -4
  13. package/dist/bin/paa-harvest.cjs +166 -74
  14. package/dist/bin/paa-harvest.cjs.map +1 -1
  15. package/dist/bin/paa-harvest.js +1 -1
  16. package/dist/chunk-3MKSXDJ7.js +7 -0
  17. package/dist/chunk-3MKSXDJ7.js.map +1 -0
  18. package/dist/{chunk-APJO2XV5.js → chunk-HL33CGJF.js} +3 -3
  19. package/dist/{chunk-APJO2XV5.js.map → chunk-HL33CGJF.js.map} +1 -1
  20. package/dist/{chunk-3K5GT6VG.js → chunk-ITJC3NN4.js} +5 -4
  21. package/dist/{chunk-GWRIO6JT.js → chunk-R7EETU7Z.js} +134 -134
  22. package/dist/chunk-R7EETU7Z.js.map +1 -0
  23. package/dist/{chunk-AUCXKRRH.js → chunk-RMPPYKUV.js} +167 -75
  24. package/dist/chunk-RMPPYKUV.js.map +1 -0
  25. package/dist/{chunk-BL7BBSYF.js → chunk-RUGJE5EB.js} +2 -2
  26. package/dist/{extract-bundle-COS56ZDO.js → extract-bundle-U4D5LW5W.js} +4 -2
  27. package/dist/extract-bundle-U4D5LW5W.js.map +1 -0
  28. package/dist/index.cjs +166 -74
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/{server-WHM446IZ.js → server-WBDVVHR6.js} +151 -96
  32. package/dist/server-WBDVVHR6.js.map +1 -0
  33. package/dist/{site-extract-repository-U476J44K.js → site-extract-repository-NVSZH35Y.js} +3 -3
  34. package/dist/{worker-KJZ3ZN2N.js → worker-JQTS437L.js} +4 -4
  35. package/dist/worker-JQTS437L.js.map +1 -0
  36. package/package.json +1 -1
  37. package/dist/chunk-AUCXKRRH.js.map +0 -1
  38. package/dist/chunk-F2BXETDX.js +0 -7
  39. package/dist/chunk-F2BXETDX.js.map +0 -1
  40. package/dist/chunk-GWRIO6JT.js.map +0 -1
  41. package/dist/extract-bundle-COS56ZDO.js.map +0 -1
  42. package/dist/server-WHM446IZ.js.map +0 -1
  43. package/dist/worker-KJZ3ZN2N.js.map +0 -1
  44. package/docs/spec-browser-routing.md +0 -108
  45. package/docs/spec-kernel-computer-controls.md +0 -166
  46. package/docs/spec-mcp-tools.md +0 -146
  47. package/docs/spec-server-instructions.md +0 -94
  48. package/docs/specs/agent-ready-seo-packet-spec.md +0 -237
  49. package/docs/specs/audit-visual-demo.md +0 -309
  50. package/docs/specs/cli-agent-wiring-spec.md +0 -206
  51. package/docs/specs/local-competitive-audit-spec.md +0 -311
  52. package/docs/specs/scheduled-workflows-api-spec.md +0 -304
  53. package/docs/specs/seo-cli-growth-roadmap-spec.md +0 -181
  54. package/docs/specs/seo-workflow-runner-and-reports-spec.md +0 -241
  55. /package/dist/{chunk-3K5GT6VG.js.map → chunk-ITJC3NN4.js.map} +0 -0
  56. /package/dist/{chunk-BL7BBSYF.js.map → chunk-RUGJE5EB.js.map} +0 -0
  57. /package/dist/{site-extract-repository-U476J44K.js.map → site-extract-repository-NVSZH35Y.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  LedgerOperation
3
- } from "./chunk-APJO2XV5.js";
3
+ } from "./chunk-HL33CGJF.js";
4
4
  import {
5
5
  sanitizeVendorName
6
6
  } from "./chunk-M2S27J6Z.js";
@@ -127,4 +127,4 @@ export {
127
127
  failExtractJob,
128
128
  settleExtractJob
129
129
  };
130
- //# sourceMappingURL=chunk-BL7BBSYF.js.map
130
+ //# sourceMappingURL=chunk-RUGJE5EB.js.map
@@ -4,7 +4,7 @@ import {
4
4
  renderImageSection,
5
5
  renderIssueReport,
6
6
  renderLinkReport
7
- } from "./chunk-GWRIO6JT.js";
7
+ } from "./chunk-R7EETU7Z.js";
8
8
  import {
9
9
  getDb
10
10
  } from "./chunk-GL4BW4CP.js";
@@ -234,6 +234,7 @@ ${renderImageSection(extras.imageAudit)}`;
234
234
  writeFileSync(join(dir, "images.jsonl"), extras.imageAudit.rows.map((r) => JSON.stringify(r)).join("\n"));
235
235
  writeFileSync(join(dir, "images-summary.json"), JSON.stringify(extras.imageAudit.summary, null, 2));
236
236
  }
237
+ if (extras.seoAudit) writeFileSync(join(dir, "seo-audit.json"), JSON.stringify(extras.seoAudit, null, 2));
237
238
  if (extras.branding) writeFileSync(join(dir, "branding.json"), JSON.stringify(extras.branding, null, 2));
238
239
  const artifactFiles = [
239
240
  { name: "report.md", type: "text/markdown" },
@@ -249,6 +250,7 @@ ${renderImageSection(extras.imageAudit)}`;
249
250
  artifactFiles.push({ name: "images.jsonl", type: "application/x-ndjson" });
250
251
  artifactFiles.push({ name: "images-summary.json", type: "application/json" });
251
252
  }
253
+ if (extras.seoAudit) artifactFiles.push({ name: "seo-audit.json", type: "application/json" });
252
254
  if (extras.branding) artifactFiles.push({ name: "branding.json", type: "application/json" });
253
255
  const zip = new ZipFile();
254
256
  const zipOut = createWriteStream(join(dir, "bundle.zip"));
@@ -273,4 +275,4 @@ ${renderImageSection(extras.imageAudit)}`;
273
275
  export {
274
276
  assembleExtractArtifacts
275
277
  };
276
- //# sourceMappingURL=extract-bundle-COS56ZDO.js.map
278
+ //# sourceMappingURL=extract-bundle-U4D5LW5W.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/api/extract-bundle.ts"],"sourcesContent":["import { createWriteStream, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { createGzip } from 'node:zlib'\nimport { once } from 'node:events'\nimport { ZipFile } from 'yazl'\nimport { getDb } from './db.js'\nimport { getBlobStore, type StoredArtifact } from './blob-store.js'\nimport { computeIssues, renderIssueReport } from './seo-issues.js'\nimport { renderLinkReport, type ExtDomainRow, type LinkReport } from './seo-link-report.js'\nimport type { PageLinkMetrics } from './seo-link-graph.js'\nimport type { SeoAudit } from './seo-audit.js'\nimport type { ExtractJobRow } from './site-extract-repository.js'\nimport type { PageData } from './site-extractor.js'\nimport { renderImageSection, type ImageAudit } from './image-audit.js'\n\nconst CHUNK = 400\n\nfunction normalize(u: string): string {\n return u.split('#')[0].replace(/\\/$/, '')\n}\n\nfunction registrableDomain(host: string): string {\n const h = host.replace(/^www\\./, '').toLowerCase()\n const parts = h.split('.')\n return parts.length <= 2 ? h : parts.slice(-2).join('.')\n}\n\nfunction slugFactory() {\n const counts = new Map<string, number>()\n return (url: string): string => {\n const base = url.replace(/^https?:\\/\\//, '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'page'\n const n = counts.get(base) ?? 0\n counts.set(base, n + 1)\n return n ? `${base}-${n}` : base\n }\n}\n\nasync function* pageChunks(jobId: string): AsyncGenerator<PageData[]> {\n const db = getDb()\n for (let offset = 0; ; offset += CHUNK) {\n const res = await db.execute({\n sql: `SELECT page FROM site_extract_pages WHERE job_id = ? ORDER BY rowid LIMIT ${CHUNK} OFFSET ?`,\n args: [jobId, offset],\n })\n if (!res.rows.length) return\n yield res.rows.map(r => JSON.parse(String((r as unknown as Record<string, unknown>).page)) as PageData)\n }\n}\n\nexport interface AssembleExtras {\n branding?: unknown\n imageAudit?: ImageAudit | null\n seoAudit?: SeoAudit | null\n}\n\nexport async function assembleExtractArtifacts(job: ExtractJobRow, extras: AssembleExtras = {}): Promise<StoredArtifact[]> {\n const dir = join(tmpdir(), `extract-bundle-${job.id}-${Date.now()}`)\n mkdirSync(join(dir, 'pages'), { recursive: true })\n try {\n const metas: PageData[] = []\n const statusByUrl = new Map<string, number | null>()\n for await (const chunk of pageChunks(job.id)) {\n for (const p of chunk) {\n statusByUrl.set(normalize(p.url), p.status)\n const { bodyMarkdown: _b, schema: _s, outlinks: _o, ...slim } = p\n metas.push(slim as unknown as PageData)\n }\n }\n\n const gz = createGzip()\n const linksOut = createWriteStream(join(dir, 'links.jsonl.gz'))\n gz.pipe(linksOut)\n const gzWrite = async (line: string) => { if (!gz.write(line)) await once(gz, 'drain') }\n\n const slug = slugFactory()\n const inboundFrom = new Map<string, Set<string>>()\n const anchorCounts = new Map<string, Map<string, number>>()\n const adjacency = new Map<string, Set<string>>()\n const outCounts = new Map<string, { int: number; ext: number }>()\n const domMap = new Map<string, { links: number; nofollow: number; pages: Set<string> }>()\n const brokenLinkPages = new Set<string>()\n let internalTotal = 0\n let externalTotal = 0\n let brokenInternal = 0\n let pagesWithBody = 0\n let siteReg = ''\n try { siteReg = registrableDomain(new URL(job.startUrl).hostname) } catch { siteReg = '' }\n\n for await (const chunk of pageChunks(job.id)) {\n for (const p of chunk) {\n if (p.bodyMarkdown) {\n pagesWithBody++\n writeFileSync(join(dir, 'pages', slug(p.url) + '.md'),\n `# ${p.title ?? p.url}\\n\\nURL: ${p.url}\\nStatus: ${p.status}\\n\\n---\\n\\n${p.bodyMarkdown}\\n`)\n }\n const from = normalize(p.url)\n const oc = { int: 0, ext: 0 }\n for (const l of p.outlinks ?? []) {\n const nofollow = (l.rel ?? '').split(/\\s+/).includes('nofollow')\n const to = normalize(l.href)\n const targetStatus = statusByUrl.get(to) ?? null\n await gzWrite(JSON.stringify({ from: p.url, to: l.href, anchor: l.anchor, rel: l.rel, internal: l.internal, nofollow, targetStatus }) + '\\n')\n if (l.internal) {\n oc.int++\n internalTotal++\n if (!inboundFrom.has(to)) inboundFrom.set(to, new Set())\n inboundFrom.get(to)!.add(from)\n if (l.anchor) {\n if (!anchorCounts.has(to)) anchorCounts.set(to, new Map())\n const ac = anchorCounts.get(to)!\n ac.set(l.anchor, (ac.get(l.anchor) ?? 0) + 1)\n }\n if (!nofollow && (statusByUrl.get(to) === 200 || !statusByUrl.has(to))) {\n if (!adjacency.has(from)) adjacency.set(from, new Set())\n adjacency.get(from)!.add(to)\n }\n if (targetStatus != null && targetStatus >= 400) {\n brokenInternal++\n brokenLinkPages.add(p.url)\n }\n } else {\n oc.ext++\n let domain = ''\n try { domain = registrableDomain(new URL(l.href).hostname) } catch { continue }\n if (!domain || domain === siteReg) continue\n externalTotal++\n const d = domMap.get(domain) ?? { links: 0, nofollow: 0, pages: new Set<string>() }\n d.links++\n if (nofollow) d.nofollow++\n d.pages.add(p.url)\n domMap.set(domain, d)\n }\n }\n outCounts.set(p.url, oc)\n }\n }\n gz.end()\n await once(linksOut, 'finish')\n\n const depth = new Map<string, number>()\n const start = normalize(job.startUrl)\n depth.set(start, 0)\n const queue: string[] = [start]\n while (queue.length > 0) {\n const cur = queue.shift()!\n const d = depth.get(cur)!\n for (const next of adjacency.get(cur) ?? []) {\n if (!depth.has(next)) { depth.set(next, d + 1); queue.push(next) }\n }\n }\n\n const metrics = new Map<string, PageLinkMetrics>()\n for (const p of metas) {\n const key = normalize(p.url)\n const inbound = inboundFrom.get(key) ?? new Set()\n const ac = anchorCounts.get(key)\n const topAnchors = ac\n ? [...ac.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(e => e[0])\n : []\n const oc = outCounts.get(p.url) ?? { int: 0, ext: 0 }\n metrics.set(p.url, {\n url: p.url,\n inlinks: inbound.size,\n uniqueInlinks: inbound.size,\n outlinksInternal: oc.int,\n outlinksExternal: oc.ext,\n crawlDepth: depth.has(key) ? depth.get(key)! : null,\n orphan: inbound.size === 0 && key !== start,\n topAnchors,\n })\n }\n\n const issues = computeIssues(metas, metrics, brokenLinkPages)\n let reportMd = renderIssueReport(job.startUrl, metas, issues, metrics)\n if (extras.imageAudit) reportMd += `\\n\\n${renderImageSection(extras.imageAudit)}`\n\n const metricValues = [...metrics.values()]\n const round = (n: number) => Math.round(n * 10) / 10\n const distribution = { zero: 0, oneToTwo: 0, threeToTen: 0, elevenPlus: 0 }\n let sumInlinks = 0\n let sumOutlinks = 0\n for (const m of metricValues) {\n sumInlinks += m.inlinks\n sumOutlinks += m.outlinksInternal + m.outlinksExternal\n if (m.inlinks === 0) distribution.zero++\n else if (m.inlinks <= 2) distribution.oneToTwo++\n else if (m.inlinks <= 10) distribution.threeToTen++\n else distribution.elevenPlus++\n }\n const externalDomains: ExtDomainRow[] = [...domMap.entries()]\n .map(([domain, d]) => ({ domain, links: d.links, nofollow: d.nofollow, pages: d.pages.size }))\n .sort((a, b) => b.links - a.links)\n const linkReport: LinkReport = {\n summary: {\n internal: {\n totalLinks: internalTotal,\n pages: metas.length,\n orphans: metricValues.filter(m => m.orphan).length,\n brokenInternal,\n avgInlinks: metas.length ? round(sumInlinks / metas.length) : 0,\n avgOutlinks: metas.length ? round(sumOutlinks / metas.length) : 0,\n distribution,\n topByInlinks: [...metricValues].sort((a, b) => b.inlinks - a.inlinks).slice(0, 20)\n .map(m => ({ url: m.url, inlinks: m.inlinks, outlinksInternal: m.outlinksInternal, outlinksExternal: m.outlinksExternal })),\n },\n external: {\n totalLinks: externalTotal,\n uniqueDomains: externalDomains.length,\n topDomains: externalDomains.slice(0, 20),\n },\n },\n externalDomains,\n }\n\n const pagesOut = createWriteStream(join(dir, 'pages.jsonl'))\n for (const p of metas) {\n const m = metrics.get(p.url)\n const row = { ...(p as unknown as Record<string, unknown>), inlinks: m?.inlinks ?? 0, crawlDepth: m?.crawlDepth ?? null, orphan: m?.orphan ?? false }\n if (!pagesOut.write(JSON.stringify(row) + '\\n')) await once(pagesOut, 'drain')\n }\n pagesOut.end()\n await once(pagesOut, 'finish')\n\n writeFileSync(join(dir, 'report.md'), reportMd)\n writeFileSync(join(dir, 'issues.json'), JSON.stringify(issues, null, 2))\n writeFileSync(join(dir, 'link-metrics.jsonl'), metricValues.map(m => JSON.stringify(m)).join('\\n'))\n writeFileSync(join(dir, 'link-report.md'), renderLinkReport(linkReport))\n writeFileSync(join(dir, 'links-summary.json'), JSON.stringify(linkReport.summary, null, 2))\n writeFileSync(join(dir, 'external-domains.json'), JSON.stringify(linkReport.externalDomains, null, 2))\n if (extras.imageAudit) {\n writeFileSync(join(dir, 'images.jsonl'), extras.imageAudit.rows.map(r => JSON.stringify(r)).join('\\n'))\n writeFileSync(join(dir, 'images-summary.json'), JSON.stringify(extras.imageAudit.summary, null, 2))\n }\n if (extras.seoAudit) writeFileSync(join(dir, 'seo-audit.json'), JSON.stringify(extras.seoAudit, null, 2))\n if (extras.branding) writeFileSync(join(dir, 'branding.json'), JSON.stringify(extras.branding, null, 2))\n\n const artifactFiles: Array<{ name: string; type: string }> = [\n { name: 'report.md', type: 'text/markdown' },\n { name: 'issues.json', type: 'application/json' },\n { name: 'pages.jsonl', type: 'application/x-ndjson' },\n { name: 'links.jsonl.gz', type: 'application/gzip' },\n { name: 'link-metrics.jsonl', type: 'application/x-ndjson' },\n { name: 'link-report.md', type: 'text/markdown' },\n { name: 'links-summary.json', type: 'application/json' },\n { name: 'external-domains.json', type: 'application/json' },\n ]\n if (extras.imageAudit) {\n artifactFiles.push({ name: 'images.jsonl', type: 'application/x-ndjson' })\n artifactFiles.push({ name: 'images-summary.json', type: 'application/json' })\n }\n if (extras.seoAudit) artifactFiles.push({ name: 'seo-audit.json', type: 'application/json' })\n if (extras.branding) artifactFiles.push({ name: 'branding.json', type: 'application/json' })\n\n const zip = new ZipFile()\n const zipOut = createWriteStream(join(dir, 'bundle.zip'))\n zip.outputStream.pipe(zipOut)\n for (const f of artifactFiles) zip.addFile(join(dir, f.name), f.name)\n if (pagesWithBody > 0) {\n const { readdirSync } = await import('node:fs')\n for (const f of readdirSync(join(dir, 'pages'))) zip.addFile(join(dir, 'pages', f), `pages/${f}`)\n }\n zip.end()\n await once(zipOut, 'finish')\n\n const store = getBlobStore()\n const stored: StoredArtifact[] = []\n for (const f of [...artifactFiles, { name: 'bundle.zip', type: 'application/zip' }]) {\n stored.push(await store.put(`extract/${job.id}/${f.name}`, readFileSync(join(dir, f.name)), f.type))\n }\n return stored\n } finally {\n rmSync(dir, { recursive: true, force: true })\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -1688,13 +1688,41 @@ var PAAExtractor = class {
1688
1688
  }
1689
1689
  return parts.join(" ").replace(/\s+/g, " ").trim();
1690
1690
  }
1691
- return Array.from(document.querySelectorAll(selectors.item)).map((pair) => ({
1692
- question: pair.getAttribute(selectors.itemDataQ) || pair.getAttribute(selectors.itemDataInitQ) || pair.querySelector(selectors.itemQuestionEl)?.innerText?.trim() || "",
1693
- answer: cleanText(pair.querySelector(selectors.answerContainer)) || void 0,
1694
- sourceTitle: pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0,
1695
- sourceSite: pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0,
1696
- sourceCite: pair.querySelector(selectors.sourceCite)?.innerText?.trim() || void 0
1697
- }));
1691
+ return Array.from(document.querySelectorAll(selectors.item)).map((pair) => {
1692
+ let sourceTitle = pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0;
1693
+ let sourceSite = pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0;
1694
+ let sourceCite = pair.querySelector(selectors.sourceCite)?.innerText?.trim() || void 0;
1695
+ if (!sourceCite) {
1696
+ const seenHrefs = /* @__PURE__ */ new Set();
1697
+ const anchors = [];
1698
+ for (const a of Array.from(pair.querySelectorAll("a[href]"))) {
1699
+ const href = a.href;
1700
+ if (!/^https?:\/\//.test(href) || seenHrefs.has(href)) continue;
1701
+ let host = "";
1702
+ try {
1703
+ host = new URL(href).hostname;
1704
+ } catch {
1705
+ continue;
1706
+ }
1707
+ if (/(^|\.)google(usercontent)?\.[a-z.]+$/.test(host)) continue;
1708
+ seenHrefs.add(href);
1709
+ anchors.push({ text: (a.textContent || "").trim(), href, host });
1710
+ }
1711
+ const primary = anchors.find((x) => x.text.length > 2) ?? anchors[0];
1712
+ if (primary) {
1713
+ sourceTitle = sourceTitle || primary.text || void 0;
1714
+ sourceSite = sourceSite || primary.host.replace(/^www\./, "");
1715
+ sourceCite = primary.href;
1716
+ }
1717
+ }
1718
+ return {
1719
+ question: pair.getAttribute(selectors.itemDataQ) || pair.getAttribute(selectors.itemDataInitQ) || pair.querySelector(selectors.itemQuestionEl)?.innerText?.trim() || "",
1720
+ answer: cleanText(pair.querySelector(selectors.answerContainer)) || void 0,
1721
+ sourceTitle,
1722
+ sourceSite,
1723
+ sourceCite
1724
+ };
1725
+ });
1698
1726
  }, sels);
1699
1727
  return raw.flatMap((item) => {
1700
1728
  const cleaned = {
@@ -1736,54 +1764,115 @@ var PAAExtractor = class {
1736
1764
  const seenQs = /* @__PURE__ */ new Set();
1737
1765
  const orderedQs = [];
1738
1766
  const results = [];
1739
- const readAllQs = () => page.evaluate(
1740
- ({ sel, dataQ, dataInitQ, questionEl }) => Array.from(document.querySelectorAll(sel)).map(
1741
- (el) => el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || ""
1742
- ).filter(Boolean),
1743
- { sel: PAASelectors.item, dataQ: PAASelectors.itemDataQ, dataInitQ: PAASelectors.itemDataInitQ, questionEl: PAASelectors.itemQuestionEl }
1767
+ const abandonedNeverReclick = /* @__PURE__ */ new Set();
1768
+ const clickedOnceEver = /* @__PURE__ */ new Set();
1769
+ const itemArgs = {
1770
+ sel: PAASelectors.item,
1771
+ dataQ: PAASelectors.itemDataQ,
1772
+ dataInitQ: PAASelectors.itemDataInitQ,
1773
+ questionEl: PAASelectors.itemQuestionEl,
1774
+ answerSel: PAASelectors.answerContainer,
1775
+ expandedClass: PAASelectors.expandedClass,
1776
+ clickTarget: PAASelectors.clickTarget
1777
+ };
1778
+ const readItemStates = () => page.evaluate(
1779
+ ({ sel, dataQ, dataInitQ, questionEl, expandedClass, answerSel }) => {
1780
+ const answerText = (root) => {
1781
+ if (!root) return "";
1782
+ let out = "";
1783
+ const walk = (n) => {
1784
+ for (const c of Array.from(n.childNodes)) {
1785
+ if (c.nodeType === Node.TEXT_NODE) out += c.textContent || "";
1786
+ else if (c.nodeType === Node.ELEMENT_NODE && c.tagName !== "STYLE" && c.tagName !== "SCRIPT") walk(c);
1787
+ }
1788
+ };
1789
+ walk(root);
1790
+ return out.replace(/\s+/g, " ").trim();
1791
+ };
1792
+ return Array.from(document.querySelectorAll(sel)).map((el) => ({
1793
+ q: el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || "",
1794
+ done: el.classList.contains(expandedClass) || answerText(el.querySelector(answerSel)).length > 30
1795
+ })).filter((s) => s.q);
1796
+ },
1797
+ itemArgs
1744
1798
  );
1799
+ const expandOneItemSerially = async (q) => {
1800
+ const clicked = await page.evaluate(
1801
+ ({ sel, dataQ, dataInitQ, clickTarget, q: target }) => {
1802
+ const item = Array.from(document.querySelectorAll(sel)).find((el) => (el.getAttribute(dataQ) || el.getAttribute(dataInitQ)) === target);
1803
+ const btn = item?.querySelector(clickTarget);
1804
+ if (!btn) return false;
1805
+ btn.scrollIntoView({ block: "center" });
1806
+ btn.click();
1807
+ return true;
1808
+ },
1809
+ { ...itemArgs, q }
1810
+ ).catch(() => false);
1811
+ if (!clicked) return "failed";
1812
+ try {
1813
+ const outcome = await page.waitForFunction(
1814
+ ({ sel, dataQ, dataInitQ, answerSel, q: target }) => {
1815
+ const answerText = (root) => {
1816
+ if (!root) return "";
1817
+ let out = "";
1818
+ const walk = (n) => {
1819
+ for (const c of Array.from(n.childNodes)) {
1820
+ if (c.nodeType === Node.TEXT_NODE) out += c.textContent || "";
1821
+ else if (c.nodeType === Node.ELEMENT_NODE && c.tagName !== "STYLE" && c.tagName !== "SCRIPT") walk(c);
1822
+ }
1823
+ };
1824
+ walk(root);
1825
+ return out.replace(/\s+/g, " ").trim();
1826
+ };
1827
+ const item = Array.from(document.querySelectorAll(sel)).find((el) => (el.getAttribute(dataQ) || el.getAttribute(dataInitQ)) === target);
1828
+ if (!item) return "gone";
1829
+ const text = answerText(item.querySelector(answerSel));
1830
+ if (/an error has occurred/i.test(text.slice(0, 80))) return "error";
1831
+ if (text.length > 30) return "ok";
1832
+ return false;
1833
+ },
1834
+ { ...itemArgs, q },
1835
+ { timeout: 6500, polling: 200 }
1836
+ );
1837
+ return await outcome.jsonValue() === "ok" ? "ok" : "failed";
1838
+ } catch {
1839
+ return "failed";
1840
+ }
1841
+ };
1745
1842
  let round = 0;
1746
- while (seenQs.size < options.maxQuestions) {
1843
+ let growthWaits = 0;
1844
+ while (true) {
1747
1845
  if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
1748
1846
  this.throwIfAborted(signal);
1749
- await this.throwIfCaptcha(page, "Google PAA expansion");
1750
- const beforeQs = await readAllQs();
1751
- if (beforeQs.length >= options.maxQuestions) break;
1752
- const unexpandedSel = `${PAASelectors.item}:not(.${PAASelectors.expandedClass}) ${PAASelectors.clickTarget}`;
1753
- const unexpandedCount = await page.locator(unexpandedSel).count();
1754
- if (unexpandedCount === 0) break;
1755
- this.reporter.onDepth(++round);
1756
- for (let ci = 0; ci < unexpandedCount; ci++) {
1757
- if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
1758
- this.throwIfAborted(signal);
1759
- try {
1760
- const btn = page.locator(unexpandedSel).first();
1761
- await btn.scrollIntoViewIfNeeded();
1762
- await btn.hover({ force: true });
1763
- await page.waitForTimeout(100);
1764
- await btn.click({ force: true });
1765
- await page.waitForTimeout(500);
1766
- } catch {
1847
+ const states = await readItemStates();
1848
+ for (const s of states) {
1849
+ if (!seenQs.has(s.q)) {
1850
+ seenQs.add(s.q);
1851
+ orderedQs.push(s.q);
1767
1852
  }
1768
1853
  }
1769
- await page.waitForFunction(
1770
- ({ sel, min }) => document.querySelectorAll(sel).length > min,
1771
- { sel: PAASelectors.item, min: beforeQs.length },
1772
- { timeout: 5e3 }
1773
- ).catch(() => {
1774
- });
1775
- await this.throwIfCaptcha(page, "Google PAA expansion");
1776
- const afterQs = await readAllQs();
1777
- if (afterQs.length === beforeQs.length) break;
1778
- for (const q of afterQs) {
1779
- if (!seenQs.has(q)) {
1780
- seenQs.add(q);
1781
- orderedQs.push(q);
1782
- }
1854
+ const kept = new Set(orderedQs.slice(0, options.maxQuestions));
1855
+ const clickable = states.filter((s) => kept.has(s.q) && !clickedOnceEver.has(s.q) && !abandonedNeverReclick.has(s.q));
1856
+ const target = clickable.find((s) => !s.done) ?? (seenQs.size < options.maxQuestions ? clickable[0] : void 0);
1857
+ if (!target) {
1858
+ if (seenQs.size >= options.maxQuestions) break;
1859
+ if (++growthWaits > 1) break;
1860
+ const grew = await page.waitForFunction(
1861
+ ({ sel, min }) => document.querySelectorAll(sel).length > min,
1862
+ { sel: PAASelectors.item, min: states.length },
1863
+ { timeout: 3e3 }
1864
+ ).catch(() => null);
1865
+ if (!grew) break;
1866
+ continue;
1783
1867
  }
1868
+ growthWaits = 0;
1869
+ this.reporter.onDepth(++round);
1870
+ await this.throwIfCaptcha(page, "Google PAA expansion");
1871
+ clickedOnceEver.add(target.q);
1872
+ if (await expandOneItemSerially(target.q) === "failed") abandonedNeverReclick.add(target.q);
1784
1873
  }
1785
1874
  const itemMap = new Map((await this.extractVisibleItems(page)).map((i) => [i.question, i]));
1786
- await this.fillIncompleteItems(page, orderedQs, itemMap, options);
1875
+ await this.fillIncompleteItems(page, orderedQs, itemMap, options, abandonedNeverReclick);
1787
1876
  for (const q of orderedQs) {
1788
1877
  if (results.length >= options.maxQuestions) break;
1789
1878
  const key = this.normalizeQuestion(q);
@@ -1801,9 +1890,11 @@ var PAAExtractor = class {
1801
1890
  this.completeness.paaWithoutSource = results.filter((r) => !r.source_title && !r.source_site && !r.source_cite).length;
1802
1891
  return results;
1803
1892
  }
1804
- async fillIncompleteItems(page, orderedQs, itemMap, options) {
1893
+ async fillIncompleteItems(page, orderedQs, itemMap, options, abandonedNeverReclick) {
1805
1894
  const kept = new Set(orderedQs.slice(0, options.maxQuestions));
1806
- const missingBefore = new Set([...kept].filter((q) => !itemMap.get(q)?.answer));
1895
+ const missingBefore = new Set(
1896
+ [...kept].filter((q) => !itemMap.get(q)?.answer && !abandonedNeverReclick?.has(q))
1897
+ );
1807
1898
  if (missingBefore.size === 0) return;
1808
1899
  for (let round = 0; round < 2; round++) {
1809
1900
  if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
@@ -2195,40 +2286,41 @@ var PAAExtractor = class {
2195
2286
  };
2196
2287
  }
2197
2288
  async captureAIOverviewShareUrl(page, options) {
2198
- for (let attempt = 0; attempt < 3; attempt++) {
2199
- if (options?.softDeadlineMs && Date.now() >= options.softDeadlineMs) return null;
2200
- const clicked = await page.evaluate(() => {
2201
- const heads = Array.from(document.querySelectorAll('h1,h2,h3,[role="heading"],div')).filter((h) => (h.textContent || "").trim() === "AI Overview");
2202
- for (const head of heads) {
2203
- let sec = head;
2204
- for (let up = 0; up < 8 && sec; up++) {
2205
- const btn = sec.querySelector('[aria-label="Share"]');
2206
- if (btn) {
2207
- btn.scrollIntoView({ block: "center" });
2208
- btn.click();
2209
- return true;
2210
- }
2211
- sec = sec.parentElement;
2289
+ if (options?.softDeadlineMs && Date.now() >= options.softDeadlineMs) return null;
2290
+ const clicked = await page.evaluate(() => {
2291
+ const heads = Array.from(document.querySelectorAll('h1,h2,h3,[role="heading"],div')).filter((h) => (h.textContent || "").trim() === "AI Overview");
2292
+ for (const head of heads) {
2293
+ let sec = head;
2294
+ for (let up = 0; up < 8 && sec; up++) {
2295
+ const btn = sec.querySelector('[aria-label="Share"]');
2296
+ if (btn) {
2297
+ btn.scrollIntoView({ block: "center" });
2298
+ btn.click();
2299
+ return true;
2212
2300
  }
2301
+ sec = sec.parentElement;
2213
2302
  }
2214
- return false;
2215
- }).catch(() => false);
2216
- if (!clicked) return null;
2217
- await page.waitForTimeout(4500);
2218
- const url = await page.evaluate(() => {
2303
+ }
2304
+ return false;
2305
+ }).catch(() => false);
2306
+ if (!clicked) return null;
2307
+ let url = null;
2308
+ try {
2309
+ const found = await page.waitForFunction(() => {
2219
2310
  const w = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
2220
2311
  while (w.nextNode()) {
2221
2312
  const m = (w.currentNode.textContent || "").match(/https:\/\/share\.google\/aimode\/\S+/);
2222
2313
  if (m) return m[0];
2223
2314
  }
2224
- return null;
2225
- }).catch(() => null);
2226
- await page.keyboard.press("Escape").catch(() => {
2227
- });
2228
- if (url) return url;
2229
- await page.waitForTimeout(600);
2315
+ return false;
2316
+ }, void 0, { timeout: 4500, polling: 250 });
2317
+ url = await found.jsonValue();
2318
+ } catch {
2319
+ url = null;
2230
2320
  }
2231
- return null;
2321
+ await page.keyboard.press("Escape").catch(() => {
2322
+ });
2323
+ return url;
2232
2324
  }
2233
2325
  buildTree(flat, _seed) {
2234
2326
  const roots = [];