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
@@ -1516,13 +1516,41 @@ var PAAExtractor = class {
1516
1516
  }
1517
1517
  return parts.join(" ").replace(/\s+/g, " ").trim();
1518
1518
  }
1519
- return Array.from(document.querySelectorAll(selectors.item)).map((pair) => ({
1520
- question: pair.getAttribute(selectors.itemDataQ) || pair.getAttribute(selectors.itemDataInitQ) || pair.querySelector(selectors.itemQuestionEl)?.innerText?.trim() || "",
1521
- answer: cleanText(pair.querySelector(selectors.answerContainer)) || void 0,
1522
- sourceTitle: pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0,
1523
- sourceSite: pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0,
1524
- sourceCite: pair.querySelector(selectors.sourceCite)?.innerText?.trim() || void 0
1525
- }));
1519
+ return Array.from(document.querySelectorAll(selectors.item)).map((pair) => {
1520
+ let sourceTitle = pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0;
1521
+ let sourceSite = pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0;
1522
+ let sourceCite = pair.querySelector(selectors.sourceCite)?.innerText?.trim() || void 0;
1523
+ if (!sourceCite) {
1524
+ const seenHrefs = /* @__PURE__ */ new Set();
1525
+ const anchors = [];
1526
+ for (const a of Array.from(pair.querySelectorAll("a[href]"))) {
1527
+ const href = a.href;
1528
+ if (!/^https?:\/\//.test(href) || seenHrefs.has(href)) continue;
1529
+ let host = "";
1530
+ try {
1531
+ host = new URL(href).hostname;
1532
+ } catch {
1533
+ continue;
1534
+ }
1535
+ if (/(^|\.)google(usercontent)?\.[a-z.]+$/.test(host)) continue;
1536
+ seenHrefs.add(href);
1537
+ anchors.push({ text: (a.textContent || "").trim(), href, host });
1538
+ }
1539
+ const primary = anchors.find((x) => x.text.length > 2) ?? anchors[0];
1540
+ if (primary) {
1541
+ sourceTitle = sourceTitle || primary.text || void 0;
1542
+ sourceSite = sourceSite || primary.host.replace(/^www\./, "");
1543
+ sourceCite = primary.href;
1544
+ }
1545
+ }
1546
+ return {
1547
+ question: pair.getAttribute(selectors.itemDataQ) || pair.getAttribute(selectors.itemDataInitQ) || pair.querySelector(selectors.itemQuestionEl)?.innerText?.trim() || "",
1548
+ answer: cleanText(pair.querySelector(selectors.answerContainer)) || void 0,
1549
+ sourceTitle,
1550
+ sourceSite,
1551
+ sourceCite
1552
+ };
1553
+ });
1526
1554
  }, sels);
1527
1555
  return raw.flatMap((item) => {
1528
1556
  const cleaned = {
@@ -1564,54 +1592,115 @@ var PAAExtractor = class {
1564
1592
  const seenQs = /* @__PURE__ */ new Set();
1565
1593
  const orderedQs = [];
1566
1594
  const results = [];
1567
- const readAllQs = () => page.evaluate(
1568
- ({ sel, dataQ, dataInitQ, questionEl }) => Array.from(document.querySelectorAll(sel)).map(
1569
- (el) => el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || ""
1570
- ).filter(Boolean),
1571
- { sel: PAASelectors.item, dataQ: PAASelectors.itemDataQ, dataInitQ: PAASelectors.itemDataInitQ, questionEl: PAASelectors.itemQuestionEl }
1595
+ const abandonedNeverReclick = /* @__PURE__ */ new Set();
1596
+ const clickedOnceEver = /* @__PURE__ */ new Set();
1597
+ const itemArgs = {
1598
+ sel: PAASelectors.item,
1599
+ dataQ: PAASelectors.itemDataQ,
1600
+ dataInitQ: PAASelectors.itemDataInitQ,
1601
+ questionEl: PAASelectors.itemQuestionEl,
1602
+ answerSel: PAASelectors.answerContainer,
1603
+ expandedClass: PAASelectors.expandedClass,
1604
+ clickTarget: PAASelectors.clickTarget
1605
+ };
1606
+ const readItemStates = () => page.evaluate(
1607
+ ({ sel, dataQ, dataInitQ, questionEl, expandedClass, answerSel }) => {
1608
+ const answerText = (root) => {
1609
+ if (!root) return "";
1610
+ let out = "";
1611
+ const walk = (n) => {
1612
+ for (const c of Array.from(n.childNodes)) {
1613
+ if (c.nodeType === Node.TEXT_NODE) out += c.textContent || "";
1614
+ else if (c.nodeType === Node.ELEMENT_NODE && c.tagName !== "STYLE" && c.tagName !== "SCRIPT") walk(c);
1615
+ }
1616
+ };
1617
+ walk(root);
1618
+ return out.replace(/\s+/g, " ").trim();
1619
+ };
1620
+ return Array.from(document.querySelectorAll(sel)).map((el) => ({
1621
+ q: el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || "",
1622
+ done: el.classList.contains(expandedClass) || answerText(el.querySelector(answerSel)).length > 30
1623
+ })).filter((s) => s.q);
1624
+ },
1625
+ itemArgs
1572
1626
  );
1627
+ const expandOneItemSerially = async (q) => {
1628
+ const clicked = await page.evaluate(
1629
+ ({ sel, dataQ, dataInitQ, clickTarget, q: target }) => {
1630
+ const item = Array.from(document.querySelectorAll(sel)).find((el) => (el.getAttribute(dataQ) || el.getAttribute(dataInitQ)) === target);
1631
+ const btn = item?.querySelector(clickTarget);
1632
+ if (!btn) return false;
1633
+ btn.scrollIntoView({ block: "center" });
1634
+ btn.click();
1635
+ return true;
1636
+ },
1637
+ { ...itemArgs, q }
1638
+ ).catch(() => false);
1639
+ if (!clicked) return "failed";
1640
+ try {
1641
+ const outcome = await page.waitForFunction(
1642
+ ({ sel, dataQ, dataInitQ, answerSel, q: target }) => {
1643
+ const answerText = (root) => {
1644
+ if (!root) return "";
1645
+ let out = "";
1646
+ const walk = (n) => {
1647
+ for (const c of Array.from(n.childNodes)) {
1648
+ if (c.nodeType === Node.TEXT_NODE) out += c.textContent || "";
1649
+ else if (c.nodeType === Node.ELEMENT_NODE && c.tagName !== "STYLE" && c.tagName !== "SCRIPT") walk(c);
1650
+ }
1651
+ };
1652
+ walk(root);
1653
+ return out.replace(/\s+/g, " ").trim();
1654
+ };
1655
+ const item = Array.from(document.querySelectorAll(sel)).find((el) => (el.getAttribute(dataQ) || el.getAttribute(dataInitQ)) === target);
1656
+ if (!item) return "gone";
1657
+ const text = answerText(item.querySelector(answerSel));
1658
+ if (/an error has occurred/i.test(text.slice(0, 80))) return "error";
1659
+ if (text.length > 30) return "ok";
1660
+ return false;
1661
+ },
1662
+ { ...itemArgs, q },
1663
+ { timeout: 6500, polling: 200 }
1664
+ );
1665
+ return await outcome.jsonValue() === "ok" ? "ok" : "failed";
1666
+ } catch {
1667
+ return "failed";
1668
+ }
1669
+ };
1573
1670
  let round = 0;
1574
- while (seenQs.size < options.maxQuestions) {
1671
+ let growthWaits = 0;
1672
+ while (true) {
1575
1673
  if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
1576
1674
  this.throwIfAborted(signal);
1577
- await this.throwIfCaptcha(page, "Google PAA expansion");
1578
- const beforeQs = await readAllQs();
1579
- if (beforeQs.length >= options.maxQuestions) break;
1580
- const unexpandedSel = `${PAASelectors.item}:not(.${PAASelectors.expandedClass}) ${PAASelectors.clickTarget}`;
1581
- const unexpandedCount = await page.locator(unexpandedSel).count();
1582
- if (unexpandedCount === 0) break;
1583
- this.reporter.onDepth(++round);
1584
- for (let ci = 0; ci < unexpandedCount; ci++) {
1585
- if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
1586
- this.throwIfAborted(signal);
1587
- try {
1588
- const btn = page.locator(unexpandedSel).first();
1589
- await btn.scrollIntoViewIfNeeded();
1590
- await btn.hover({ force: true });
1591
- await page.waitForTimeout(100);
1592
- await btn.click({ force: true });
1593
- await page.waitForTimeout(500);
1594
- } catch {
1675
+ const states = await readItemStates();
1676
+ for (const s of states) {
1677
+ if (!seenQs.has(s.q)) {
1678
+ seenQs.add(s.q);
1679
+ orderedQs.push(s.q);
1595
1680
  }
1596
1681
  }
1597
- await page.waitForFunction(
1598
- ({ sel, min }) => document.querySelectorAll(sel).length > min,
1599
- { sel: PAASelectors.item, min: beforeQs.length },
1600
- { timeout: 5e3 }
1601
- ).catch(() => {
1602
- });
1603
- await this.throwIfCaptcha(page, "Google PAA expansion");
1604
- const afterQs = await readAllQs();
1605
- if (afterQs.length === beforeQs.length) break;
1606
- for (const q of afterQs) {
1607
- if (!seenQs.has(q)) {
1608
- seenQs.add(q);
1609
- orderedQs.push(q);
1610
- }
1682
+ const kept = new Set(orderedQs.slice(0, options.maxQuestions));
1683
+ const clickable = states.filter((s) => kept.has(s.q) && !clickedOnceEver.has(s.q) && !abandonedNeverReclick.has(s.q));
1684
+ const target = clickable.find((s) => !s.done) ?? (seenQs.size < options.maxQuestions ? clickable[0] : void 0);
1685
+ if (!target) {
1686
+ if (seenQs.size >= options.maxQuestions) break;
1687
+ if (++growthWaits > 1) break;
1688
+ const grew = await page.waitForFunction(
1689
+ ({ sel, min }) => document.querySelectorAll(sel).length > min,
1690
+ { sel: PAASelectors.item, min: states.length },
1691
+ { timeout: 3e3 }
1692
+ ).catch(() => null);
1693
+ if (!grew) break;
1694
+ continue;
1611
1695
  }
1696
+ growthWaits = 0;
1697
+ this.reporter.onDepth(++round);
1698
+ await this.throwIfCaptcha(page, "Google PAA expansion");
1699
+ clickedOnceEver.add(target.q);
1700
+ if (await expandOneItemSerially(target.q) === "failed") abandonedNeverReclick.add(target.q);
1612
1701
  }
1613
1702
  const itemMap = new Map((await this.extractVisibleItems(page)).map((i) => [i.question, i]));
1614
- await this.fillIncompleteItems(page, orderedQs, itemMap, options);
1703
+ await this.fillIncompleteItems(page, orderedQs, itemMap, options, abandonedNeverReclick);
1615
1704
  for (const q of orderedQs) {
1616
1705
  if (results.length >= options.maxQuestions) break;
1617
1706
  const key = this.normalizeQuestion(q);
@@ -1629,9 +1718,11 @@ var PAAExtractor = class {
1629
1718
  this.completeness.paaWithoutSource = results.filter((r) => !r.source_title && !r.source_site && !r.source_cite).length;
1630
1719
  return results;
1631
1720
  }
1632
- async fillIncompleteItems(page, orderedQs, itemMap, options) {
1721
+ async fillIncompleteItems(page, orderedQs, itemMap, options, abandonedNeverReclick) {
1633
1722
  const kept = new Set(orderedQs.slice(0, options.maxQuestions));
1634
- const missingBefore = new Set([...kept].filter((q) => !itemMap.get(q)?.answer));
1723
+ const missingBefore = new Set(
1724
+ [...kept].filter((q) => !itemMap.get(q)?.answer && !abandonedNeverReclick?.has(q))
1725
+ );
1635
1726
  if (missingBefore.size === 0) return;
1636
1727
  for (let round = 0; round < 2; round++) {
1637
1728
  if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
@@ -2023,40 +2114,41 @@ var PAAExtractor = class {
2023
2114
  };
2024
2115
  }
2025
2116
  async captureAIOverviewShareUrl(page, options) {
2026
- for (let attempt = 0; attempt < 3; attempt++) {
2027
- if (options?.softDeadlineMs && Date.now() >= options.softDeadlineMs) return null;
2028
- const clicked = await page.evaluate(() => {
2029
- const heads = Array.from(document.querySelectorAll('h1,h2,h3,[role="heading"],div')).filter((h) => (h.textContent || "").trim() === "AI Overview");
2030
- for (const head of heads) {
2031
- let sec = head;
2032
- for (let up = 0; up < 8 && sec; up++) {
2033
- const btn = sec.querySelector('[aria-label="Share"]');
2034
- if (btn) {
2035
- btn.scrollIntoView({ block: "center" });
2036
- btn.click();
2037
- return true;
2038
- }
2039
- sec = sec.parentElement;
2117
+ if (options?.softDeadlineMs && Date.now() >= options.softDeadlineMs) return null;
2118
+ const clicked = await page.evaluate(() => {
2119
+ const heads = Array.from(document.querySelectorAll('h1,h2,h3,[role="heading"],div')).filter((h) => (h.textContent || "").trim() === "AI Overview");
2120
+ for (const head of heads) {
2121
+ let sec = head;
2122
+ for (let up = 0; up < 8 && sec; up++) {
2123
+ const btn = sec.querySelector('[aria-label="Share"]');
2124
+ if (btn) {
2125
+ btn.scrollIntoView({ block: "center" });
2126
+ btn.click();
2127
+ return true;
2040
2128
  }
2129
+ sec = sec.parentElement;
2041
2130
  }
2042
- return false;
2043
- }).catch(() => false);
2044
- if (!clicked) return null;
2045
- await page.waitForTimeout(4500);
2046
- const url = await page.evaluate(() => {
2131
+ }
2132
+ return false;
2133
+ }).catch(() => false);
2134
+ if (!clicked) return null;
2135
+ let url = null;
2136
+ try {
2137
+ const found = await page.waitForFunction(() => {
2047
2138
  const w = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
2048
2139
  while (w.nextNode()) {
2049
2140
  const m = (w.currentNode.textContent || "").match(/https:\/\/share\.google\/aimode\/\S+/);
2050
2141
  if (m) return m[0];
2051
2142
  }
2052
- return null;
2053
- }).catch(() => null);
2054
- await page.keyboard.press("Escape").catch(() => {
2055
- });
2056
- if (url) return url;
2057
- await page.waitForTimeout(600);
2143
+ return false;
2144
+ }, void 0, { timeout: 4500, polling: 250 });
2145
+ url = await found.jsonValue();
2146
+ } catch {
2147
+ url = null;
2058
2148
  }
2059
- return null;
2149
+ await page.keyboard.press("Escape").catch(() => {
2150
+ });
2151
+ return url;
2060
2152
  }
2061
2153
  buildTree(flat, _seed) {
2062
2154
  const roots = [];
@@ -3277,4 +3369,4 @@ export {
3277
3369
  resolveKernelProxyId,
3278
3370
  harvest
3279
3371
  };
3280
- //# sourceMappingURL=chunk-AUCXKRRH.js.map
3372
+ //# sourceMappingURL=chunk-RMPPYKUV.js.map