mcp-scraper 0.2.24 → 0.3.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 (35) hide show
  1. package/dist/bin/api-server.cjs +916 -7
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +1 -1
  4. package/dist/bin/browser-agent-stdio-server.cjs +989 -28
  5. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  6. package/dist/bin/browser-agent-stdio-server.js +3 -2
  7. package/dist/bin/browser-agent-stdio-server.js.map +1 -1
  8. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  9. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-cli.js +1 -1
  11. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +1008 -47
  12. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-scraper-combined-stdio-server.js +4 -3
  14. package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
  15. package/dist/bin/mcp-scraper-install.cjs +1 -1
  16. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  17. package/dist/bin/mcp-scraper-install.js +1 -1
  18. package/dist/bin/mcp-stdio-server.cjs +1 -1
  19. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  20. package/dist/bin/mcp-stdio-server.js +2 -2
  21. package/dist/{chunk-4TNQUYP7.js → chunk-DH4H3F6S.js} +131 -3
  22. package/dist/chunk-DH4H3F6S.js.map +1 -0
  23. package/dist/chunk-NXRWFOEZ.js +816 -0
  24. package/dist/chunk-NXRWFOEZ.js.map +1 -0
  25. package/dist/chunk-PGWJ2EJ3.js +7 -0
  26. package/dist/chunk-PGWJ2EJ3.js.map +1 -0
  27. package/dist/{chunk-TTSW2MHR.js → chunk-SIXJ6EVS.js} +2 -2
  28. package/dist/{server-TZ5EMANS.js → server-MOP7NIUK.js} +40 -3
  29. package/dist/{server-TZ5EMANS.js.map → server-MOP7NIUK.js.map} +1 -1
  30. package/docs/mcp-tool-manifest.generated.json +37 -6
  31. package/package.json +1 -1
  32. package/dist/chunk-4TNQUYP7.js.map +0 -1
  33. package/dist/chunk-ISIITOAL.js +0 -7
  34. package/dist/chunk-ISIITOAL.js.map +0 -1
  35. /package/dist/{chunk-TTSW2MHR.js.map → chunk-SIXJ6EVS.js.map} +0 -0
@@ -1,10 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
3
25
 
4
26
  // bin/mcp-scraper-combined-stdio-server.ts
5
- var import_node_fs6 = require("fs");
6
- var import_node_os6 = require("os");
7
- var import_node_path8 = require("path");
27
+ var import_node_fs7 = require("fs");
28
+ var import_node_os7 = require("os");
29
+ var import_node_path9 = require("path");
8
30
  var import_mcp3 = require("@modelcontextprotocol/sdk/server/mcp.js");
9
31
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
10
32
 
@@ -246,9 +268,9 @@ var HttpMcpToolExecutor = class {
246
268
 
247
269
  // src/mcp/browser-agent-mcp-server.ts
248
270
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
249
- var import_node_fs3 = require("fs");
250
- var import_node_os4 = require("os");
251
- var import_node_path5 = require("path");
271
+ var import_node_fs4 = require("fs");
272
+ var import_node_os5 = require("os");
273
+ var import_node_path6 = require("path");
252
274
 
253
275
  // src/lib/chrome-profiles.ts
254
276
  var import_promises = require("fs/promises");
@@ -470,18 +492,834 @@ async function syncImportedChromeProfile(name, options = {}) {
470
492
 
471
493
  // src/services/browser-agent/local-browser-agent-service.ts
472
494
  var import_node_crypto = require("crypto");
495
+ var import_node_fs3 = require("fs");
496
+ var import_node_path4 = require("path");
497
+ var import_playwright_extra = require("playwright-extra");
498
+ var import_puppeteer_extra_plugin_stealth = __toESM(require("puppeteer-extra-plugin-stealth"), 1);
499
+
500
+ // src/services/fanout/cdp-capture.ts
501
+ var ANSWER_RE = /\/backend-api\/(f\/)?conversation$|\/chat_conversations\/[^/]+\/(completion|retry_completion)$|\/completion$/i;
502
+ var FanoutCdpCapture = class {
503
+ client = null;
504
+ reqUrl = /* @__PURE__ */ new Map();
505
+ ctById = /* @__PURE__ */ new Map();
506
+ answerIds = /* @__PURE__ */ new Set();
507
+ bodies = [];
508
+ pending = [];
509
+ answerStreams = 0;
510
+ async attach(page) {
511
+ if (this.client) return;
512
+ const client = await page.context().newCDPSession(page);
513
+ this.client = client;
514
+ await client.send("Network.enable", { maxTotalBufferSize: 2e8, maxResourceBufferSize: 2e8 });
515
+ client.on("Network.requestWillBeSent", (e) => {
516
+ const id = e.requestId;
517
+ const req = e.request;
518
+ if (id) this.reqUrl.set(id, req?.url || "");
519
+ });
520
+ client.on("Network.responseReceived", (e) => {
521
+ const id = e.requestId;
522
+ const res = e.response;
523
+ const url = res?.url || this.reqUrl.get(id) || "";
524
+ const headers = res?.headers || {};
525
+ const ct = String(headers["content-type"] || headers["Content-Type"] || "");
526
+ if (id) this.ctById.set(id, ct);
527
+ if (id && (this.isAnswerUrl(url) || /event-stream/i.test(ct))) this.answerIds.add(id);
528
+ });
529
+ client.on("Network.loadingFinished", (e) => this.grab(e.requestId));
530
+ }
531
+ reset() {
532
+ this.bodies = [];
533
+ this.answerStreams = 0;
534
+ this.answerIds.clear();
535
+ this.pending = [];
536
+ }
537
+ answerStreamCount() {
538
+ return this.answerStreams;
539
+ }
540
+ async drain() {
541
+ await Promise.all(this.pending);
542
+ }
543
+ bestAnswerText() {
544
+ const eventStreams = this.bodies.filter((b) => /event-stream/i.test(b.ct));
545
+ const answerStreams = eventStreams.filter((b) => this.isAnswerUrl(b.url)).sort((a, b) => b.text.length - a.text.length);
546
+ if (answerStreams.length) return answerStreams.map((b) => b.text).join("\n");
547
+ const answerJson = this.bodies.filter((b) => this.isAnswerUrl(b.url)).sort((a, b) => b.text.length - a.text.length);
548
+ if (answerJson.length) return answerJson[0].text;
549
+ if (eventStreams.length) return eventStreams.sort((a, b) => b.text.length - a.text.length)[0].text;
550
+ return "";
551
+ }
552
+ async detach() {
553
+ try {
554
+ await this.client?.detach();
555
+ } catch {
556
+ this.client = null;
557
+ }
558
+ this.client = null;
559
+ }
560
+ isAnswerUrl(url) {
561
+ let path = "";
562
+ try {
563
+ path = new URL(url).pathname;
564
+ } catch {
565
+ path = String(url || "").split("?")[0];
566
+ }
567
+ return ANSWER_RE.test(path);
568
+ }
569
+ grab(requestId) {
570
+ if (!requestId || !this.client || !this.answerIds.has(requestId)) return;
571
+ const url = this.reqUrl.get(requestId) || "";
572
+ const ct = this.ctById.get(requestId) || "";
573
+ const client = this.client;
574
+ const p = client.send("Network.getResponseBody", { requestId }).then((r) => {
575
+ const text = r.base64Encoded ? Buffer.from(String(r.body), "base64").toString("utf8") : String(r.body || "");
576
+ if (text) {
577
+ this.bodies.push({ url, ct, text });
578
+ if (this.isAnswerUrl(url)) this.answerStreams++;
579
+ }
580
+ }).catch(() => void 0);
581
+ this.pending.push(p);
582
+ }
583
+ };
584
+
585
+ // src/services/fanout/parse-helpers.ts
586
+ var INTERNAL = /chatgpt\.com|openai\.com|oaiusercontent|oaistatic|claude\.ai|anthropic\.com/i;
587
+ var TRACK = /^(utm_|oai|fbclid|gclid|ref$|ref_|sa$|usg$|ved$|sca_esv$|sources$)/i;
588
+ function domainOf(u) {
589
+ try {
590
+ return new URL(u).hostname.replace(/^www\./, "");
591
+ } catch {
592
+ return "";
593
+ }
594
+ }
595
+ function normUrl(u) {
596
+ try {
597
+ const x = new URL(u);
598
+ x.hash = "";
599
+ for (const k of [...x.searchParams.keys()]) {
600
+ if (TRACK.test(k)) x.searchParams.delete(k);
601
+ }
602
+ return x.toString().replace(/\/$/, "").toLowerCase();
603
+ } catch {
604
+ return String(u || "").toLowerCase();
605
+ }
606
+ }
607
+ function isExternal(u) {
608
+ const h = domainOf(u);
609
+ return Boolean(h) && !INTERNAL.test(h);
610
+ }
611
+ function unesc(s) {
612
+ try {
613
+ return JSON.parse('"' + s + '"');
614
+ } catch {
615
+ return String(s).replace(/\\(.)/g, "$1");
616
+ }
617
+ }
618
+ function stripCitationMarkers(s) {
619
+ return String(s || "").replace(/[-]/g, "").replace(/cite(?:turn\d+\w+?\d+)+/g, "").replace(/\s+/g, " ").trim();
620
+ }
621
+ function sliceBalanced(text, openIdx) {
622
+ const open = text[openIdx];
623
+ const close = open === "[" ? "]" : "}";
624
+ let depth = 0;
625
+ let inStr = false;
626
+ let esc2 = false;
627
+ for (let i = openIdx; i < text.length; i++) {
628
+ const ch = text[i];
629
+ if (inStr) {
630
+ if (esc2) esc2 = false;
631
+ else if (ch === "\\") esc2 = true;
632
+ else if (ch === '"') inStr = false;
633
+ continue;
634
+ }
635
+ if (ch === '"') {
636
+ inStr = true;
637
+ continue;
638
+ }
639
+ if (ch === open) depth++;
640
+ else if (ch === close) {
641
+ depth--;
642
+ if (depth === 0) return text.slice(openIdx, i + 1);
643
+ }
644
+ }
645
+ return null;
646
+ }
647
+ function findJsonValues(text, field) {
648
+ const out = [];
649
+ const re = new RegExp('"' + field + '"\\s*:\\s*(\\[|\\{)', "g");
650
+ let m;
651
+ while (m = re.exec(text)) {
652
+ const openIdx = m.index + m[0].length - 1;
653
+ const slice = sliceBalanced(text, openIdx);
654
+ if (slice) {
655
+ try {
656
+ out.push(JSON.parse(slice));
657
+ } catch {
658
+ continue;
659
+ }
660
+ }
661
+ }
662
+ return out;
663
+ }
664
+ function stringsInArrayBody(arrBody) {
665
+ const out = [];
666
+ const re = /"((?:[^"\\]|\\.){2,300}?)"/g;
667
+ let m;
668
+ while (m = re.exec(arrBody)) out.push(unesc(m[1]));
669
+ return out;
670
+ }
671
+ function cleanQuery(q) {
672
+ const t = (q || "").trim();
673
+ if (t.length < 2 || t.length > 300) return null;
674
+ return t;
675
+ }
676
+
677
+ // src/services/fanout/adapters/chatgpt.ts
678
+ function roundOf(refId) {
679
+ const m = /^turn(\d+)/.exec(refId || "");
680
+ return m ? Number(m[1]) : null;
681
+ }
682
+ function normRef(r) {
683
+ if (typeof r === "string") {
684
+ const m = /turn(\d+)(search|news|ref|video|image|product)(\d+)/.exec(r);
685
+ return m ? `turn${m[1]}${m[2]}${m[3]}` : null;
686
+ }
687
+ if (r && typeof r === "object") {
688
+ const o = r;
689
+ if (o.turn_index != null) {
690
+ return `turn${o.turn_index}${o.ref_type || "search"}${o.ref_index != null ? o.ref_index : ""}`;
691
+ }
692
+ }
693
+ return null;
694
+ }
695
+ function refsInText(s) {
696
+ return (String(s || "").match(/turn\d+(?:search|news|ref|video|image|product)\d+/g) || []).map(normRef).filter((x) => Boolean(x));
697
+ }
698
+ function collectInto(arr, v) {
699
+ if (Array.isArray(v)) arr.push(...v);
700
+ else if (v && typeof v === "object") arr.push(v);
701
+ }
702
+ function reconstructFromSsePatches(raw) {
703
+ const contentReferences = [];
704
+ const searchResultGroups = [];
705
+ const citations = [];
706
+ const citedUrls = /* @__PURE__ */ new Set();
707
+ const state = { found: false };
708
+ const handleOp = (op) => {
709
+ if (!op || typeof op !== "object") return;
710
+ const o = op;
711
+ const v = o.v;
712
+ if (o.o === "patch" && Array.isArray(v)) {
713
+ state.found = true;
714
+ v.forEach(handleOp);
715
+ return;
716
+ }
717
+ const path = typeof o.p === "string" ? o.p : "";
718
+ if ((path === "" || path === "/message") && v && typeof v === "object") {
719
+ const root = v;
720
+ const msg = root.message || root;
721
+ const md = msg && msg.metadata;
722
+ if (md) {
723
+ collectInto(contentReferences, md.content_references);
724
+ collectInto(searchResultGroups, md.search_result_groups);
725
+ collectInto(citations, md.citations);
726
+ state.found = true;
727
+ }
728
+ }
729
+ if (!path) return;
730
+ if (/\/safe_urls$/.test(path) && Array.isArray(v)) {
731
+ for (const u of v) if (typeof u === "string") citedUrls.add(u);
732
+ state.found = true;
733
+ } else if (path.includes("content_references")) {
734
+ collectInto(contentReferences, v);
735
+ state.found = true;
736
+ } else if (path.includes("search_result_groups")) {
737
+ collectInto(searchResultGroups, v);
738
+ state.found = true;
739
+ } else if (path.includes("citations")) {
740
+ collectInto(citations, v);
741
+ state.found = true;
742
+ }
743
+ };
744
+ for (const line of raw.split("\n")) {
745
+ const s = (line.startsWith("data:") ? line.slice(5) : line).trim();
746
+ if (!s || s === "[DONE]" || s[0] !== "{" && s[0] !== "[") continue;
747
+ let parsed;
748
+ try {
749
+ parsed = JSON.parse(s);
750
+ } catch {
751
+ continue;
752
+ }
753
+ const ops = Array.isArray(parsed) ? parsed : [parsed];
754
+ ops.forEach(handleOp);
755
+ }
756
+ if (!state.found) return raw;
757
+ const citedRefs = [...citedUrls].map((url) => ({ type: "grouped_webpages", items: [{ url }] }));
758
+ const synthetic = JSON.stringify({
759
+ content_references: [...contentReferences, ...citedRefs],
760
+ search_result_groups: searchResultGroups,
761
+ citations
762
+ });
763
+ return raw + "\n" + synthetic;
764
+ }
765
+ var chatgptAdapter = {
766
+ platform: "ChatGPT",
767
+ matchesHost(hostname) {
768
+ return /(^|\.)chatgpt\.com$|(^|\.)openai\.com$/i.test(hostname);
769
+ },
770
+ parse(rawInput, prompt) {
771
+ const raw = reconstructFromSsePatches(rawInput);
772
+ const sources = /* @__PURE__ */ new Map();
773
+ const snippets = /* @__PURE__ */ new Map();
774
+ const queries = /* @__PURE__ */ new Set();
775
+ const unmapped = /* @__PURE__ */ new Set();
776
+ const meta = { model: "", finishType: "", title: "", rounds: 0 };
777
+ const ensure = (url) => {
778
+ if (!isExternal(url)) return null;
779
+ const k = normUrl(url);
780
+ let s = sources.get(k);
781
+ if (!s) {
782
+ s = { url, domain: domainOf(url), title: "", cited: false, timesCited: 0, firstOrder: Infinity, snippet: "", round: null };
783
+ sources.set(k, s);
784
+ }
785
+ return s;
786
+ };
787
+ const addSnippet = (url, title, text) => {
788
+ const clean = stripCitationMarkers(text);
789
+ if (!isExternal(url) || clean.length < 2) return;
790
+ snippets.set(normUrl(url), { url, domain: domainOf(url), title: title || domainOf(url), text: clean.slice(0, 400) });
791
+ };
792
+ for (const m of raw.matchAll(/"([a-z_]*(?:quer|search)[a-z_]*)"\s*:/gi)) unmapped.add(m[1]);
793
+ for (const m of raw.matchAll(/"search_model_queries"\s*:\s*\[([^\]]*)\]/g)) {
794
+ for (const q of stringsInArrayBody(m[1])) {
795
+ const c = cleanQuery(q);
796
+ if (c) queries.add(c);
797
+ }
798
+ }
799
+ for (const m of raw.matchAll(/"(?:search_queries|queries|search_query)"\s*:\s*\[([^\]]*)\]/g)) {
800
+ for (const q of stringsInArrayBody(m[1])) {
801
+ const c = cleanQuery(q);
802
+ if (c) queries.add(c);
803
+ }
804
+ }
805
+ for (const v of findJsonValues(raw, "search_result_groups")) {
806
+ if (!Array.isArray(v)) continue;
807
+ for (const g of v) {
808
+ const entries = g && typeof g === "object" && Array.isArray(g.entries) ? g.entries : [];
809
+ for (const e of entries) {
810
+ const url = typeof e.url === "string" ? e.url : "";
811
+ const s = ensure(url);
812
+ if (!s) continue;
813
+ if (!s.title && typeof e.title === "string") s.title = e.title.slice(0, 200);
814
+ const ref = normRef(e.ref_id);
815
+ if (ref && s.round == null) s.round = roundOf(ref);
816
+ if (typeof e.snippet === "string" && e.snippet) addSnippet(url, s.title, e.snippet);
817
+ }
818
+ }
819
+ }
820
+ for (const m of raw.matchAll(/"url"\s*:\s*"(https?:\/\/[^"\\]+)"(?:[\s\S]{0,120}?"(?:title|name)"\s*:\s*"((?:[^"\\]|\\.){0,200})")?/g)) {
821
+ const s = ensure(m[1]);
822
+ if (s && !s.title && m[2]) s.title = unesc(m[2]).slice(0, 200);
823
+ }
824
+ let spanTotal = 0;
825
+ for (const cr of findJsonValues(raw, "content_references")) {
826
+ if (!Array.isArray(cr)) continue;
827
+ for (const ref of cr) {
828
+ const type = typeof ref.type === "string" ? ref.type : "";
829
+ const isBundle = /sources_footnote|nav_list|alt_text/.test(type);
830
+ const refs = refsInText(typeof ref.matched_text === "string" ? ref.matched_text : "");
831
+ if (!isBundle) spanTotal++;
832
+ const ord = typeof ref.start_idx === "number" ? ref.start_idx : isBundle ? Infinity : spanTotal;
833
+ const items = Array.isArray(ref.items) ? ref.items : [];
834
+ items.forEach((it, idx) => {
835
+ const refForItem = refs.length === items.length ? refs[idx] : refs[0];
836
+ const urls = [it.url].concat((Array.isArray(it.supporting_websites) ? it.supporting_websites : []).map((s) => s?.url)).filter((u) => typeof u === "string");
837
+ for (const u of urls) {
838
+ const s = ensure(u);
839
+ if (!s) continue;
840
+ if (!isBundle) {
841
+ s.cited = true;
842
+ s.timesCited++;
843
+ if (ord < s.firstOrder) s.firstOrder = ord;
844
+ } else if (!s.cited) {
845
+ s.cited = true;
846
+ }
847
+ if (refForItem && s.round == null) s.round = roundOf(refForItem);
848
+ }
849
+ if (typeof it.url === "string" && typeof it.snippet === "string") addSnippet(it.url, typeof it.title === "string" ? it.title : "", it.snippet);
850
+ });
851
+ }
852
+ }
853
+ for (const v of findJsonValues(raw, "citations")) {
854
+ if (!Array.isArray(v)) continue;
855
+ v.forEach((c, i) => {
856
+ const md = c && typeof c === "object" ? c.metadata : null;
857
+ if (!md || typeof md.url !== "string") return;
858
+ const s = ensure(md.url);
859
+ if (!s) return;
860
+ s.cited = true;
861
+ s.timesCited++;
862
+ const ord = typeof c.start_ix === "number" ? c.start_ix : i;
863
+ if (ord < s.firstOrder) s.firstOrder = ord;
864
+ if (typeof md.text === "string") addSnippet(md.url, typeof md.title === "string" ? md.title : "", md.text);
865
+ });
866
+ }
867
+ for (const m of raw.matchAll(/"content_type"\s*:\s*"tether_quote"([\s\S]{0,4000}?)"text"\s*:\s*"((?:[^"\\]|\\.)*)"/g)) {
868
+ const url = (m[1].match(/"url"\s*:\s*"([^"]+)"/) || [])[1];
869
+ const title = (m[1].match(/"title"\s*:\s*"((?:[^"\\]|\\.)*)"/) || [])[1];
870
+ if (url) {
871
+ ensure(url);
872
+ addSnippet(url, title ? unesc(title) : "", unesc(m[2]));
873
+ }
874
+ }
875
+ const modelMatches = [...raw.matchAll(/"resolved_model_slug"\s*:\s*"([^"]+)"/g)];
876
+ const slugMatches = modelMatches.length ? modelMatches : [...raw.matchAll(/"model_slug"\s*:\s*"([^"]+)"/g)];
877
+ if (slugMatches.length) meta.model = slugMatches[slugMatches.length - 1][1];
878
+ const finish = raw.match(/"finish_details"\s*:\s*\{[^}]*"type"\s*:\s*"([^"]+)"/);
879
+ if (finish) meta.finishType = finish[1];
880
+ const searchRounds = (raw.match(/"recipient"\s*:\s*"(?:web|browser)"[\s\S]{0,400}?"content_type"\s*:\s*"code"[\s\S]{0,200}?search\(/g) || []).length;
881
+ const sourceRounds = /* @__PURE__ */ new Set();
882
+ for (const s of sources.values()) if (s.round != null) sourceRounds.add(s.round);
883
+ meta.rounds = Math.max(searchRounds, sourceRounds.size);
884
+ const ordered = [...sources.values()].sort((a, b) => a.cited === b.cited ? b.timesCited - a.timesCited || a.firstOrder - b.firstOrder : a.cited ? -1 : 1).map((s) => ({
885
+ url: s.url,
886
+ domain: s.domain,
887
+ title: s.title || s.domain,
888
+ cited: s.cited,
889
+ timesCited: s.timesCited,
890
+ snippet: snippets.get(normUrl(s.url))?.text || "",
891
+ round: s.round
892
+ }));
893
+ return {
894
+ subQueries: [...queries],
895
+ sources: ordered,
896
+ snippets: [...snippets.values()],
897
+ meta,
898
+ unmappedKeys: [...unmapped].slice(0, 50)
899
+ };
900
+ }
901
+ };
902
+
903
+ // src/services/fanout/adapters/claude.ts
904
+ function isFavicon(u) {
905
+ return /\/s2\/favicons/i.test(u) || /favicon/i.test(u);
906
+ }
907
+ var claudeAdapter = {
908
+ platform: "Claude",
909
+ matchesHost(hostname) {
910
+ return /(^|\.)claude\.ai$/i.test(hostname);
911
+ },
912
+ parse(raw, prompt) {
913
+ const sources = /* @__PURE__ */ new Map();
914
+ const snippets = /* @__PURE__ */ new Map();
915
+ const queries = /* @__PURE__ */ new Set();
916
+ const unmapped = /* @__PURE__ */ new Set();
917
+ const meta = { model: "", finishType: "", title: "", rounds: 0 };
918
+ const ensure = (url) => {
919
+ if (!url || !isExternal(url) || isFavicon(url)) return null;
920
+ const k = normUrl(url);
921
+ let s = sources.get(k);
922
+ if (!s) {
923
+ s = { url, domain: domainOf(url), title: "", cited: false, timesCited: 0, snippet: "", round: null };
924
+ sources.set(k, s);
925
+ }
926
+ return s;
927
+ };
928
+ const toolInputs = /* @__PURE__ */ new Map();
929
+ const searchBlocks = /* @__PURE__ */ new Set();
930
+ const finalizeTool = (index) => {
931
+ const t = toolInputs.get(index);
932
+ if (!t || !/web_search|web_fetch|search|browse/i.test(t.name)) return;
933
+ try {
934
+ const input = JSON.parse(t.buf);
935
+ if (typeof input.query === "string") {
936
+ const q = cleanQuery(input.query);
937
+ if (q) queries.add(q);
938
+ }
939
+ } catch {
940
+ const m = t.buf.match(/"query"\s*:\s*"((?:[^"\\]|\\.)*)"/);
941
+ if (m) {
942
+ const q = cleanQuery(unesc(m[1]));
943
+ if (q) queries.add(q);
944
+ }
945
+ }
946
+ };
947
+ for (const line of raw.split("\n")) {
948
+ const s = (line.startsWith("data:") ? line.slice(5) : line).trim();
949
+ if (!s || s[0] !== "{" && s[0] !== "[") continue;
950
+ let ev;
951
+ try {
952
+ ev = JSON.parse(s);
953
+ } catch {
954
+ continue;
955
+ }
956
+ const type = ev.type;
957
+ if (type === "content_block_start") {
958
+ const cb = ev.content_block || {};
959
+ const index = typeof ev.index === "number" ? ev.index : -1;
960
+ if (cb.type === "tool_use" && index >= 0) {
961
+ const name = typeof cb.name === "string" ? cb.name : "";
962
+ toolInputs.set(index, { name, buf: "" });
963
+ if (/web_search|web_fetch|search|browse/i.test(name)) searchBlocks.add(index);
964
+ }
965
+ } else if (type === "content_block_delta") {
966
+ const d = ev.delta || {};
967
+ const index = typeof ev.index === "number" ? ev.index : -1;
968
+ if (d.type === "input_json_delta" && typeof d.partial_json === "string") {
969
+ const t = toolInputs.get(index);
970
+ if (t) t.buf += d.partial_json;
971
+ } else if (d.type === "citation_start_delta") {
972
+ const c = d.citation || {};
973
+ if (typeof c.url === "string") {
974
+ const src = ensure(c.url);
975
+ if (src) {
976
+ src.cited = true;
977
+ src.timesCited++;
978
+ if (!src.title && typeof c.title === "string") src.title = c.title.slice(0, 200);
979
+ if (typeof c.cited_text === "string" && c.cited_text) {
980
+ snippets.set(normUrl(c.url), { url: c.url, domain: domainOf(c.url), title: src.title, text: String(c.cited_text).slice(0, 400) });
981
+ }
982
+ }
983
+ }
984
+ }
985
+ } else if (type === "content_block_stop") {
986
+ const index = typeof ev.index === "number" ? ev.index : -1;
987
+ if (index >= 0) finalizeTool(index);
988
+ } else if (type === "message_delta" || type === "message_stop" || type === "message_start") {
989
+ const d = ev.delta || ev.message || {};
990
+ if (typeof d.stop_reason === "string" && !meta.finishType) meta.finishType = d.stop_reason;
991
+ if (typeof d.model === "string" && !meta.model) meta.model = d.model;
992
+ }
993
+ }
994
+ for (const index of searchBlocks) finalizeTool(index);
995
+ for (const m of raw.matchAll(/"title"\s*:\s*"((?:[^"\\]|\\.){1,200})"\s*,\s*"url"\s*:\s*"(https?:\/\/[^"\\]+)"/g)) {
996
+ const src = ensure(m[2]);
997
+ if (src && !src.title) src.title = unesc(m[1]).slice(0, 200);
998
+ }
999
+ for (const m of raw.matchAll(/"url"\s*:\s*"(https?:\/\/[^"\\]+)"/g)) ensure(m[1]);
1000
+ for (const m of raw.matchAll(/\\"title\\"\s*:\s*\\"((?:[^"\\]|\\.){1,160}?)\\"[\s\S]{0,160}?\\"url\\"\s*:\s*\\"(https?:[^"\\]+)\\"/g)) {
1001
+ const src = ensure(m[2]);
1002
+ if (src && !src.title) src.title = unesc(unesc(m[1])).slice(0, 200);
1003
+ }
1004
+ for (const m of raw.matchAll(/\\"url\\"\s*:\s*\\"(https?:[^"\\]+)\\"/g)) ensure(m[1]);
1005
+ if (!meta.model) {
1006
+ const mm = raw.match(/"model"\s*:\s*"(claude[^"]+)"/);
1007
+ if (mm) meta.model = mm[1];
1008
+ }
1009
+ meta.rounds = queries.size > 0 ? searchBlocks.size : 0;
1010
+ for (const m of raw.matchAll(/"([a-z_]*(?:quer|search)[a-z_]*)"\s*:/gi)) unmapped.add(m[1]);
1011
+ const ordered = [...sources.values()].sort((a, b) => a.cited === b.cited ? b.timesCited - a.timesCited : a.cited ? -1 : 1).map((s) => ({
1012
+ url: s.url,
1013
+ domain: s.domain,
1014
+ title: s.title || s.domain,
1015
+ cited: s.cited,
1016
+ timesCited: s.timesCited,
1017
+ snippet: snippets.get(normUrl(s.url))?.text || "",
1018
+ round: s.round
1019
+ }));
1020
+ return {
1021
+ subQueries: [...queries],
1022
+ sources: ordered,
1023
+ snippets: [...snippets.values()],
1024
+ meta,
1025
+ unmappedKeys: [...unmapped].slice(0, 50)
1026
+ };
1027
+ }
1028
+ };
1029
+
1030
+ // src/services/fanout/adapters/index.ts
1031
+ var FANOUT_ADAPTERS = [chatgptAdapter, claudeAdapter];
1032
+ function adapterForHost(hostname) {
1033
+ return FANOUT_ADAPTERS.find((a) => a.matchesHost(hostname)) ?? null;
1034
+ }
1035
+
1036
+ // src/services/fanout/build.ts
1037
+ function buildFanoutResult(ctx) {
1038
+ const { platform, parsed, ready, rawBytes } = ctx;
1039
+ const browsedUrls = parsed.sources;
1040
+ const citedUrls = browsedUrls.filter((s) => s.cited);
1041
+ const browsedOnly = browsedUrls.filter((s) => !s.cited);
1042
+ const prompt = ctx.prompt || parsed.meta.title || "";
1043
+ const result = {
1044
+ platform,
1045
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1046
+ prompt,
1047
+ meta: parsed.meta,
1048
+ subQueries: parsed.subQueries,
1049
+ browsedUrls,
1050
+ citedUrls,
1051
+ browsedOnly,
1052
+ snippets: parsed.snippets,
1053
+ counts: {
1054
+ subQueries: parsed.subQueries.length,
1055
+ browsed: browsedUrls.length,
1056
+ cited: citedUrls.length,
1057
+ browsedOnly: browsedOnly.length
1058
+ }
1059
+ };
1060
+ if (parsed.subQueries.length === 0 && browsedUrls.length === 0) {
1061
+ result.debug = {
1062
+ interceptorReady: ready,
1063
+ rawBytes,
1064
+ unmappedKeys: parsed.unmappedKeys,
1065
+ note: ready ? "Capture hook is live but no fan-out was seen yet. Run a NEW prompt that triggers web search in this session, then call browser_capture_fanout again. Fan-out is only captured as it streams; past answers from before the session opened cannot be recovered." : "Capture hook not detected on this page. The session must be opened with MCP_SCRAPER_BROWSER_MODE=local and navigated to chatgpt.com or claude.ai after opening, so the hook installs before the site scripts run."
1066
+ };
1067
+ }
1068
+ return result;
1069
+ }
1070
+
1071
+ // src/services/fanout/classify.ts
1072
+ var SOCIAL = /(^|\.)(youtube\.com|youtu\.be|instagram\.com|tiktok\.com|facebook\.com|fb\.com|x\.com|twitter\.com|linkedin\.com|pinterest\.com|threads\.net|vimeo\.com)$/i;
1073
+ var ENCYCLOPEDIA = /(^|\.)(wikipedia\.org|wikimedia\.org|britannica\.com|merriam-webster\.com|dictionary\.com|fandom\.com|investopedia\.com)$/i;
1074
+ var REVIEW = /(^|\.)(yelp\.com|trustpilot\.com|g2\.com|capterra\.com|angi\.com|angieslist\.com|bbb\.org|consumeraffairs\.com|bestcompany\.com|sitejabber\.com|clutch\.co|bestpickreports\.com|homeguide\.com|porch\.com|ontoplist\.com|thumbtack\.com)$/i;
1075
+ var NEWS = /(^|\.)(reuters\.com|apnews\.com|nytimes\.com|wsj\.com|washingtonpost\.com|bloomberg\.com|cnbc\.com|forbes\.com|bbc\.com|theguardian\.com|cnn\.com|npr\.org|usatoday\.com|expressnews\.com|mysanantonio\.com|beaumontenterprise\.com)$/i;
1076
+ function classifyDomain(domain, firstPartyDomain) {
1077
+ const d = domain.toLowerCase().replace(/^www\./, "");
1078
+ if (firstPartyDomain) {
1079
+ const fp = firstPartyDomain.toLowerCase().replace(/^www\./, "");
1080
+ if (fp && (d === fp || d.endsWith("." + fp))) return "First-party/vendor";
1081
+ }
1082
+ if (/(^|\.)reddit\.com$/i.test(d)) return "Reddit";
1083
+ if (SOCIAL.test(d)) return "Social/video";
1084
+ if (ENCYCLOPEDIA.test(d)) return "Encyclopedia";
1085
+ if (REVIEW.test(d)) return "Review site";
1086
+ if (NEWS.test(d) || /(^|\.)news\./i.test(d)) return "News/media";
1087
+ if (/^docs?\.|^developer\.|^developers\.|readthedocs|\.dev$/i.test(d)) return "Docs";
1088
+ return "Blog";
1089
+ }
1090
+ function emptyCategoryCounts() {
1091
+ return { "First-party/vendor": 0, "News/media": 0, Reddit: 0, "Social/video": 0, Encyclopedia: 0, "Review site": 0, Docs: 0, Blog: 0 };
1092
+ }
1093
+ function enrichFanout(result, opts = {}) {
1094
+ const addSiteType = (s) => ({ ...s, siteType: classifyDomain(s.domain, opts.firstPartyDomain) });
1095
+ const browsedUrls = result.browsedUrls.map(addSiteType);
1096
+ const citedUrls = result.citedUrls.map(addSiteType);
1097
+ const browsedOnly = result.browsedOnly.map(addSiteType);
1098
+ const byCategory = emptyCategoryCounts();
1099
+ const domainAgg = /* @__PURE__ */ new Map();
1100
+ for (const s of browsedUrls) {
1101
+ byCategory[s.siteType]++;
1102
+ const cur = domainAgg.get(s.domain);
1103
+ if (!cur) domainAgg.set(s.domain, { domain: s.domain, count: 1, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType });
1104
+ else {
1105
+ cur.count++;
1106
+ cur.cited = cur.cited || s.cited;
1107
+ cur.timesCited += s.timesCited;
1108
+ }
1109
+ }
1110
+ const topSites = [...domainAgg.values()].sort((a, b) => b.count - a.count || b.timesCited - a.timesCited);
1111
+ const citationOrder = citedUrls.slice().sort((a, b) => b.timesCited - a.timesCited).map((s, i) => ({ rank: i + 1, domain: s.domain, url: s.url, timesCited: s.timesCited }));
1112
+ return {
1113
+ platform: result.platform,
1114
+ capturedAt: result.capturedAt,
1115
+ prompt: result.prompt,
1116
+ meta: result.meta,
1117
+ queries: result.subQueries,
1118
+ browsedUrls,
1119
+ citedUrls,
1120
+ browsedOnly,
1121
+ snippets: result.snippets,
1122
+ counts: result.counts,
1123
+ aggregates: { topSites, citationOrder, byCategory },
1124
+ firstPartyDomain: opts.firstPartyDomain ? opts.firstPartyDomain.toLowerCase().replace(/^www\./, "") : null,
1125
+ ...result.debug ? { debug: result.debug } : {}
1126
+ };
1127
+ }
1128
+
1129
+ // src/services/fanout/export.ts
473
1130
  var import_node_fs2 = require("fs");
1131
+ var import_node_os3 = require("os");
474
1132
  var import_node_path3 = require("path");
475
- var import_playwright = require("playwright");
1133
+ var import_papaparse = __toESM(require("papaparse"), 1);
1134
+ function outputBaseDir() {
1135
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path3.join)((0, import_node_os3.homedir)(), "Downloads", "mcp-scraper");
1136
+ }
1137
+ function safe(value) {
1138
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "capture";
1139
+ }
1140
+ function writeTable(path, rows, delimiter) {
1141
+ (0, import_node_fs2.writeFileSync)(path, import_papaparse.default.unparse(rows.length ? rows : [{}], { delimiter }));
1142
+ }
1143
+ function exportFanout(enriched) {
1144
+ const stamp = safe(enriched.capturedAt.replace(/[:.]/g, "-"));
1145
+ const dir = (0, import_node_path3.join)(outputBaseDir(), "fanout", `${stamp}-${safe(enriched.platform)}`);
1146
+ (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
1147
+ const queryRows = enriched.queries.map((q, i) => ({ index: i + 1, query: q }));
1148
+ const citationRows = enriched.aggregates.citationOrder.map((c) => {
1149
+ const src = enriched.citedUrls.find((s) => s.url === c.url);
1150
+ return { rank: c.rank, domain: c.domain, url: c.url, timesCited: c.timesCited, siteType: src?.siteType ?? "", title: src?.title ?? "" };
1151
+ });
1152
+ const sourceRows = enriched.browsedUrls.map((s) => ({ domain: s.domain, url: s.url, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType, title: s.title, round: s.round ?? "" }));
1153
+ const domainRows = enriched.aggregates.topSites.map((d) => ({ domain: d.domain, count: d.count, cited: d.cited, timesCited: d.timesCited, siteType: d.siteType }));
1154
+ const paths = {
1155
+ dir,
1156
+ json: (0, import_node_path3.join)(dir, "fanout.json"),
1157
+ queriesCsv: (0, import_node_path3.join)(dir, "queries.csv"),
1158
+ queriesTsv: (0, import_node_path3.join)(dir, "queries.tsv"),
1159
+ citationsCsv: (0, import_node_path3.join)(dir, "citations.csv"),
1160
+ sourcesCsv: (0, import_node_path3.join)(dir, "sources.csv"),
1161
+ domainsCsv: (0, import_node_path3.join)(dir, "domains.csv"),
1162
+ report: (0, import_node_path3.join)(dir, "report.html")
1163
+ };
1164
+ (0, import_node_fs2.writeFileSync)(paths.json, JSON.stringify(enriched, null, 2));
1165
+ writeTable(paths.queriesCsv, queryRows, ",");
1166
+ writeTable(paths.queriesTsv, queryRows, " ");
1167
+ writeTable(paths.citationsCsv, citationRows, ",");
1168
+ writeTable(paths.sourcesCsv, sourceRows, ",");
1169
+ writeTable(paths.domainsCsv, domainRows, ",");
1170
+ (0, import_node_fs2.writeFileSync)(paths.report, renderReportHtml(enriched));
1171
+ return paths;
1172
+ }
1173
+ function esc(s) {
1174
+ return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1175
+ }
1176
+ function renderReportHtml(enriched) {
1177
+ const data = JSON.stringify(enriched).replace(/</g, "\\u003c");
1178
+ return `<!doctype html>
1179
+ <html lang="en">
1180
+ <head>
1181
+ <meta charset="utf-8" />
1182
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1183
+ <title>AI Search Fan-Out \u2014 ${esc(enriched.platform)}</title>
1184
+ <style>
1185
+ :root{--bg:#0b0d12;--panel:#151922;--line:#252b38;--ink:#e8ecf3;--mut:#8b95a7;--acc:#5b9dff;--cite:#39d98a;--browse:#5a6677}
1186
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}
1187
+ .wrap{max-width:980px;margin:0 auto;padding:28px 20px 80px}
1188
+ h1{font-size:20px;margin:0 0 2px}.sub{color:var(--mut);margin:0 0 20px;font-size:13px}
1189
+ .stat{font-size:30px;font-weight:700}.stat small{font-size:13px;font-weight:400;color:var(--mut)}
1190
+ .cards{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}
1191
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
1192
+ .card b{font-size:22px;display:block}.card span{color:var(--mut);font-size:12px}
1193
+ .bars{margin:8px 0 22px}.bar{display:flex;align-items:center;gap:8px;margin:4px 0}
1194
+ .bar .lab{width:150px;color:var(--mut);font-size:12px;text-align:right}.bar .track{flex:1;background:#10141c;border-radius:5px;overflow:hidden}
1195
+ .bar .fill{background:var(--acc);height:16px}.bar .n{width:34px;font-size:12px;color:var(--mut)}
1196
+ .tabs{display:flex;gap:8px;margin:22px 0 12px}.tab{background:var(--panel);border:1px solid var(--line);color:var(--ink);padding:7px 14px;border-radius:8px;cursor:pointer}
1197
+ .tab.on{background:var(--acc);border-color:var(--acc);color:#04101f;font-weight:600}
1198
+ .chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
1199
+ .chip{background:var(--panel);border:1px solid var(--line);color:var(--mut);padding:4px 10px;border-radius:20px;cursor:pointer;font-size:12px}
1200
+ .chip.on{border-color:var(--acc);color:var(--ink)}.chip b{color:var(--ink)}
1201
+ .row{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin:6px 0;display:flex;gap:10px;align-items:flex-start}
1202
+ .row .idx{color:var(--mut);min-width:22px}.row .main{flex:1}
1203
+ .tag{font-size:11px;padding:2px 7px;border-radius:6px;border:1px solid var(--line);color:var(--mut);margin-right:5px}
1204
+ .dom{font-weight:600}.url{color:var(--mut);font-size:12px;word-break:break-all}
1205
+ .pill{font-size:11px;padding:2px 7px;border-radius:6px}.pill.cite{background:rgba(57,217,138,.15);color:var(--cite)}.pill.brow{background:rgba(90,102,119,.2);color:#aeb8c8}
1206
+ .copy{background:none;border:1px solid var(--line);color:var(--mut);border-radius:6px;cursor:pointer;font-size:11px;padding:2px 8px}
1207
+ .hide{display:none}
1208
+ </style>
1209
+ </head>
1210
+ <body><div class="wrap">
1211
+ <h1>AI Search Fan-Out</h1>
1212
+ <p class="sub" id="sub"></p>
1213
+ <div class="stat" id="stat"></div>
1214
+ <div class="cards" id="cards"></div>
1215
+ <div id="charts"></div>
1216
+ <div class="tabs"><button class="tab on" data-tab="q">Queries</button><button class="tab" data-tab="u">URLs</button></div>
1217
+ <div class="chips" id="chips"></div>
1218
+ <div id="list"></div>
1219
+ </div>
1220
+ <script>
1221
+ const D=${data};
1222
+ const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
1223
+ document.getElementById('sub').textContent=D.platform+' \xB7 '+(D.meta.model||'model n/a')+' \xB7 '+D.meta.rounds+' search round(s) \xB7 prompt: '+(D.prompt||'');
1224
+ document.getElementById('stat').innerHTML='Total Fan-Out: '+D.counts.subQueries+' <small>sub-queries \xB7 '+D.counts.browsed+' URLs researched \xB7 '+D.counts.cited+' cited</small>';
1225
+ const cards=[['Sub-queries',D.counts.subQueries],['Researched',D.counts.browsed],['Cited',D.counts.cited],['Browsed-only',D.counts.browsedOnly]];
1226
+ const cc=document.getElementById('cards');cards.forEach(([k,v])=>{const c=el('div','card');c.appendChild(el('b',null,v));c.appendChild(el('span',null,k));cc.appendChild(c)});
1227
+ function barChart(title,obj){const max=Math.max(1,...Object.values(obj));const box=el('div','bars');box.appendChild(el('div',null,'<b>'+title+'</b>'));Object.entries(obj).forEach(([k,v])=>{const r=el('div','bar');r.appendChild(el('div','lab',k));const t=el('div','track');const f=el('div','fill');f.style.width=(v/max*100)+'%';t.appendChild(f);r.appendChild(t);r.appendChild(el('div','n',v));box.appendChild(r)});return box}
1228
+ const charts=document.getElementById('charts');
1229
+ charts.appendChild(barChart('URL Categories',D.aggregates.byCategory));
1230
+ let tab='q',filter=null;
1231
+ const chips=document.getElementById('chips'),list=document.getElementById('list');
1232
+ function render(){
1233
+ chips.innerHTML='';list.innerHTML='';
1234
+ if(tab==='q'){
1235
+ D.queries.forEach((q,i)=>{const r=el('div','row');r.appendChild(el('div','idx',i+1));const m=el('div','main');m.appendChild(el('div',null,esc(q)));r.appendChild(m);const b=el('button','copy','copy');b.onclick=()=>navigator.clipboard.writeText(q);r.appendChild(b);list.appendChild(r)});
1236
+ } else {
1237
+ const counts={};D.browsedUrls.forEach(s=>{counts[s.siteType]=(counts[s.siteType]||0)+1});
1238
+ Object.entries(counts).forEach(([k,v])=>{const c=el('button','chip'+(filter===k?' on':''),k+' <b>'+v+'</b>');c.onclick=()=>{filter=filter===k?null:k;render()};chips.appendChild(c)});
1239
+ D.browsedUrls.forEach(s=>{if(filter&&s.siteType!==filter)return;const r=el('div','row');const m=el('div','main');m.appendChild(el('div',null,'<span class="dom">'+esc(s.domain)+'</span> <span class="pill '+(s.cited?'cite':'brow')+'">'+(s.cited?'cited '+s.timesCited+'\xD7':'browsed')+'</span> <span class="tag">'+s.siteType+'</span>'));m.appendChild(el('div','url',esc(s.url)));r.appendChild(m);list.appendChild(r)});
1240
+ }
1241
+ }
1242
+ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
1243
+ document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>{document.querySelectorAll('.tab').forEach(x=>x.classList.remove('on'));t.classList.add('on');tab=t.dataset.tab;filter=null;render()});
1244
+ render();
1245
+ </script>
1246
+ </body></html>`;
1247
+ }
1248
+
1249
+ // src/services/fanout/run-capture.ts
1250
+ var COMPOSER_SELECTORS = [
1251
+ "#prompt-textarea",
1252
+ "div#prompt-textarea",
1253
+ 'textarea[data-testid="prompt-textarea"]',
1254
+ 'div.ProseMirror[contenteditable="true"]',
1255
+ 'div[contenteditable="true"]',
1256
+ "textarea"
1257
+ ];
1258
+ async function sendPrompt(page, text) {
1259
+ for (const selector of COMPOSER_SELECTORS) {
1260
+ const locator = page.locator(selector).first();
1261
+ try {
1262
+ await locator.waitFor({ state: "visible", timeout: 12e3 });
1263
+ } catch {
1264
+ continue;
1265
+ }
1266
+ await locator.click({ timeout: 4e3 }).catch(() => void 0);
1267
+ await locator.focus().catch(() => void 0);
1268
+ await page.keyboard.type(text, { delay: 12 });
1269
+ await page.waitForTimeout(300).catch(() => void 0);
1270
+ await page.keyboard.press("Enter");
1271
+ return true;
1272
+ }
1273
+ return false;
1274
+ }
1275
+ async function waitForAnswer(page, cap, baseline, waitMs) {
1276
+ const start = Date.now();
1277
+ while (Date.now() - start < waitMs && cap.answerStreamCount() <= baseline) {
1278
+ await page.waitForTimeout(400).catch(() => void 0);
1279
+ }
1280
+ await page.waitForTimeout(1e3).catch(() => void 0);
1281
+ }
1282
+ async function runFanoutCapture(page, input) {
1283
+ const cap = new FanoutCdpCapture();
1284
+ await cap.attach(page);
1285
+ try {
1286
+ const baseline = cap.answerStreamCount();
1287
+ if (input.prompt) await sendPrompt(page, input.prompt).catch(() => false);
1288
+ const waitMs = input.wait_ms ?? (input.prompt ? 9e4 : 8e3);
1289
+ await waitForAnswer(page, cap, baseline, waitMs);
1290
+ await cap.drain();
1291
+ let hostname = "";
1292
+ try {
1293
+ hostname = new URL(page.url()).hostname;
1294
+ } catch {
1295
+ hostname = "";
1296
+ }
1297
+ const adapter = adapterForHost(hostname);
1298
+ if (!adapter) {
1299
+ throw new Error(`browser_capture_fanout supports chatgpt.com and claude.ai only; the session is on ${hostname || "an unknown page"}. Navigate there first with browser_goto.`);
1300
+ }
1301
+ const text = cap.bestAnswerText();
1302
+ const parsed = adapter.parse(text, input.prompt || "");
1303
+ const result = buildFanoutResult({ platform: adapter.platform, parsed, prompt: input.prompt || "", ready: true, rawBytes: text.length });
1304
+ const enriched = enrichFanout(result, { firstPartyDomain: input.first_party_domain });
1305
+ const exports2 = input.export ? exportFanout(enriched) : null;
1306
+ return { result: enriched, exports: exports2 };
1307
+ } finally {
1308
+ await cap.detach().catch(() => void 0);
1309
+ }
1310
+ }
1311
+
1312
+ // src/services/browser-agent/local-browser-agent-service.ts
1313
+ import_playwright_extra.chromium.use((0, import_puppeteer_extra_plugin_stealth.default)());
476
1314
  function directProfileDir() {
477
1315
  return process.env.MCP_SCRAPER_BROWSER_PROFILE_DIR?.trim() || void 0;
478
1316
  }
479
1317
  function configuredProfileName(profile) {
480
- return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directProfileDir() ? (0, import_node_path3.basename)(directProfileDir()) : "default");
1318
+ return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directProfileDir() ? (0, import_node_path4.basename)(directProfileDir()) : "default");
481
1319
  }
482
1320
  function launchExecutablePath(profile) {
483
1321
  const configured = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || profile.browserExecutablePath || defaultChromeExecutablePath();
484
- return configured && (0, import_node_fs2.existsSync)(configured) ? configured : void 0;
1322
+ return configured && (0, import_node_fs3.existsSync)(configured) ? configured : void 0;
485
1323
  }
486
1324
  function ignoreDefaultArgsFor(executablePath) {
487
1325
  if (!executablePath) return void 0;
@@ -754,6 +1592,17 @@ var LocalBrowserAgentService = class {
754
1592
  list(includeClosed = false) {
755
1593
  return Array.from(this.sessions.values()).filter((session) => includeClosed || session.status === "open").map(sessionSummary);
756
1594
  }
1595
+ async captureFanout(sessionId, input) {
1596
+ const session = this.requireOpenSession(sessionId);
1597
+ this.touch(session);
1598
+ return runFanoutCapture(session.page, input);
1599
+ }
1600
+ async screenshotOnly(sessionId) {
1601
+ const session = this.requireOpenSession(sessionId);
1602
+ await this.waitForPage(session.page);
1603
+ const img = await session.page.screenshot({ type: "png", fullPage: false });
1604
+ return img.toString("base64");
1605
+ }
757
1606
  async resolveProfile(profile) {
758
1607
  const directDir = directProfileDir();
759
1608
  const requestedName = configuredProfileName(profile);
@@ -777,7 +1626,7 @@ var LocalBrowserAgentService = class {
777
1626
  const existing = this.contexts.get(profile.userDataDir);
778
1627
  if (existing) return existing;
779
1628
  const executablePath = launchExecutablePath(profile);
780
- const context = await import_playwright.chromium.launchPersistentContext(profile.userDataDir, {
1629
+ const context = await import_playwright_extra.chromium.launchPersistentContext(profile.userDataDir, {
781
1630
  headless: false,
782
1631
  viewport: { width: 1440, height: 900 },
783
1632
  acceptDownloads: true,
@@ -824,7 +1673,7 @@ var LocalBrowserAgentService = class {
824
1673
  };
825
1674
 
826
1675
  // src/version.ts
827
- var PACKAGE_VERSION = "0.2.24";
1676
+ var PACKAGE_VERSION = "0.3.0";
828
1677
 
829
1678
  // src/mcp/browser-agent-tool-schemas.ts
830
1679
  var import_zod = require("zod");
@@ -967,6 +1816,70 @@ var BrowserReplayAnnotateInputSchema = {
967
1816
  var BrowserListInputSchema = {
968
1817
  include_closed: import_zod.z.boolean().default(false).describe("Include closed sessions in the list.")
969
1818
  };
1819
+ var BrowserCaptureFanoutInputSchema = {
1820
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. The session must be a local-mode session on chatgpt.com or claude.ai that is logged in. Use only a returned session_id; do not construct one yourself."),
1821
+ prompt: import_zod.z.string().optional().describe("Optional prompt to type into the chat composer and submit before capturing. Omit to passively capture the fan-out of a prompt the user just ran in the visible browser. The prompt must trigger web search for there to be a fan-out."),
1822
+ wait_ms: import_zod.z.number().int().min(0).max(18e4).optional().describe("How long to wait for the answer stream to finish before parsing. Defaults to 90000 when a prompt is sent, 8000 for passive capture. Capture ends as soon as the answer stream finishes."),
1823
+ first_party_domain: import_zod.z.string().optional().describe("The brand/site being researched, for example example.com. Sources on this domain are tagged First-party/vendor. Set this when the prompt is about a specific business."),
1824
+ reset: import_zod.z.boolean().default(false).describe("Clear any previously buffered stream for this page before capturing. Use when re-running to avoid mixing turns."),
1825
+ export: import_zod.z.boolean().default(false).describe("When true, write JSON, CSV, TSV exports (queries, citations, sources, domains) and a self-contained HTML report to MCP_SCRAPER_OUTPUT_DIR/fanout and return their paths.")
1826
+ };
1827
+ var FanoutSourceOutput = import_zod.z.object({
1828
+ url: import_zod.z.string(),
1829
+ domain: import_zod.z.string(),
1830
+ title: import_zod.z.string(),
1831
+ cited: import_zod.z.boolean(),
1832
+ timesCited: import_zod.z.number().int().min(0),
1833
+ snippet: import_zod.z.string(),
1834
+ round: import_zod.z.number().int().nullable(),
1835
+ siteType: import_zod.z.string().describe("URL category: First-party/vendor, News/media, Reddit, Social/video, Encyclopedia, Review site, Docs, or Blog.")
1836
+ });
1837
+ var BrowserCaptureFanoutOutputSchema = {
1838
+ ...BrowserBaseOutput,
1839
+ tool: import_zod.z.literal("browser_capture_fanout"),
1840
+ platform: import_zod.z.enum(["ChatGPT", "Claude"]).describe("Which AI-search surface the fan-out was captured from."),
1841
+ captured_at: import_zod.z.string(),
1842
+ prompt: import_zod.z.string().describe("The user prompt that triggered the captured fan-out, when recoverable."),
1843
+ meta: import_zod.z.object({
1844
+ model: import_zod.z.string(),
1845
+ finishType: import_zod.z.string(),
1846
+ title: import_zod.z.string(),
1847
+ rounds: import_zod.z.number().int().min(0)
1848
+ }),
1849
+ queries: import_zod.z.array(import_zod.z.string()).describe("Every web-search sub-query the model issued, in capture order. Classify these by funnel stage and type yourself from the text."),
1850
+ browsed_urls: import_zod.z.array(FanoutSourceOutput).describe("Every researched URL (cited and browsed-only), cited first."),
1851
+ cited_urls: import_zod.z.array(FanoutSourceOutput).describe("The subset of researched URLs actually cited in the final answer."),
1852
+ browsed_only: import_zod.z.array(FanoutSourceOutput).describe("Researched URLs the model pulled but did not cite."),
1853
+ snippets: import_zod.z.array(import_zod.z.object({ url: import_zod.z.string(), domain: import_zod.z.string(), title: import_zod.z.string(), text: import_zod.z.string() })),
1854
+ counts: import_zod.z.object({
1855
+ subQueries: import_zod.z.number().int().min(0),
1856
+ browsed: import_zod.z.number().int().min(0),
1857
+ cited: import_zod.z.number().int().min(0),
1858
+ browsedOnly: import_zod.z.number().int().min(0)
1859
+ }),
1860
+ aggregates: import_zod.z.object({
1861
+ topSites: import_zod.z.array(import_zod.z.object({ domain: import_zod.z.string(), count: import_zod.z.number().int(), cited: import_zod.z.boolean(), timesCited: import_zod.z.number().int(), siteType: import_zod.z.string() })),
1862
+ citationOrder: import_zod.z.array(import_zod.z.object({ rank: import_zod.z.number().int(), domain: import_zod.z.string(), url: import_zod.z.string(), timesCited: import_zod.z.number().int() })),
1863
+ byCategory: import_zod.z.record(import_zod.z.number().int())
1864
+ }).describe("Objective aggregates: top sourced sites by frequency, citation order, and URL-category counts."),
1865
+ first_party_domain: import_zod.z.string().nullable(),
1866
+ exports: import_zod.z.object({
1867
+ dir: import_zod.z.string(),
1868
+ json: import_zod.z.string(),
1869
+ queriesCsv: import_zod.z.string(),
1870
+ queriesTsv: import_zod.z.string(),
1871
+ citationsCsv: import_zod.z.string(),
1872
+ sourcesCsv: import_zod.z.string(),
1873
+ domainsCsv: import_zod.z.string(),
1874
+ report: import_zod.z.string()
1875
+ }).nullable().describe("Local file paths when export=true, otherwise null."),
1876
+ debug: import_zod.z.object({
1877
+ interceptorReady: import_zod.z.boolean(),
1878
+ rawBytes: import_zod.z.number().int().min(0),
1879
+ unmappedKeys: import_zod.z.array(import_zod.z.string()),
1880
+ note: import_zod.z.string()
1881
+ }).optional()
1882
+ };
970
1883
  var BrowserOpenOutputSchema = {
971
1884
  ok: import_zod.z.boolean(),
972
1885
  tool: import_zod.z.literal("browser_open"),
@@ -1115,8 +2028,8 @@ var BrowserListSessionsOutputSchema = {
1115
2028
  // src/mcp/replay-annotator.ts
1116
2029
  var import_node_child_process = require("child_process");
1117
2030
  var import_promises3 = require("fs/promises");
1118
- var import_node_os3 = require("os");
1119
- var import_node_path4 = require("path");
2031
+ var import_node_os4 = require("os");
2032
+ var import_node_path5 = require("path");
1120
2033
  var import_node_util = require("util");
1121
2034
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
1122
2035
  function finiteNumber(value) {
@@ -1126,11 +2039,11 @@ function clamp(value, min, max) {
1126
2039
  return Math.min(max, Math.max(min, value));
1127
2040
  }
1128
2041
  function formatAssTime(seconds) {
1129
- const safe = Math.max(0, seconds);
1130
- const hours = Math.floor(safe / 3600);
1131
- const minutes = Math.floor(safe % 3600 / 60);
1132
- const wholeSeconds = Math.floor(safe % 60);
1133
- const centiseconds = Math.floor((safe - Math.floor(safe)) * 100);
2042
+ const safe2 = Math.max(0, seconds);
2043
+ const hours = Math.floor(safe2 / 3600);
2044
+ const minutes = Math.floor(safe2 % 3600 / 60);
2045
+ const wholeSeconds = Math.floor(safe2 % 60);
2046
+ const centiseconds = Math.floor((safe2 - Math.floor(safe2)) * 100);
1134
2047
  return `${hours}:${String(minutes).padStart(2, "0")}:${String(wholeSeconds).padStart(2, "0")}.${String(centiseconds).padStart(2, "0")}`;
1135
2048
  }
1136
2049
  function cssHexToAssColor(color) {
@@ -1339,8 +2252,8 @@ function ffmpegFilterPath(path) {
1339
2252
  async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
1340
2253
  if (!options.annotations.length) throw new Error("annotations must include at least one item");
1341
2254
  const size = await videoSize(inputFilePath);
1342
- const tmp = await (0, import_promises3.mkdtemp)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "mcp-scraper-ass-"));
1343
- const assPath = (0, import_node_path4.join)(tmp, "annotations.ass");
2255
+ const tmp = await (0, import_promises3.mkdtemp)((0, import_node_path5.join)((0, import_node_os4.tmpdir)(), "mcp-scraper-ass-"));
2256
+ const assPath = (0, import_node_path5.join)(tmp, "annotations.ass");
1344
2257
  try {
1345
2258
  await (0, import_promises3.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
1346
2259
  await execFileAsync("ffmpeg", [
@@ -1408,8 +2321,8 @@ function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_
1408
2321
  nextRecommendedTool
1409
2322
  });
1410
2323
  }
1411
- function outputBaseDir() {
1412
- return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path5.join)((0, import_node_os4.homedir)(), "Downloads", "mcp-scraper");
2324
+ function outputBaseDir2() {
2325
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path6.join)((0, import_node_os5.homedir)(), "Downloads", "mcp-scraper");
1413
2326
  }
1414
2327
  function safeFilePart(value) {
1415
2328
  return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
@@ -1418,7 +2331,7 @@ function replayFilePath(sessionId, replayId, filename) {
1418
2331
  const requested = filename?.trim();
1419
2332
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1420
2333
  const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
1421
- return (0, import_node_path5.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
2334
+ return (0, import_node_path6.join)(outputBaseDir2(), "browser-replays", `${name}.mp4`);
1422
2335
  }
1423
2336
  function annotatedReplayFilePath(sessionId, replayId, filename) {
1424
2337
  const requested = filename?.trim();
@@ -1478,8 +2391,8 @@ function registerBrowserAgentMcpTools(server2, opts) {
1478
2391
  }
1479
2392
  const bytes = Buffer.from(await res.arrayBuffer());
1480
2393
  const filePath = replayFilePath(sessionId, replayId, filename);
1481
- (0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(outputBaseDir(), "browser-replays"), { recursive: true });
1482
- (0, import_node_fs3.writeFileSync)(filePath, bytes);
2394
+ (0, import_node_fs4.mkdirSync)((0, import_node_path6.join)(outputBaseDir2(), "browser-replays"), { recursive: true });
2395
+ (0, import_node_fs4.writeFileSync)(filePath, bytes);
1483
2396
  return {
1484
2397
  ok: true,
1485
2398
  data: {
@@ -2130,7 +3043,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
2130
3043
  try {
2131
3044
  const sourcePath = String(downloaded.data.file_path);
2132
3045
  const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
2133
- (0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(outputBaseDir(), "browser-replays"), { recursive: true });
3046
+ (0, import_node_fs4.mkdirSync)((0, import_node_path6.join)(outputBaseDir2(), "browser-replays"), { recursive: true });
2134
3047
  const result = await annotateReplayVideo(sourcePath, outputPath, {
2135
3048
  annotations: input.annotations,
2136
3049
  sourceWidth: input.source_width,
@@ -2223,17 +3136,65 @@ function registerBrowserAgentMcpTools(server2, opts) {
2223
3136
  });
2224
3137
  }
2225
3138
  );
3139
+ server2.registerTool(
3140
+ "browser_capture_fanout",
3141
+ {
3142
+ title: "Capture AI Search Fan-Out",
3143
+ description: "Capture the query fan-out behind a ChatGPT or Claude web-search answer for Answer Engine Optimization (AEO) analysis: the exact sub-queries the model issued, every researched URL split into cited vs browsed-only with citation frequency and snippet, each source tagged by site category, plus top sourced sites and citation order. This tool returns the raw, structured fan-out data; YOU then analyze it \u2014 classify each sub-query by funnel stage (Problem/Solution/Decision-aware, Retention) and type (BoFu, Branded, Comparison, How-to, operator), identify which brands the model researched, and surface AEO insights. Requires MCP_SCRAPER_BROWSER_MODE=local with the user's own logged-in Chrome profile (or a logged-in hosted profile); it reads the network stream the page receives and never leaves the machine. Open the session with browser_open and browser_goto to chatgpt.com or claude.ai first. Pass prompt to send and capture a new query, or omit it to capture a prompt the user just ran. Fan-out is only captured as it streams, so run the prompt with the session open.",
3144
+ inputSchema: BrowserCaptureFanoutInputSchema,
3145
+ outputSchema: BrowserCaptureFanoutOutputSchema,
3146
+ annotations: annotations("Capture AI Search Fan-Out", true)
3147
+ },
3148
+ async (input) => {
3149
+ const emit = (result, exports2) => structuredResult({
3150
+ ok: true,
3151
+ tool: "browser_capture_fanout",
3152
+ session_id: input.session_id,
3153
+ platform: result.platform,
3154
+ captured_at: result.capturedAt,
3155
+ prompt: result.prompt,
3156
+ meta: result.meta,
3157
+ queries: result.queries,
3158
+ browsed_urls: result.browsedUrls,
3159
+ cited_urls: result.citedUrls,
3160
+ browsed_only: result.browsedOnly,
3161
+ snippets: result.snippets,
3162
+ counts: result.counts,
3163
+ aggregates: result.aggregates,
3164
+ first_party_domain: result.firstPartyDomain,
3165
+ exports: exports2 ?? null,
3166
+ ...result.debug ? { debug: result.debug } : {}
3167
+ });
3168
+ if (localBrowserModeEnabled()) {
3169
+ try {
3170
+ const { result, exports: exports2 } = await localBrowser.captureFanout(input.session_id, input);
3171
+ return emit(result, exports2);
3172
+ } catch (err) {
3173
+ return errorResult("browser_capture_fanout", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
3174
+ }
3175
+ }
3176
+ const res = await req("POST", `/agent/sessions/${input.session_id}/capture-fanout`, {
3177
+ prompt: input.prompt,
3178
+ wait_ms: input.wait_ms,
3179
+ first_party_domain: input.first_party_domain,
3180
+ export: false
3181
+ });
3182
+ if (!res.ok) return errorResult("browser_capture_fanout", res.data, input.session_id);
3183
+ const hosted = res.data?.result ?? res.data;
3184
+ return emit(hosted, res.data?.exports ?? null);
3185
+ }
3186
+ );
2226
3187
  }
2227
3188
 
2228
3189
  // src/mcp/paa-mcp-server.ts
2229
3190
  var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
2230
- var import_node_fs5 = require("fs");
2231
- var import_node_path7 = require("path");
3191
+ var import_node_fs6 = require("fs");
3192
+ var import_node_path8 = require("path");
2232
3193
 
2233
3194
  // src/mcp/mcp-response-formatter.ts
2234
- var import_node_fs4 = require("fs");
2235
- var import_node_os5 = require("os");
2236
- var import_node_path6 = require("path");
3195
+ var import_node_fs5 = require("fs");
3196
+ var import_node_os6 = require("os");
3197
+ var import_node_path7 = require("path");
2237
3198
 
2238
3199
  // src/errors.ts
2239
3200
  function sanitizeVendorName(message) {
@@ -2369,17 +3330,17 @@ function reportTitle(full) {
2369
3330
  const title = full.split("\n").find((line) => line.startsWith("# "));
2370
3331
  return title?.replace(/^#\s+/, "").trim() || "MCP Scraper Report";
2371
3332
  }
2372
- function outputBaseDir2() {
2373
- return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path6.join)((0, import_node_os5.homedir)(), "Downloads", "mcp-scraper");
3333
+ function outputBaseDir3() {
3334
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path7.join)((0, import_node_os6.homedir)(), "Downloads", "mcp-scraper");
2374
3335
  }
2375
3336
  function saveFullReport(full) {
2376
3337
  if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
2377
- const outDir = outputBaseDir2();
3338
+ const outDir = outputBaseDir3();
2378
3339
  try {
2379
- (0, import_node_fs4.mkdirSync)(outDir, { recursive: true });
3340
+ (0, import_node_fs5.mkdirSync)(outDir, { recursive: true });
2380
3341
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2381
- const file = (0, import_node_path6.join)(outDir, `${stamp}-${slugifyReportName(reportTitle(full))}.md`);
2382
- (0, import_node_fs4.writeFileSync)(file, full, "utf8");
3342
+ const file = (0, import_node_path7.join)(outDir, `${stamp}-${slugifyReportName(reportTitle(full))}.md`);
3343
+ (0, import_node_fs5.writeFileSync)(file, full, "utf8");
2383
3344
  return file;
2384
3345
  } catch {
2385
3346
  return null;
@@ -2388,12 +3349,12 @@ function saveFullReport(full) {
2388
3349
  function persistScreenshotLocally(base64, url) {
2389
3350
  if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
2390
3351
  try {
2391
- const dir = (0, import_node_path6.join)(outputBaseDir2(), "screenshots");
2392
- (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
3352
+ const dir = (0, import_node_path7.join)(outputBaseDir3(), "screenshots");
3353
+ (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
2393
3354
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2394
3355
  const slug = url.replace(/^https?:\/\//, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 60);
2395
- const filePath = (0, import_node_path6.join)(dir, `${stamp}-${slug}.png`);
2396
- (0, import_node_fs4.writeFileSync)(filePath, Buffer.from(base64, "base64"));
3356
+ const filePath = (0, import_node_path7.join)(dir, `${stamp}-${slug}.png`);
3357
+ (0, import_node_fs5.writeFileSync)(filePath, Buffer.from(base64, "base64"));
2397
3358
  return filePath;
2398
3359
  } catch {
2399
3360
  return null;
@@ -4768,8 +5729,8 @@ function localPlanningToolAnnotations(title) {
4768
5729
  }
4769
5730
  function listSavedReports() {
4770
5731
  try {
4771
- const dir = outputBaseDir2();
4772
- return (0, import_node_fs5.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs5.statSync)((0, import_node_path7.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 100);
5732
+ const dir = outputBaseDir3();
5733
+ return (0, import_node_fs6.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs6.statSync)((0, import_node_path8.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 100);
4773
5734
  } catch {
4774
5735
  return [];
4775
5736
  }
@@ -4793,9 +5754,9 @@ function registerSavedReportResources(server2) {
4793
5754
  },
4794
5755
  async (uri, variables) => {
4795
5756
  const requested = Array.isArray(variables.filename) ? variables.filename[0] : variables.filename;
4796
- const filename = (0, import_node_path7.basename)(decodeURIComponent(String(requested ?? "")));
5757
+ const filename = (0, import_node_path8.basename)(decodeURIComponent(String(requested ?? "")));
4797
5758
  if (!filename.endsWith(".md")) throw new Error("Only saved .md reports can be read");
4798
- const text = (0, import_node_fs5.readFileSync)((0, import_node_path7.join)(outputBaseDir2(), filename), "utf8");
5759
+ const text = (0, import_node_fs6.readFileSync)((0, import_node_path8.join)(outputBaseDir3(), filename), "utf8");
4799
5760
  return { contents: [{ uri: uri.href, mimeType: "text/markdown", text }] };
4800
5761
  }
4801
5762
  );
@@ -5081,10 +6042,10 @@ if (!forceStdio && (interactiveTerminal || wantsHelp)) {
5081
6042
  }
5082
6043
  function readApiKeyFile() {
5083
6044
  const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
5084
- const paths = [explicitPath, (0, import_node_path8.join)((0, import_node_os6.homedir)(), ".mcp-scraper-key")].filter(Boolean);
6045
+ const paths = [explicitPath, (0, import_node_path9.join)((0, import_node_os7.homedir)(), ".mcp-scraper-key")].filter(Boolean);
5085
6046
  for (const path of paths) {
5086
6047
  try {
5087
- const value = (0, import_node_fs6.readFileSync)(path, "utf8").trim();
6048
+ const value = (0, import_node_fs7.readFileSync)(path, "utf8").trim();
5088
6049
  if (value) return value;
5089
6050
  } catch {
5090
6051
  }