mcp-scraper 0.2.24 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/api-server.cjs +916 -7
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +1 -1
- package/dist/bin/browser-agent-stdio-server.cjs +989 -28
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
- package/dist/bin/browser-agent-stdio-server.js +3 -2
- package/dist/bin/browser-agent-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs +1008 -47
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +4 -3
- package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-install.cjs +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +1 -1
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/{chunk-TTSW2MHR.js → chunk-55T4SRLJ.js} +2 -2
- package/dist/chunk-D4JDGKOV.js +7 -0
- package/dist/chunk-D4JDGKOV.js.map +1 -0
- package/dist/{chunk-4TNQUYP7.js → chunk-LICHCMV6.js} +131 -3
- package/dist/chunk-LICHCMV6.js.map +1 -0
- package/dist/chunk-NXRWFOEZ.js +816 -0
- package/dist/chunk-NXRWFOEZ.js.map +1 -0
- package/dist/{server-TZ5EMANS.js → server-AXPNL2RV.js} +40 -3
- package/dist/{server-TZ5EMANS.js.map → server-AXPNL2RV.js.map} +1 -1
- package/docs/mcp-tool-manifest.generated.json +39 -7
- package/package.json +1 -1
- package/dist/chunk-4TNQUYP7.js.map +0 -1
- package/dist/chunk-ISIITOAL.js +0 -7
- package/dist/chunk-ISIITOAL.js.map +0 -1
- /package/dist/{chunk-TTSW2MHR.js.map → chunk-55T4SRLJ.js.map} +0 -0
package/dist/bin/api-server.cjs
CHANGED
|
@@ -10723,10 +10723,10 @@ function buildTranscriptMarkdown(result) {
|
|
|
10723
10723
|
return lines.join("\n");
|
|
10724
10724
|
}
|
|
10725
10725
|
function buildTranscriptHtml(result) {
|
|
10726
|
-
const
|
|
10726
|
+
const esc2 = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
10727
10727
|
const fmtSecs = (n) => `${Math.floor(n / 60)}:${String(Math.floor(n % 60)).padStart(2, "0")}`;
|
|
10728
10728
|
const chunks = (result.chunks ?? []).map(
|
|
10729
|
-
(ch) => `<div class="chunk"><span class="ts">${fmtSecs(ch.timestamp[0])} \u2192 ${fmtSecs(ch.timestamp[1])}</span><span class="ct">${
|
|
10729
|
+
(ch) => `<div class="chunk"><span class="ts">${fmtSecs(ch.timestamp[0])} \u2192 ${fmtSecs(ch.timestamp[1])}</span><span class="ct">${esc2(ch.text.trim())}</span></div>`
|
|
10730
10730
|
).join("\n");
|
|
10731
10731
|
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
10732
10732
|
${ATTRIBUTION_PIXEL_SCRIPT}
|
|
@@ -10737,10 +10737,10 @@ h1{font-size:1.2rem;margin-bottom:.25rem}.meta{color:#666;font-size:.85rem;margi
|
|
|
10737
10737
|
.ts{font-family:monospace;font-size:.75rem;color:#888;white-space:nowrap;flex-shrink:0}
|
|
10738
10738
|
.ct{font-size:.875rem;flex:1;min-width:0;word-break:break-word}
|
|
10739
10739
|
</style></head><body>
|
|
10740
|
-
<h1>Transcript \u2014 ${
|
|
10740
|
+
<h1>Transcript \u2014 ${esc2(result.videoId)}</h1>
|
|
10741
10741
|
<div class="meta">Transcribed in ${(result.durationMs / 1e3).toFixed(1)}s</div>
|
|
10742
10742
|
<h2 style="font-size:1.05rem">Full Text</h2>
|
|
10743
|
-
<div class="full-text">${
|
|
10743
|
+
<div class="full-text">${esc2(result.text)}</div>
|
|
10744
10744
|
${chunks.length ? `<h2 style="font-size:1.05rem">Timestamped Segments</h2>
|
|
10745
10745
|
${chunks}` : ""}
|
|
10746
10746
|
</body></html>`;
|
|
@@ -21002,7 +21002,7 @@ var PACKAGE_VERSION;
|
|
|
21002
21002
|
var init_version = __esm({
|
|
21003
21003
|
"src/version.ts"() {
|
|
21004
21004
|
"use strict";
|
|
21005
|
-
PACKAGE_VERSION = "0.
|
|
21005
|
+
PACKAGE_VERSION = "0.3.1";
|
|
21006
21006
|
}
|
|
21007
21007
|
});
|
|
21008
21008
|
|
|
@@ -22696,6 +22696,880 @@ var init_browser_agent_db = __esm({
|
|
|
22696
22696
|
}
|
|
22697
22697
|
});
|
|
22698
22698
|
|
|
22699
|
+
// src/services/fanout/cdp-capture.ts
|
|
22700
|
+
var ANSWER_RE, FanoutCdpCapture;
|
|
22701
|
+
var init_cdp_capture = __esm({
|
|
22702
|
+
"src/services/fanout/cdp-capture.ts"() {
|
|
22703
|
+
"use strict";
|
|
22704
|
+
ANSWER_RE = /\/backend-api\/(f\/)?conversation$|\/chat_conversations\/[^/]+\/(completion|retry_completion)$|\/completion$/i;
|
|
22705
|
+
FanoutCdpCapture = class {
|
|
22706
|
+
client = null;
|
|
22707
|
+
reqUrl = /* @__PURE__ */ new Map();
|
|
22708
|
+
ctById = /* @__PURE__ */ new Map();
|
|
22709
|
+
answerIds = /* @__PURE__ */ new Set();
|
|
22710
|
+
bodies = [];
|
|
22711
|
+
pending = [];
|
|
22712
|
+
answerStreams = 0;
|
|
22713
|
+
async attach(page) {
|
|
22714
|
+
if (this.client) return;
|
|
22715
|
+
const client2 = await page.context().newCDPSession(page);
|
|
22716
|
+
this.client = client2;
|
|
22717
|
+
await client2.send("Network.enable", { maxTotalBufferSize: 2e8, maxResourceBufferSize: 2e8 });
|
|
22718
|
+
client2.on("Network.requestWillBeSent", (e) => {
|
|
22719
|
+
const id = e.requestId;
|
|
22720
|
+
const req = e.request;
|
|
22721
|
+
if (id) this.reqUrl.set(id, req?.url || "");
|
|
22722
|
+
});
|
|
22723
|
+
client2.on("Network.responseReceived", (e) => {
|
|
22724
|
+
const id = e.requestId;
|
|
22725
|
+
const res = e.response;
|
|
22726
|
+
const url = res?.url || this.reqUrl.get(id) || "";
|
|
22727
|
+
const headers = res?.headers || {};
|
|
22728
|
+
const ct = String(headers["content-type"] || headers["Content-Type"] || "");
|
|
22729
|
+
if (id) this.ctById.set(id, ct);
|
|
22730
|
+
if (id && (this.isAnswerUrl(url) || /event-stream/i.test(ct))) this.answerIds.add(id);
|
|
22731
|
+
});
|
|
22732
|
+
client2.on("Network.loadingFinished", (e) => this.grab(e.requestId));
|
|
22733
|
+
}
|
|
22734
|
+
reset() {
|
|
22735
|
+
this.bodies = [];
|
|
22736
|
+
this.answerStreams = 0;
|
|
22737
|
+
this.answerIds.clear();
|
|
22738
|
+
this.pending = [];
|
|
22739
|
+
}
|
|
22740
|
+
answerStreamCount() {
|
|
22741
|
+
return this.answerStreams;
|
|
22742
|
+
}
|
|
22743
|
+
async drain() {
|
|
22744
|
+
await Promise.all(this.pending);
|
|
22745
|
+
}
|
|
22746
|
+
bestAnswerText() {
|
|
22747
|
+
const eventStreams = this.bodies.filter((b) => /event-stream/i.test(b.ct));
|
|
22748
|
+
const answerStreams = eventStreams.filter((b) => this.isAnswerUrl(b.url)).sort((a, b) => b.text.length - a.text.length);
|
|
22749
|
+
if (answerStreams.length) return answerStreams.map((b) => b.text).join("\n");
|
|
22750
|
+
const answerJson = this.bodies.filter((b) => this.isAnswerUrl(b.url)).sort((a, b) => b.text.length - a.text.length);
|
|
22751
|
+
if (answerJson.length) return answerJson[0].text;
|
|
22752
|
+
if (eventStreams.length) return eventStreams.sort((a, b) => b.text.length - a.text.length)[0].text;
|
|
22753
|
+
return "";
|
|
22754
|
+
}
|
|
22755
|
+
async detach() {
|
|
22756
|
+
try {
|
|
22757
|
+
await this.client?.detach();
|
|
22758
|
+
} catch {
|
|
22759
|
+
this.client = null;
|
|
22760
|
+
}
|
|
22761
|
+
this.client = null;
|
|
22762
|
+
}
|
|
22763
|
+
isAnswerUrl(url) {
|
|
22764
|
+
let path6 = "";
|
|
22765
|
+
try {
|
|
22766
|
+
path6 = new URL(url).pathname;
|
|
22767
|
+
} catch {
|
|
22768
|
+
path6 = String(url || "").split("?")[0];
|
|
22769
|
+
}
|
|
22770
|
+
return ANSWER_RE.test(path6);
|
|
22771
|
+
}
|
|
22772
|
+
grab(requestId) {
|
|
22773
|
+
if (!requestId || !this.client || !this.answerIds.has(requestId)) return;
|
|
22774
|
+
const url = this.reqUrl.get(requestId) || "";
|
|
22775
|
+
const ct = this.ctById.get(requestId) || "";
|
|
22776
|
+
const client2 = this.client;
|
|
22777
|
+
const p = client2.send("Network.getResponseBody", { requestId }).then((r) => {
|
|
22778
|
+
const text = r.base64Encoded ? Buffer.from(String(r.body), "base64").toString("utf8") : String(r.body || "");
|
|
22779
|
+
if (text) {
|
|
22780
|
+
this.bodies.push({ url, ct, text });
|
|
22781
|
+
if (this.isAnswerUrl(url)) this.answerStreams++;
|
|
22782
|
+
}
|
|
22783
|
+
}).catch(() => void 0);
|
|
22784
|
+
this.pending.push(p);
|
|
22785
|
+
}
|
|
22786
|
+
};
|
|
22787
|
+
}
|
|
22788
|
+
});
|
|
22789
|
+
|
|
22790
|
+
// src/services/fanout/parse-helpers.ts
|
|
22791
|
+
function domainOf(u) {
|
|
22792
|
+
try {
|
|
22793
|
+
return new URL(u).hostname.replace(/^www\./, "");
|
|
22794
|
+
} catch {
|
|
22795
|
+
return "";
|
|
22796
|
+
}
|
|
22797
|
+
}
|
|
22798
|
+
function normUrl(u) {
|
|
22799
|
+
try {
|
|
22800
|
+
const x = new URL(u);
|
|
22801
|
+
x.hash = "";
|
|
22802
|
+
for (const k of [...x.searchParams.keys()]) {
|
|
22803
|
+
if (TRACK.test(k)) x.searchParams.delete(k);
|
|
22804
|
+
}
|
|
22805
|
+
return x.toString().replace(/\/$/, "").toLowerCase();
|
|
22806
|
+
} catch {
|
|
22807
|
+
return String(u || "").toLowerCase();
|
|
22808
|
+
}
|
|
22809
|
+
}
|
|
22810
|
+
function isExternal(u) {
|
|
22811
|
+
const h = domainOf(u);
|
|
22812
|
+
return Boolean(h) && !INTERNAL.test(h);
|
|
22813
|
+
}
|
|
22814
|
+
function unesc(s) {
|
|
22815
|
+
try {
|
|
22816
|
+
return JSON.parse('"' + s + '"');
|
|
22817
|
+
} catch {
|
|
22818
|
+
return String(s).replace(/\\(.)/g, "$1");
|
|
22819
|
+
}
|
|
22820
|
+
}
|
|
22821
|
+
function stripCitationMarkers(s) {
|
|
22822
|
+
return String(s || "").replace(/[-]/g, "").replace(/cite(?:turn\d+\w+?\d+)+/g, "").replace(/\s+/g, " ").trim();
|
|
22823
|
+
}
|
|
22824
|
+
function sliceBalanced(text, openIdx) {
|
|
22825
|
+
const open = text[openIdx];
|
|
22826
|
+
const close = open === "[" ? "]" : "}";
|
|
22827
|
+
let depth = 0;
|
|
22828
|
+
let inStr = false;
|
|
22829
|
+
let esc2 = false;
|
|
22830
|
+
for (let i = openIdx; i < text.length; i++) {
|
|
22831
|
+
const ch = text[i];
|
|
22832
|
+
if (inStr) {
|
|
22833
|
+
if (esc2) esc2 = false;
|
|
22834
|
+
else if (ch === "\\") esc2 = true;
|
|
22835
|
+
else if (ch === '"') inStr = false;
|
|
22836
|
+
continue;
|
|
22837
|
+
}
|
|
22838
|
+
if (ch === '"') {
|
|
22839
|
+
inStr = true;
|
|
22840
|
+
continue;
|
|
22841
|
+
}
|
|
22842
|
+
if (ch === open) depth++;
|
|
22843
|
+
else if (ch === close) {
|
|
22844
|
+
depth--;
|
|
22845
|
+
if (depth === 0) return text.slice(openIdx, i + 1);
|
|
22846
|
+
}
|
|
22847
|
+
}
|
|
22848
|
+
return null;
|
|
22849
|
+
}
|
|
22850
|
+
function findJsonValues(text, field) {
|
|
22851
|
+
const out = [];
|
|
22852
|
+
const re = new RegExp('"' + field + '"\\s*:\\s*(\\[|\\{)', "g");
|
|
22853
|
+
let m;
|
|
22854
|
+
while (m = re.exec(text)) {
|
|
22855
|
+
const openIdx = m.index + m[0].length - 1;
|
|
22856
|
+
const slice = sliceBalanced(text, openIdx);
|
|
22857
|
+
if (slice) {
|
|
22858
|
+
try {
|
|
22859
|
+
out.push(JSON.parse(slice));
|
|
22860
|
+
} catch {
|
|
22861
|
+
continue;
|
|
22862
|
+
}
|
|
22863
|
+
}
|
|
22864
|
+
}
|
|
22865
|
+
return out;
|
|
22866
|
+
}
|
|
22867
|
+
function stringsInArrayBody(arrBody) {
|
|
22868
|
+
const out = [];
|
|
22869
|
+
const re = /"((?:[^"\\]|\\.){2,300}?)"/g;
|
|
22870
|
+
let m;
|
|
22871
|
+
while (m = re.exec(arrBody)) out.push(unesc(m[1]));
|
|
22872
|
+
return out;
|
|
22873
|
+
}
|
|
22874
|
+
function cleanQuery(q) {
|
|
22875
|
+
const t = (q || "").trim();
|
|
22876
|
+
if (t.length < 2 || t.length > 300) return null;
|
|
22877
|
+
return t;
|
|
22878
|
+
}
|
|
22879
|
+
var INTERNAL, TRACK;
|
|
22880
|
+
var init_parse_helpers = __esm({
|
|
22881
|
+
"src/services/fanout/parse-helpers.ts"() {
|
|
22882
|
+
"use strict";
|
|
22883
|
+
INTERNAL = /chatgpt\.com|openai\.com|oaiusercontent|oaistatic|claude\.ai|anthropic\.com/i;
|
|
22884
|
+
TRACK = /^(utm_|oai|fbclid|gclid|ref$|ref_|sa$|usg$|ved$|sca_esv$|sources$)/i;
|
|
22885
|
+
}
|
|
22886
|
+
});
|
|
22887
|
+
|
|
22888
|
+
// src/services/fanout/adapters/chatgpt.ts
|
|
22889
|
+
function roundOf(refId) {
|
|
22890
|
+
const m = /^turn(\d+)/.exec(refId || "");
|
|
22891
|
+
return m ? Number(m[1]) : null;
|
|
22892
|
+
}
|
|
22893
|
+
function normRef(r) {
|
|
22894
|
+
if (typeof r === "string") {
|
|
22895
|
+
const m = /turn(\d+)(search|news|ref|video|image|product)(\d+)/.exec(r);
|
|
22896
|
+
return m ? `turn${m[1]}${m[2]}${m[3]}` : null;
|
|
22897
|
+
}
|
|
22898
|
+
if (r && typeof r === "object") {
|
|
22899
|
+
const o = r;
|
|
22900
|
+
if (o.turn_index != null) {
|
|
22901
|
+
return `turn${o.turn_index}${o.ref_type || "search"}${o.ref_index != null ? o.ref_index : ""}`;
|
|
22902
|
+
}
|
|
22903
|
+
}
|
|
22904
|
+
return null;
|
|
22905
|
+
}
|
|
22906
|
+
function refsInText(s) {
|
|
22907
|
+
return (String(s || "").match(/turn\d+(?:search|news|ref|video|image|product)\d+/g) || []).map(normRef).filter((x) => Boolean(x));
|
|
22908
|
+
}
|
|
22909
|
+
function collectInto(arr, v) {
|
|
22910
|
+
if (Array.isArray(v)) arr.push(...v);
|
|
22911
|
+
else if (v && typeof v === "object") arr.push(v);
|
|
22912
|
+
}
|
|
22913
|
+
function reconstructFromSsePatches(raw) {
|
|
22914
|
+
const contentReferences = [];
|
|
22915
|
+
const searchResultGroups = [];
|
|
22916
|
+
const citations = [];
|
|
22917
|
+
const citedUrls = /* @__PURE__ */ new Set();
|
|
22918
|
+
const state = { found: false };
|
|
22919
|
+
const handleOp = (op) => {
|
|
22920
|
+
if (!op || typeof op !== "object") return;
|
|
22921
|
+
const o = op;
|
|
22922
|
+
const v = o.v;
|
|
22923
|
+
if (o.o === "patch" && Array.isArray(v)) {
|
|
22924
|
+
state.found = true;
|
|
22925
|
+
v.forEach(handleOp);
|
|
22926
|
+
return;
|
|
22927
|
+
}
|
|
22928
|
+
const path6 = typeof o.p === "string" ? o.p : "";
|
|
22929
|
+
if ((path6 === "" || path6 === "/message") && v && typeof v === "object") {
|
|
22930
|
+
const root = v;
|
|
22931
|
+
const msg = root.message || root;
|
|
22932
|
+
const md = msg && msg.metadata;
|
|
22933
|
+
if (md) {
|
|
22934
|
+
collectInto(contentReferences, md.content_references);
|
|
22935
|
+
collectInto(searchResultGroups, md.search_result_groups);
|
|
22936
|
+
collectInto(citations, md.citations);
|
|
22937
|
+
state.found = true;
|
|
22938
|
+
}
|
|
22939
|
+
}
|
|
22940
|
+
if (!path6) return;
|
|
22941
|
+
if (/\/safe_urls$/.test(path6) && Array.isArray(v)) {
|
|
22942
|
+
for (const u of v) if (typeof u === "string") citedUrls.add(u);
|
|
22943
|
+
state.found = true;
|
|
22944
|
+
} else if (path6.includes("content_references")) {
|
|
22945
|
+
collectInto(contentReferences, v);
|
|
22946
|
+
state.found = true;
|
|
22947
|
+
} else if (path6.includes("search_result_groups")) {
|
|
22948
|
+
collectInto(searchResultGroups, v);
|
|
22949
|
+
state.found = true;
|
|
22950
|
+
} else if (path6.includes("citations")) {
|
|
22951
|
+
collectInto(citations, v);
|
|
22952
|
+
state.found = true;
|
|
22953
|
+
}
|
|
22954
|
+
};
|
|
22955
|
+
for (const line of raw.split("\n")) {
|
|
22956
|
+
const s = (line.startsWith("data:") ? line.slice(5) : line).trim();
|
|
22957
|
+
if (!s || s === "[DONE]" || s[0] !== "{" && s[0] !== "[") continue;
|
|
22958
|
+
let parsed;
|
|
22959
|
+
try {
|
|
22960
|
+
parsed = JSON.parse(s);
|
|
22961
|
+
} catch {
|
|
22962
|
+
continue;
|
|
22963
|
+
}
|
|
22964
|
+
const ops = Array.isArray(parsed) ? parsed : [parsed];
|
|
22965
|
+
ops.forEach(handleOp);
|
|
22966
|
+
}
|
|
22967
|
+
if (!state.found) return raw;
|
|
22968
|
+
const citedRefs = [...citedUrls].map((url) => ({ type: "grouped_webpages", items: [{ url }] }));
|
|
22969
|
+
const synthetic = JSON.stringify({
|
|
22970
|
+
content_references: [...contentReferences, ...citedRefs],
|
|
22971
|
+
search_result_groups: searchResultGroups,
|
|
22972
|
+
citations
|
|
22973
|
+
});
|
|
22974
|
+
return raw + "\n" + synthetic;
|
|
22975
|
+
}
|
|
22976
|
+
var chatgptAdapter;
|
|
22977
|
+
var init_chatgpt = __esm({
|
|
22978
|
+
"src/services/fanout/adapters/chatgpt.ts"() {
|
|
22979
|
+
"use strict";
|
|
22980
|
+
init_parse_helpers();
|
|
22981
|
+
chatgptAdapter = {
|
|
22982
|
+
platform: "ChatGPT",
|
|
22983
|
+
matchesHost(hostname) {
|
|
22984
|
+
return /(^|\.)chatgpt\.com$|(^|\.)openai\.com$/i.test(hostname);
|
|
22985
|
+
},
|
|
22986
|
+
parse(rawInput, prompt) {
|
|
22987
|
+
const raw = reconstructFromSsePatches(rawInput);
|
|
22988
|
+
const sources = /* @__PURE__ */ new Map();
|
|
22989
|
+
const snippets = /* @__PURE__ */ new Map();
|
|
22990
|
+
const queries = /* @__PURE__ */ new Set();
|
|
22991
|
+
const unmapped = /* @__PURE__ */ new Set();
|
|
22992
|
+
const meta = { model: "", finishType: "", title: "", rounds: 0 };
|
|
22993
|
+
const ensure = (url) => {
|
|
22994
|
+
if (!isExternal(url)) return null;
|
|
22995
|
+
const k = normUrl(url);
|
|
22996
|
+
let s = sources.get(k);
|
|
22997
|
+
if (!s) {
|
|
22998
|
+
s = { url, domain: domainOf(url), title: "", cited: false, timesCited: 0, firstOrder: Infinity, snippet: "", round: null };
|
|
22999
|
+
sources.set(k, s);
|
|
23000
|
+
}
|
|
23001
|
+
return s;
|
|
23002
|
+
};
|
|
23003
|
+
const addSnippet = (url, title, text) => {
|
|
23004
|
+
const clean2 = stripCitationMarkers(text);
|
|
23005
|
+
if (!isExternal(url) || clean2.length < 2) return;
|
|
23006
|
+
snippets.set(normUrl(url), { url, domain: domainOf(url), title: title || domainOf(url), text: clean2.slice(0, 400) });
|
|
23007
|
+
};
|
|
23008
|
+
for (const m of raw.matchAll(/"([a-z_]*(?:quer|search)[a-z_]*)"\s*:/gi)) unmapped.add(m[1]);
|
|
23009
|
+
for (const m of raw.matchAll(/"search_model_queries"\s*:\s*\[([^\]]*)\]/g)) {
|
|
23010
|
+
for (const q of stringsInArrayBody(m[1])) {
|
|
23011
|
+
const c = cleanQuery(q);
|
|
23012
|
+
if (c) queries.add(c);
|
|
23013
|
+
}
|
|
23014
|
+
}
|
|
23015
|
+
for (const m of raw.matchAll(/"(?:search_queries|queries|search_query)"\s*:\s*\[([^\]]*)\]/g)) {
|
|
23016
|
+
for (const q of stringsInArrayBody(m[1])) {
|
|
23017
|
+
const c = cleanQuery(q);
|
|
23018
|
+
if (c) queries.add(c);
|
|
23019
|
+
}
|
|
23020
|
+
}
|
|
23021
|
+
for (const v of findJsonValues(raw, "search_result_groups")) {
|
|
23022
|
+
if (!Array.isArray(v)) continue;
|
|
23023
|
+
for (const g of v) {
|
|
23024
|
+
const entries = g && typeof g === "object" && Array.isArray(g.entries) ? g.entries : [];
|
|
23025
|
+
for (const e of entries) {
|
|
23026
|
+
const url = typeof e.url === "string" ? e.url : "";
|
|
23027
|
+
const s = ensure(url);
|
|
23028
|
+
if (!s) continue;
|
|
23029
|
+
if (!s.title && typeof e.title === "string") s.title = e.title.slice(0, 200);
|
|
23030
|
+
const ref = normRef(e.ref_id);
|
|
23031
|
+
if (ref && s.round == null) s.round = roundOf(ref);
|
|
23032
|
+
if (typeof e.snippet === "string" && e.snippet) addSnippet(url, s.title, e.snippet);
|
|
23033
|
+
}
|
|
23034
|
+
}
|
|
23035
|
+
}
|
|
23036
|
+
for (const m of raw.matchAll(/"url"\s*:\s*"(https?:\/\/[^"\\]+)"(?:[\s\S]{0,120}?"(?:title|name)"\s*:\s*"((?:[^"\\]|\\.){0,200})")?/g)) {
|
|
23037
|
+
const s = ensure(m[1]);
|
|
23038
|
+
if (s && !s.title && m[2]) s.title = unesc(m[2]).slice(0, 200);
|
|
23039
|
+
}
|
|
23040
|
+
let spanTotal = 0;
|
|
23041
|
+
for (const cr of findJsonValues(raw, "content_references")) {
|
|
23042
|
+
if (!Array.isArray(cr)) continue;
|
|
23043
|
+
for (const ref of cr) {
|
|
23044
|
+
const type = typeof ref.type === "string" ? ref.type : "";
|
|
23045
|
+
const isBundle = /sources_footnote|nav_list|alt_text/.test(type);
|
|
23046
|
+
const refs = refsInText(typeof ref.matched_text === "string" ? ref.matched_text : "");
|
|
23047
|
+
if (!isBundle) spanTotal++;
|
|
23048
|
+
const ord = typeof ref.start_idx === "number" ? ref.start_idx : isBundle ? Infinity : spanTotal;
|
|
23049
|
+
const items = Array.isArray(ref.items) ? ref.items : [];
|
|
23050
|
+
items.forEach((it, idx) => {
|
|
23051
|
+
const refForItem = refs.length === items.length ? refs[idx] : refs[0];
|
|
23052
|
+
const urls = [it.url].concat((Array.isArray(it.supporting_websites) ? it.supporting_websites : []).map((s) => s?.url)).filter((u) => typeof u === "string");
|
|
23053
|
+
for (const u of urls) {
|
|
23054
|
+
const s = ensure(u);
|
|
23055
|
+
if (!s) continue;
|
|
23056
|
+
if (!isBundle) {
|
|
23057
|
+
s.cited = true;
|
|
23058
|
+
s.timesCited++;
|
|
23059
|
+
if (ord < s.firstOrder) s.firstOrder = ord;
|
|
23060
|
+
} else if (!s.cited) {
|
|
23061
|
+
s.cited = true;
|
|
23062
|
+
}
|
|
23063
|
+
if (refForItem && s.round == null) s.round = roundOf(refForItem);
|
|
23064
|
+
}
|
|
23065
|
+
if (typeof it.url === "string" && typeof it.snippet === "string") addSnippet(it.url, typeof it.title === "string" ? it.title : "", it.snippet);
|
|
23066
|
+
});
|
|
23067
|
+
}
|
|
23068
|
+
}
|
|
23069
|
+
for (const v of findJsonValues(raw, "citations")) {
|
|
23070
|
+
if (!Array.isArray(v)) continue;
|
|
23071
|
+
v.forEach((c, i) => {
|
|
23072
|
+
const md = c && typeof c === "object" ? c.metadata : null;
|
|
23073
|
+
if (!md || typeof md.url !== "string") return;
|
|
23074
|
+
const s = ensure(md.url);
|
|
23075
|
+
if (!s) return;
|
|
23076
|
+
s.cited = true;
|
|
23077
|
+
s.timesCited++;
|
|
23078
|
+
const ord = typeof c.start_ix === "number" ? c.start_ix : i;
|
|
23079
|
+
if (ord < s.firstOrder) s.firstOrder = ord;
|
|
23080
|
+
if (typeof md.text === "string") addSnippet(md.url, typeof md.title === "string" ? md.title : "", md.text);
|
|
23081
|
+
});
|
|
23082
|
+
}
|
|
23083
|
+
for (const m of raw.matchAll(/"content_type"\s*:\s*"tether_quote"([\s\S]{0,4000}?)"text"\s*:\s*"((?:[^"\\]|\\.)*)"/g)) {
|
|
23084
|
+
const url = (m[1].match(/"url"\s*:\s*"([^"]+)"/) || [])[1];
|
|
23085
|
+
const title = (m[1].match(/"title"\s*:\s*"((?:[^"\\]|\\.)*)"/) || [])[1];
|
|
23086
|
+
if (url) {
|
|
23087
|
+
ensure(url);
|
|
23088
|
+
addSnippet(url, title ? unesc(title) : "", unesc(m[2]));
|
|
23089
|
+
}
|
|
23090
|
+
}
|
|
23091
|
+
const modelMatches = [...raw.matchAll(/"resolved_model_slug"\s*:\s*"([^"]+)"/g)];
|
|
23092
|
+
const slugMatches = modelMatches.length ? modelMatches : [...raw.matchAll(/"model_slug"\s*:\s*"([^"]+)"/g)];
|
|
23093
|
+
if (slugMatches.length) meta.model = slugMatches[slugMatches.length - 1][1];
|
|
23094
|
+
const finish = raw.match(/"finish_details"\s*:\s*\{[^}]*"type"\s*:\s*"([^"]+)"/);
|
|
23095
|
+
if (finish) meta.finishType = finish[1];
|
|
23096
|
+
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;
|
|
23097
|
+
const sourceRounds = /* @__PURE__ */ new Set();
|
|
23098
|
+
for (const s of sources.values()) if (s.round != null) sourceRounds.add(s.round);
|
|
23099
|
+
meta.rounds = Math.max(searchRounds, sourceRounds.size);
|
|
23100
|
+
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) => ({
|
|
23101
|
+
url: s.url,
|
|
23102
|
+
domain: s.domain,
|
|
23103
|
+
title: s.title || s.domain,
|
|
23104
|
+
cited: s.cited,
|
|
23105
|
+
timesCited: s.timesCited,
|
|
23106
|
+
snippet: snippets.get(normUrl(s.url))?.text || "",
|
|
23107
|
+
round: s.round
|
|
23108
|
+
}));
|
|
23109
|
+
return {
|
|
23110
|
+
subQueries: [...queries],
|
|
23111
|
+
sources: ordered,
|
|
23112
|
+
snippets: [...snippets.values()],
|
|
23113
|
+
meta,
|
|
23114
|
+
unmappedKeys: [...unmapped].slice(0, 50)
|
|
23115
|
+
};
|
|
23116
|
+
}
|
|
23117
|
+
};
|
|
23118
|
+
}
|
|
23119
|
+
});
|
|
23120
|
+
|
|
23121
|
+
// src/services/fanout/adapters/claude.ts
|
|
23122
|
+
function isFavicon(u) {
|
|
23123
|
+
return /\/s2\/favicons/i.test(u) || /favicon/i.test(u);
|
|
23124
|
+
}
|
|
23125
|
+
var claudeAdapter;
|
|
23126
|
+
var init_claude = __esm({
|
|
23127
|
+
"src/services/fanout/adapters/claude.ts"() {
|
|
23128
|
+
"use strict";
|
|
23129
|
+
init_parse_helpers();
|
|
23130
|
+
claudeAdapter = {
|
|
23131
|
+
platform: "Claude",
|
|
23132
|
+
matchesHost(hostname) {
|
|
23133
|
+
return /(^|\.)claude\.ai$/i.test(hostname);
|
|
23134
|
+
},
|
|
23135
|
+
parse(raw, prompt) {
|
|
23136
|
+
const sources = /* @__PURE__ */ new Map();
|
|
23137
|
+
const snippets = /* @__PURE__ */ new Map();
|
|
23138
|
+
const queries = /* @__PURE__ */ new Set();
|
|
23139
|
+
const unmapped = /* @__PURE__ */ new Set();
|
|
23140
|
+
const meta = { model: "", finishType: "", title: "", rounds: 0 };
|
|
23141
|
+
const ensure = (url) => {
|
|
23142
|
+
if (!url || !isExternal(url) || isFavicon(url)) return null;
|
|
23143
|
+
const k = normUrl(url);
|
|
23144
|
+
let s = sources.get(k);
|
|
23145
|
+
if (!s) {
|
|
23146
|
+
s = { url, domain: domainOf(url), title: "", cited: false, timesCited: 0, snippet: "", round: null };
|
|
23147
|
+
sources.set(k, s);
|
|
23148
|
+
}
|
|
23149
|
+
return s;
|
|
23150
|
+
};
|
|
23151
|
+
const toolInputs = /* @__PURE__ */ new Map();
|
|
23152
|
+
const searchBlocks = /* @__PURE__ */ new Set();
|
|
23153
|
+
const finalizeTool = (index) => {
|
|
23154
|
+
const t = toolInputs.get(index);
|
|
23155
|
+
if (!t || !/web_search|web_fetch|search|browse/i.test(t.name)) return;
|
|
23156
|
+
try {
|
|
23157
|
+
const input = JSON.parse(t.buf);
|
|
23158
|
+
if (typeof input.query === "string") {
|
|
23159
|
+
const q = cleanQuery(input.query);
|
|
23160
|
+
if (q) queries.add(q);
|
|
23161
|
+
}
|
|
23162
|
+
} catch {
|
|
23163
|
+
const m = t.buf.match(/"query"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
23164
|
+
if (m) {
|
|
23165
|
+
const q = cleanQuery(unesc(m[1]));
|
|
23166
|
+
if (q) queries.add(q);
|
|
23167
|
+
}
|
|
23168
|
+
}
|
|
23169
|
+
};
|
|
23170
|
+
for (const line of raw.split("\n")) {
|
|
23171
|
+
const s = (line.startsWith("data:") ? line.slice(5) : line).trim();
|
|
23172
|
+
if (!s || s[0] !== "{" && s[0] !== "[") continue;
|
|
23173
|
+
let ev;
|
|
23174
|
+
try {
|
|
23175
|
+
ev = JSON.parse(s);
|
|
23176
|
+
} catch {
|
|
23177
|
+
continue;
|
|
23178
|
+
}
|
|
23179
|
+
const type = ev.type;
|
|
23180
|
+
if (type === "content_block_start") {
|
|
23181
|
+
const cb = ev.content_block || {};
|
|
23182
|
+
const index = typeof ev.index === "number" ? ev.index : -1;
|
|
23183
|
+
if (cb.type === "tool_use" && index >= 0) {
|
|
23184
|
+
const name = typeof cb.name === "string" ? cb.name : "";
|
|
23185
|
+
toolInputs.set(index, { name, buf: "" });
|
|
23186
|
+
if (/web_search|web_fetch|search|browse/i.test(name)) searchBlocks.add(index);
|
|
23187
|
+
}
|
|
23188
|
+
} else if (type === "content_block_delta") {
|
|
23189
|
+
const d = ev.delta || {};
|
|
23190
|
+
const index = typeof ev.index === "number" ? ev.index : -1;
|
|
23191
|
+
if (d.type === "input_json_delta" && typeof d.partial_json === "string") {
|
|
23192
|
+
const t = toolInputs.get(index);
|
|
23193
|
+
if (t) t.buf += d.partial_json;
|
|
23194
|
+
} else if (d.type === "citation_start_delta") {
|
|
23195
|
+
const c = d.citation || {};
|
|
23196
|
+
if (typeof c.url === "string") {
|
|
23197
|
+
const src = ensure(c.url);
|
|
23198
|
+
if (src) {
|
|
23199
|
+
src.cited = true;
|
|
23200
|
+
src.timesCited++;
|
|
23201
|
+
if (!src.title && typeof c.title === "string") src.title = c.title.slice(0, 200);
|
|
23202
|
+
if (typeof c.cited_text === "string" && c.cited_text) {
|
|
23203
|
+
snippets.set(normUrl(c.url), { url: c.url, domain: domainOf(c.url), title: src.title, text: String(c.cited_text).slice(0, 400) });
|
|
23204
|
+
}
|
|
23205
|
+
}
|
|
23206
|
+
}
|
|
23207
|
+
}
|
|
23208
|
+
} else if (type === "content_block_stop") {
|
|
23209
|
+
const index = typeof ev.index === "number" ? ev.index : -1;
|
|
23210
|
+
if (index >= 0) finalizeTool(index);
|
|
23211
|
+
} else if (type === "message_delta" || type === "message_stop" || type === "message_start") {
|
|
23212
|
+
const d = ev.delta || ev.message || {};
|
|
23213
|
+
if (typeof d.stop_reason === "string" && !meta.finishType) meta.finishType = d.stop_reason;
|
|
23214
|
+
if (typeof d.model === "string" && !meta.model) meta.model = d.model;
|
|
23215
|
+
}
|
|
23216
|
+
}
|
|
23217
|
+
for (const index of searchBlocks) finalizeTool(index);
|
|
23218
|
+
for (const m of raw.matchAll(/"title"\s*:\s*"((?:[^"\\]|\\.){1,200})"\s*,\s*"url"\s*:\s*"(https?:\/\/[^"\\]+)"/g)) {
|
|
23219
|
+
const src = ensure(m[2]);
|
|
23220
|
+
if (src && !src.title) src.title = unesc(m[1]).slice(0, 200);
|
|
23221
|
+
}
|
|
23222
|
+
for (const m of raw.matchAll(/"url"\s*:\s*"(https?:\/\/[^"\\]+)"/g)) ensure(m[1]);
|
|
23223
|
+
for (const m of raw.matchAll(/\\"title\\"\s*:\s*\\"((?:[^"\\]|\\.){1,160}?)\\"[\s\S]{0,160}?\\"url\\"\s*:\s*\\"(https?:[^"\\]+)\\"/g)) {
|
|
23224
|
+
const src = ensure(m[2]);
|
|
23225
|
+
if (src && !src.title) src.title = unesc(unesc(m[1])).slice(0, 200);
|
|
23226
|
+
}
|
|
23227
|
+
for (const m of raw.matchAll(/\\"url\\"\s*:\s*\\"(https?:[^"\\]+)\\"/g)) ensure(m[1]);
|
|
23228
|
+
if (!meta.model) {
|
|
23229
|
+
const mm = raw.match(/"model"\s*:\s*"(claude[^"]+)"/);
|
|
23230
|
+
if (mm) meta.model = mm[1];
|
|
23231
|
+
}
|
|
23232
|
+
meta.rounds = queries.size > 0 ? searchBlocks.size : 0;
|
|
23233
|
+
for (const m of raw.matchAll(/"([a-z_]*(?:quer|search)[a-z_]*)"\s*:/gi)) unmapped.add(m[1]);
|
|
23234
|
+
const ordered = [...sources.values()].sort((a, b) => a.cited === b.cited ? b.timesCited - a.timesCited : a.cited ? -1 : 1).map((s) => ({
|
|
23235
|
+
url: s.url,
|
|
23236
|
+
domain: s.domain,
|
|
23237
|
+
title: s.title || s.domain,
|
|
23238
|
+
cited: s.cited,
|
|
23239
|
+
timesCited: s.timesCited,
|
|
23240
|
+
snippet: snippets.get(normUrl(s.url))?.text || "",
|
|
23241
|
+
round: s.round
|
|
23242
|
+
}));
|
|
23243
|
+
return {
|
|
23244
|
+
subQueries: [...queries],
|
|
23245
|
+
sources: ordered,
|
|
23246
|
+
snippets: [...snippets.values()],
|
|
23247
|
+
meta,
|
|
23248
|
+
unmappedKeys: [...unmapped].slice(0, 50)
|
|
23249
|
+
};
|
|
23250
|
+
}
|
|
23251
|
+
};
|
|
23252
|
+
}
|
|
23253
|
+
});
|
|
23254
|
+
|
|
23255
|
+
// src/services/fanout/adapters/index.ts
|
|
23256
|
+
function adapterForHost(hostname) {
|
|
23257
|
+
return FANOUT_ADAPTERS.find((a) => a.matchesHost(hostname)) ?? null;
|
|
23258
|
+
}
|
|
23259
|
+
var FANOUT_ADAPTERS;
|
|
23260
|
+
var init_adapters = __esm({
|
|
23261
|
+
"src/services/fanout/adapters/index.ts"() {
|
|
23262
|
+
"use strict";
|
|
23263
|
+
init_chatgpt();
|
|
23264
|
+
init_claude();
|
|
23265
|
+
FANOUT_ADAPTERS = [chatgptAdapter, claudeAdapter];
|
|
23266
|
+
}
|
|
23267
|
+
});
|
|
23268
|
+
|
|
23269
|
+
// src/services/fanout/build.ts
|
|
23270
|
+
function buildFanoutResult(ctx) {
|
|
23271
|
+
const { platform: platform2, parsed, ready, rawBytes } = ctx;
|
|
23272
|
+
const browsedUrls = parsed.sources;
|
|
23273
|
+
const citedUrls = browsedUrls.filter((s) => s.cited);
|
|
23274
|
+
const browsedOnly = browsedUrls.filter((s) => !s.cited);
|
|
23275
|
+
const prompt = ctx.prompt || parsed.meta.title || "";
|
|
23276
|
+
const result = {
|
|
23277
|
+
platform: platform2,
|
|
23278
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23279
|
+
prompt,
|
|
23280
|
+
meta: parsed.meta,
|
|
23281
|
+
subQueries: parsed.subQueries,
|
|
23282
|
+
browsedUrls,
|
|
23283
|
+
citedUrls,
|
|
23284
|
+
browsedOnly,
|
|
23285
|
+
snippets: parsed.snippets,
|
|
23286
|
+
counts: {
|
|
23287
|
+
subQueries: parsed.subQueries.length,
|
|
23288
|
+
browsed: browsedUrls.length,
|
|
23289
|
+
cited: citedUrls.length,
|
|
23290
|
+
browsedOnly: browsedOnly.length
|
|
23291
|
+
}
|
|
23292
|
+
};
|
|
23293
|
+
if (parsed.subQueries.length === 0 && browsedUrls.length === 0) {
|
|
23294
|
+
result.debug = {
|
|
23295
|
+
interceptorReady: ready,
|
|
23296
|
+
rawBytes,
|
|
23297
|
+
unmappedKeys: parsed.unmappedKeys,
|
|
23298
|
+
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."
|
|
23299
|
+
};
|
|
23300
|
+
}
|
|
23301
|
+
return result;
|
|
23302
|
+
}
|
|
23303
|
+
var init_build = __esm({
|
|
23304
|
+
"src/services/fanout/build.ts"() {
|
|
23305
|
+
"use strict";
|
|
23306
|
+
}
|
|
23307
|
+
});
|
|
23308
|
+
|
|
23309
|
+
// src/services/fanout/classify.ts
|
|
23310
|
+
function classifyDomain(domain, firstPartyDomain) {
|
|
23311
|
+
const d = domain.toLowerCase().replace(/^www\./, "");
|
|
23312
|
+
if (firstPartyDomain) {
|
|
23313
|
+
const fp = firstPartyDomain.toLowerCase().replace(/^www\./, "");
|
|
23314
|
+
if (fp && (d === fp || d.endsWith("." + fp))) return "First-party/vendor";
|
|
23315
|
+
}
|
|
23316
|
+
if (/(^|\.)reddit\.com$/i.test(d)) return "Reddit";
|
|
23317
|
+
if (SOCIAL.test(d)) return "Social/video";
|
|
23318
|
+
if (ENCYCLOPEDIA.test(d)) return "Encyclopedia";
|
|
23319
|
+
if (REVIEW.test(d)) return "Review site";
|
|
23320
|
+
if (NEWS.test(d) || /(^|\.)news\./i.test(d)) return "News/media";
|
|
23321
|
+
if (/^docs?\.|^developer\.|^developers\.|readthedocs|\.dev$/i.test(d)) return "Docs";
|
|
23322
|
+
return "Blog";
|
|
23323
|
+
}
|
|
23324
|
+
function emptyCategoryCounts() {
|
|
23325
|
+
return { "First-party/vendor": 0, "News/media": 0, Reddit: 0, "Social/video": 0, Encyclopedia: 0, "Review site": 0, Docs: 0, Blog: 0 };
|
|
23326
|
+
}
|
|
23327
|
+
function enrichFanout(result, opts = {}) {
|
|
23328
|
+
const addSiteType = (s) => ({ ...s, siteType: classifyDomain(s.domain, opts.firstPartyDomain) });
|
|
23329
|
+
const browsedUrls = result.browsedUrls.map(addSiteType);
|
|
23330
|
+
const citedUrls = result.citedUrls.map(addSiteType);
|
|
23331
|
+
const browsedOnly = result.browsedOnly.map(addSiteType);
|
|
23332
|
+
const byCategory = emptyCategoryCounts();
|
|
23333
|
+
const domainAgg = /* @__PURE__ */ new Map();
|
|
23334
|
+
for (const s of browsedUrls) {
|
|
23335
|
+
byCategory[s.siteType]++;
|
|
23336
|
+
const cur = domainAgg.get(s.domain);
|
|
23337
|
+
if (!cur) domainAgg.set(s.domain, { domain: s.domain, count: 1, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType });
|
|
23338
|
+
else {
|
|
23339
|
+
cur.count++;
|
|
23340
|
+
cur.cited = cur.cited || s.cited;
|
|
23341
|
+
cur.timesCited += s.timesCited;
|
|
23342
|
+
}
|
|
23343
|
+
}
|
|
23344
|
+
const topSites = [...domainAgg.values()].sort((a, b) => b.count - a.count || b.timesCited - a.timesCited);
|
|
23345
|
+
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 }));
|
|
23346
|
+
return {
|
|
23347
|
+
platform: result.platform,
|
|
23348
|
+
capturedAt: result.capturedAt,
|
|
23349
|
+
prompt: result.prompt,
|
|
23350
|
+
meta: result.meta,
|
|
23351
|
+
queries: result.subQueries,
|
|
23352
|
+
browsedUrls,
|
|
23353
|
+
citedUrls,
|
|
23354
|
+
browsedOnly,
|
|
23355
|
+
snippets: result.snippets,
|
|
23356
|
+
counts: result.counts,
|
|
23357
|
+
aggregates: { topSites, citationOrder, byCategory },
|
|
23358
|
+
firstPartyDomain: opts.firstPartyDomain ? opts.firstPartyDomain.toLowerCase().replace(/^www\./, "") : null,
|
|
23359
|
+
...result.debug ? { debug: result.debug } : {}
|
|
23360
|
+
};
|
|
23361
|
+
}
|
|
23362
|
+
var SOCIAL, ENCYCLOPEDIA, REVIEW, NEWS;
|
|
23363
|
+
var init_classify = __esm({
|
|
23364
|
+
"src/services/fanout/classify.ts"() {
|
|
23365
|
+
"use strict";
|
|
23366
|
+
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;
|
|
23367
|
+
ENCYCLOPEDIA = /(^|\.)(wikipedia\.org|wikimedia\.org|britannica\.com|merriam-webster\.com|dictionary\.com|fandom\.com|investopedia\.com)$/i;
|
|
23368
|
+
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;
|
|
23369
|
+
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;
|
|
23370
|
+
}
|
|
23371
|
+
});
|
|
23372
|
+
|
|
23373
|
+
// src/services/fanout/export.ts
|
|
23374
|
+
function outputBaseDir2() {
|
|
23375
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path10.join)((0, import_node_os5.homedir)(), "Downloads", "mcp-scraper");
|
|
23376
|
+
}
|
|
23377
|
+
function safe(value) {
|
|
23378
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "capture";
|
|
23379
|
+
}
|
|
23380
|
+
function writeTable(path6, rows, delimiter) {
|
|
23381
|
+
(0, import_node_fs7.writeFileSync)(path6, import_papaparse3.default.unparse(rows.length ? rows : [{}], { delimiter }));
|
|
23382
|
+
}
|
|
23383
|
+
function exportFanout(enriched) {
|
|
23384
|
+
const stamp = safe(enriched.capturedAt.replace(/[:.]/g, "-"));
|
|
23385
|
+
const dir = (0, import_node_path10.join)(outputBaseDir2(), "fanout", `${stamp}-${safe(enriched.platform)}`);
|
|
23386
|
+
(0, import_node_fs7.mkdirSync)(dir, { recursive: true });
|
|
23387
|
+
const queryRows = enriched.queries.map((q, i) => ({ index: i + 1, query: q }));
|
|
23388
|
+
const citationRows = enriched.aggregates.citationOrder.map((c) => {
|
|
23389
|
+
const src = enriched.citedUrls.find((s) => s.url === c.url);
|
|
23390
|
+
return { rank: c.rank, domain: c.domain, url: c.url, timesCited: c.timesCited, siteType: src?.siteType ?? "", title: src?.title ?? "" };
|
|
23391
|
+
});
|
|
23392
|
+
const sourceRows2 = 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 ?? "" }));
|
|
23393
|
+
const domainRows = enriched.aggregates.topSites.map((d) => ({ domain: d.domain, count: d.count, cited: d.cited, timesCited: d.timesCited, siteType: d.siteType }));
|
|
23394
|
+
const paths = {
|
|
23395
|
+
dir,
|
|
23396
|
+
json: (0, import_node_path10.join)(dir, "fanout.json"),
|
|
23397
|
+
queriesCsv: (0, import_node_path10.join)(dir, "queries.csv"),
|
|
23398
|
+
queriesTsv: (0, import_node_path10.join)(dir, "queries.tsv"),
|
|
23399
|
+
citationsCsv: (0, import_node_path10.join)(dir, "citations.csv"),
|
|
23400
|
+
sourcesCsv: (0, import_node_path10.join)(dir, "sources.csv"),
|
|
23401
|
+
domainsCsv: (0, import_node_path10.join)(dir, "domains.csv"),
|
|
23402
|
+
report: (0, import_node_path10.join)(dir, "report.html")
|
|
23403
|
+
};
|
|
23404
|
+
(0, import_node_fs7.writeFileSync)(paths.json, JSON.stringify(enriched, null, 2));
|
|
23405
|
+
writeTable(paths.queriesCsv, queryRows, ",");
|
|
23406
|
+
writeTable(paths.queriesTsv, queryRows, " ");
|
|
23407
|
+
writeTable(paths.citationsCsv, citationRows, ",");
|
|
23408
|
+
writeTable(paths.sourcesCsv, sourceRows2, ",");
|
|
23409
|
+
writeTable(paths.domainsCsv, domainRows, ",");
|
|
23410
|
+
(0, import_node_fs7.writeFileSync)(paths.report, renderReportHtml(enriched));
|
|
23411
|
+
return paths;
|
|
23412
|
+
}
|
|
23413
|
+
function esc(s) {
|
|
23414
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
23415
|
+
}
|
|
23416
|
+
function renderReportHtml(enriched) {
|
|
23417
|
+
const data = JSON.stringify(enriched).replace(/</g, "\\u003c");
|
|
23418
|
+
return `<!doctype html>
|
|
23419
|
+
<html lang="en">
|
|
23420
|
+
<head>
|
|
23421
|
+
<meta charset="utf-8" />
|
|
23422
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
23423
|
+
<title>AI Search Fan-Out \u2014 ${esc(enriched.platform)}</title>
|
|
23424
|
+
<style>
|
|
23425
|
+
:root{--bg:#0b0d12;--panel:#151922;--line:#252b38;--ink:#e8ecf3;--mut:#8b95a7;--acc:#5b9dff;--cite:#39d98a;--browse:#5a6677}
|
|
23426
|
+
*{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}
|
|
23427
|
+
.wrap{max-width:980px;margin:0 auto;padding:28px 20px 80px}
|
|
23428
|
+
h1{font-size:20px;margin:0 0 2px}.sub{color:var(--mut);margin:0 0 20px;font-size:13px}
|
|
23429
|
+
.stat{font-size:30px;font-weight:700}.stat small{font-size:13px;font-weight:400;color:var(--mut)}
|
|
23430
|
+
.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}
|
|
23431
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
|
|
23432
|
+
.card b{font-size:22px;display:block}.card span{color:var(--mut);font-size:12px}
|
|
23433
|
+
.bars{margin:8px 0 22px}.bar{display:flex;align-items:center;gap:8px;margin:4px 0}
|
|
23434
|
+
.bar .lab{width:150px;color:var(--mut);font-size:12px;text-align:right}.bar .track{flex:1;background:#10141c;border-radius:5px;overflow:hidden}
|
|
23435
|
+
.bar .fill{background:var(--acc);height:16px}.bar .n{width:34px;font-size:12px;color:var(--mut)}
|
|
23436
|
+
.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}
|
|
23437
|
+
.tab.on{background:var(--acc);border-color:var(--acc);color:#04101f;font-weight:600}
|
|
23438
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
|
|
23439
|
+
.chip{background:var(--panel);border:1px solid var(--line);color:var(--mut);padding:4px 10px;border-radius:20px;cursor:pointer;font-size:12px}
|
|
23440
|
+
.chip.on{border-color:var(--acc);color:var(--ink)}.chip b{color:var(--ink)}
|
|
23441
|
+
.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}
|
|
23442
|
+
.row .idx{color:var(--mut);min-width:22px}.row .main{flex:1}
|
|
23443
|
+
.tag{font-size:11px;padding:2px 7px;border-radius:6px;border:1px solid var(--line);color:var(--mut);margin-right:5px}
|
|
23444
|
+
.dom{font-weight:600}.url{color:var(--mut);font-size:12px;word-break:break-all}
|
|
23445
|
+
.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}
|
|
23446
|
+
.copy{background:none;border:1px solid var(--line);color:var(--mut);border-radius:6px;cursor:pointer;font-size:11px;padding:2px 8px}
|
|
23447
|
+
.hide{display:none}
|
|
23448
|
+
</style>
|
|
23449
|
+
</head>
|
|
23450
|
+
<body><div class="wrap">
|
|
23451
|
+
<h1>AI Search Fan-Out</h1>
|
|
23452
|
+
<p class="sub" id="sub"></p>
|
|
23453
|
+
<div class="stat" id="stat"></div>
|
|
23454
|
+
<div class="cards" id="cards"></div>
|
|
23455
|
+
<div id="charts"></div>
|
|
23456
|
+
<div class="tabs"><button class="tab on" data-tab="q">Queries</button><button class="tab" data-tab="u">URLs</button></div>
|
|
23457
|
+
<div class="chips" id="chips"></div>
|
|
23458
|
+
<div id="list"></div>
|
|
23459
|
+
</div>
|
|
23460
|
+
<script>
|
|
23461
|
+
const D=${data};
|
|
23462
|
+
const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
|
|
23463
|
+
document.getElementById('sub').textContent=D.platform+' \xB7 '+(D.meta.model||'model n/a')+' \xB7 '+D.meta.rounds+' search round(s) \xB7 prompt: '+(D.prompt||'');
|
|
23464
|
+
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>';
|
|
23465
|
+
const cards=[['Sub-queries',D.counts.subQueries],['Researched',D.counts.browsed],['Cited',D.counts.cited],['Browsed-only',D.counts.browsedOnly]];
|
|
23466
|
+
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)});
|
|
23467
|
+
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}
|
|
23468
|
+
const charts=document.getElementById('charts');
|
|
23469
|
+
charts.appendChild(barChart('URL Categories',D.aggregates.byCategory));
|
|
23470
|
+
let tab='q',filter=null;
|
|
23471
|
+
const chips=document.getElementById('chips'),list=document.getElementById('list');
|
|
23472
|
+
function render(){
|
|
23473
|
+
chips.innerHTML='';list.innerHTML='';
|
|
23474
|
+
if(tab==='q'){
|
|
23475
|
+
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)});
|
|
23476
|
+
} else {
|
|
23477
|
+
const counts={};D.browsedUrls.forEach(s=>{counts[s.siteType]=(counts[s.siteType]||0)+1});
|
|
23478
|
+
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)});
|
|
23479
|
+
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)});
|
|
23480
|
+
}
|
|
23481
|
+
}
|
|
23482
|
+
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
|
23483
|
+
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()});
|
|
23484
|
+
render();
|
|
23485
|
+
</script>
|
|
23486
|
+
</body></html>`;
|
|
23487
|
+
}
|
|
23488
|
+
var import_node_fs7, import_node_os5, import_node_path10, import_papaparse3;
|
|
23489
|
+
var init_export = __esm({
|
|
23490
|
+
"src/services/fanout/export.ts"() {
|
|
23491
|
+
"use strict";
|
|
23492
|
+
import_node_fs7 = require("fs");
|
|
23493
|
+
import_node_os5 = require("os");
|
|
23494
|
+
import_node_path10 = require("path");
|
|
23495
|
+
import_papaparse3 = __toESM(require("papaparse"), 1);
|
|
23496
|
+
}
|
|
23497
|
+
});
|
|
23498
|
+
|
|
23499
|
+
// src/services/fanout/run-capture.ts
|
|
23500
|
+
async function sendPrompt(page, text) {
|
|
23501
|
+
for (const selector of COMPOSER_SELECTORS) {
|
|
23502
|
+
const locator = page.locator(selector).first();
|
|
23503
|
+
try {
|
|
23504
|
+
await locator.waitFor({ state: "visible", timeout: 12e3 });
|
|
23505
|
+
} catch {
|
|
23506
|
+
continue;
|
|
23507
|
+
}
|
|
23508
|
+
await locator.click({ timeout: 4e3 }).catch(() => void 0);
|
|
23509
|
+
await locator.focus().catch(() => void 0);
|
|
23510
|
+
await page.keyboard.type(text, { delay: 12 });
|
|
23511
|
+
await page.waitForTimeout(300).catch(() => void 0);
|
|
23512
|
+
await page.keyboard.press("Enter");
|
|
23513
|
+
return true;
|
|
23514
|
+
}
|
|
23515
|
+
return false;
|
|
23516
|
+
}
|
|
23517
|
+
async function waitForAnswer(page, cap, baseline, waitMs) {
|
|
23518
|
+
const start = Date.now();
|
|
23519
|
+
while (Date.now() - start < waitMs && cap.answerStreamCount() <= baseline) {
|
|
23520
|
+
await page.waitForTimeout(400).catch(() => void 0);
|
|
23521
|
+
}
|
|
23522
|
+
await page.waitForTimeout(1e3).catch(() => void 0);
|
|
23523
|
+
}
|
|
23524
|
+
async function runFanoutCapture(page, input) {
|
|
23525
|
+
const cap = new FanoutCdpCapture();
|
|
23526
|
+
await cap.attach(page);
|
|
23527
|
+
try {
|
|
23528
|
+
const baseline = cap.answerStreamCount();
|
|
23529
|
+
if (input.prompt) await sendPrompt(page, input.prompt).catch(() => false);
|
|
23530
|
+
const waitMs = input.wait_ms ?? (input.prompt ? 9e4 : 8e3);
|
|
23531
|
+
await waitForAnswer(page, cap, baseline, waitMs);
|
|
23532
|
+
await cap.drain();
|
|
23533
|
+
let hostname = "";
|
|
23534
|
+
try {
|
|
23535
|
+
hostname = new URL(page.url()).hostname;
|
|
23536
|
+
} catch {
|
|
23537
|
+
hostname = "";
|
|
23538
|
+
}
|
|
23539
|
+
const adapter = adapterForHost(hostname);
|
|
23540
|
+
if (!adapter) {
|
|
23541
|
+
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.`);
|
|
23542
|
+
}
|
|
23543
|
+
const text = cap.bestAnswerText();
|
|
23544
|
+
const parsed = adapter.parse(text, input.prompt || "");
|
|
23545
|
+
const result = buildFanoutResult({ platform: adapter.platform, parsed, prompt: input.prompt || "", ready: true, rawBytes: text.length });
|
|
23546
|
+
const enriched = enrichFanout(result, { firstPartyDomain: input.first_party_domain });
|
|
23547
|
+
const exports2 = input.export ? exportFanout(enriched) : null;
|
|
23548
|
+
return { result: enriched, exports: exports2 };
|
|
23549
|
+
} finally {
|
|
23550
|
+
await cap.detach().catch(() => void 0);
|
|
23551
|
+
}
|
|
23552
|
+
}
|
|
23553
|
+
var COMPOSER_SELECTORS;
|
|
23554
|
+
var init_run_capture = __esm({
|
|
23555
|
+
"src/services/fanout/run-capture.ts"() {
|
|
23556
|
+
"use strict";
|
|
23557
|
+
init_cdp_capture();
|
|
23558
|
+
init_adapters();
|
|
23559
|
+
init_build();
|
|
23560
|
+
init_classify();
|
|
23561
|
+
init_export();
|
|
23562
|
+
COMPOSER_SELECTORS = [
|
|
23563
|
+
"#prompt-textarea",
|
|
23564
|
+
"div#prompt-textarea",
|
|
23565
|
+
'textarea[data-testid="prompt-textarea"]',
|
|
23566
|
+
'div.ProseMirror[contenteditable="true"]',
|
|
23567
|
+
'div[contenteditable="true"]',
|
|
23568
|
+
"textarea"
|
|
23569
|
+
];
|
|
23570
|
+
}
|
|
23571
|
+
});
|
|
23572
|
+
|
|
22699
23573
|
// src/services/browser-agent/browser-agent-service.ts
|
|
22700
23574
|
function client() {
|
|
22701
23575
|
const apiKey = browserServiceApiKey();
|
|
@@ -22797,6 +23671,17 @@ async function goto(cdpWsUrl, url) {
|
|
|
22797
23671
|
});
|
|
22798
23672
|
}
|
|
22799
23673
|
}
|
|
23674
|
+
async function captureFanout(cdpWsUrl, input) {
|
|
23675
|
+
const browser = await import_playwright4.chromium.connectOverCDP(cdpWsUrl);
|
|
23676
|
+
try {
|
|
23677
|
+
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
23678
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
23679
|
+
return await runFanoutCapture(page, input);
|
|
23680
|
+
} finally {
|
|
23681
|
+
await browser.close().catch(() => {
|
|
23682
|
+
});
|
|
23683
|
+
}
|
|
23684
|
+
}
|
|
22800
23685
|
async function readPage(cdpWsUrl) {
|
|
22801
23686
|
const browser = await import_playwright4.chromium.connectOverCDP(cdpWsUrl);
|
|
22802
23687
|
try {
|
|
@@ -23026,6 +23911,7 @@ var init_browser_agent_service = __esm({
|
|
|
23026
23911
|
import_sdk6 = __toESM(require("@onkernel/sdk"), 1);
|
|
23027
23912
|
import_playwright4 = require("playwright");
|
|
23028
23913
|
init_browser_service_env();
|
|
23914
|
+
init_run_capture();
|
|
23029
23915
|
DEFAULT_TIMEOUT_SECONDS = 600;
|
|
23030
23916
|
}
|
|
23031
23917
|
});
|
|
@@ -23195,6 +24081,29 @@ function buildBrowserAgentRoutes() {
|
|
|
23195
24081
|
return c.json(failure(err), 502);
|
|
23196
24082
|
}
|
|
23197
24083
|
});
|
|
24084
|
+
app2.post("/sessions/:id/capture-fanout", async (c) => {
|
|
24085
|
+
const user = c.get("user");
|
|
24086
|
+
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
24087
|
+
if (!row) return c.json({ error: "not found" }, 404);
|
|
24088
|
+
const body = await c.req.json().catch(() => ({}));
|
|
24089
|
+
const input = {
|
|
24090
|
+
prompt: typeof body.prompt === "string" ? body.prompt : void 0,
|
|
24091
|
+
wait_ms: typeof body.wait_ms === "number" ? body.wait_ms : void 0,
|
|
24092
|
+
first_party_domain: typeof body.first_party_domain === "string" ? body.first_party_domain : void 0,
|
|
24093
|
+
export: body.export === true
|
|
24094
|
+
};
|
|
24095
|
+
const t0 = Date.now();
|
|
24096
|
+
try {
|
|
24097
|
+
const result = await captureFanout(row.cdp_ws_url, input);
|
|
24098
|
+
await charge(row.id, user.id, t0);
|
|
24099
|
+
await recordAction({ sessionId: row.id, type: "capture_fanout", params: { prompt: input.prompt ?? null }, ok: true });
|
|
24100
|
+
return c.json(result);
|
|
24101
|
+
} catch (err) {
|
|
24102
|
+
await charge(row.id, user.id, t0);
|
|
24103
|
+
await recordAction({ sessionId: row.id, type: "capture_fanout", params: { prompt: input.prompt ?? null }, ok: false, error: String(err) });
|
|
24104
|
+
return c.json(failure(err), 502);
|
|
24105
|
+
}
|
|
24106
|
+
});
|
|
23198
24107
|
app2.post("/sessions/:id/screenshot", async (c) => {
|
|
23199
24108
|
const user = c.get("user");
|
|
23200
24109
|
const row = await loadOpenSession(c.req.param("id"), user.id);
|
|
@@ -25101,10 +26010,10 @@ ${ATTRIBUTION_PIXEL_SCRIPT}
|
|
|
25101
26010
|
});
|
|
25102
26011
|
|
|
25103
26012
|
// bin/api-server.ts
|
|
25104
|
-
var
|
|
26013
|
+
var import_node_fs8 = require("fs");
|
|
25105
26014
|
function loadDotEnv() {
|
|
25106
26015
|
try {
|
|
25107
|
-
for (const line of (0,
|
|
26016
|
+
for (const line of (0, import_node_fs8.readFileSync)(".env", "utf8").split("\n")) {
|
|
25108
26017
|
const eq = line.indexOf("=");
|
|
25109
26018
|
if (eq < 1 || line.trimStart().startsWith("#")) continue;
|
|
25110
26019
|
const k = line.slice(0, eq).trim();
|