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
|
@@ -1,17 +1,39 @@
|
|
|
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/browser-agent-stdio-server.ts
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
27
|
+
var import_node_fs5 = require("fs");
|
|
28
|
+
var import_node_os6 = require("os");
|
|
29
|
+
var import_node_path7 = require("path");
|
|
8
30
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
31
|
|
|
10
32
|
// src/mcp/browser-agent-mcp-server.ts
|
|
11
33
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
12
|
-
var
|
|
13
|
-
var
|
|
14
|
-
var
|
|
34
|
+
var import_node_fs4 = require("fs");
|
|
35
|
+
var import_node_os5 = require("os");
|
|
36
|
+
var import_node_path6 = require("path");
|
|
15
37
|
|
|
16
38
|
// src/lib/chrome-profiles.ts
|
|
17
39
|
var import_promises = require("fs/promises");
|
|
@@ -233,18 +255,834 @@ async function syncImportedChromeProfile(name, options = {}) {
|
|
|
233
255
|
|
|
234
256
|
// src/services/browser-agent/local-browser-agent-service.ts
|
|
235
257
|
var import_node_crypto = require("crypto");
|
|
258
|
+
var import_node_fs3 = require("fs");
|
|
259
|
+
var import_node_path4 = require("path");
|
|
260
|
+
var import_playwright_extra = require("playwright-extra");
|
|
261
|
+
var import_puppeteer_extra_plugin_stealth = __toESM(require("puppeteer-extra-plugin-stealth"), 1);
|
|
262
|
+
|
|
263
|
+
// src/services/fanout/cdp-capture.ts
|
|
264
|
+
var ANSWER_RE = /\/backend-api\/(f\/)?conversation$|\/chat_conversations\/[^/]+\/(completion|retry_completion)$|\/completion$/i;
|
|
265
|
+
var FanoutCdpCapture = class {
|
|
266
|
+
client = null;
|
|
267
|
+
reqUrl = /* @__PURE__ */ new Map();
|
|
268
|
+
ctById = /* @__PURE__ */ new Map();
|
|
269
|
+
answerIds = /* @__PURE__ */ new Set();
|
|
270
|
+
bodies = [];
|
|
271
|
+
pending = [];
|
|
272
|
+
answerStreams = 0;
|
|
273
|
+
async attach(page) {
|
|
274
|
+
if (this.client) return;
|
|
275
|
+
const client = await page.context().newCDPSession(page);
|
|
276
|
+
this.client = client;
|
|
277
|
+
await client.send("Network.enable", { maxTotalBufferSize: 2e8, maxResourceBufferSize: 2e8 });
|
|
278
|
+
client.on("Network.requestWillBeSent", (e) => {
|
|
279
|
+
const id = e.requestId;
|
|
280
|
+
const req = e.request;
|
|
281
|
+
if (id) this.reqUrl.set(id, req?.url || "");
|
|
282
|
+
});
|
|
283
|
+
client.on("Network.responseReceived", (e) => {
|
|
284
|
+
const id = e.requestId;
|
|
285
|
+
const res = e.response;
|
|
286
|
+
const url = res?.url || this.reqUrl.get(id) || "";
|
|
287
|
+
const headers = res?.headers || {};
|
|
288
|
+
const ct = String(headers["content-type"] || headers["Content-Type"] || "");
|
|
289
|
+
if (id) this.ctById.set(id, ct);
|
|
290
|
+
if (id && (this.isAnswerUrl(url) || /event-stream/i.test(ct))) this.answerIds.add(id);
|
|
291
|
+
});
|
|
292
|
+
client.on("Network.loadingFinished", (e) => this.grab(e.requestId));
|
|
293
|
+
}
|
|
294
|
+
reset() {
|
|
295
|
+
this.bodies = [];
|
|
296
|
+
this.answerStreams = 0;
|
|
297
|
+
this.answerIds.clear();
|
|
298
|
+
this.pending = [];
|
|
299
|
+
}
|
|
300
|
+
answerStreamCount() {
|
|
301
|
+
return this.answerStreams;
|
|
302
|
+
}
|
|
303
|
+
async drain() {
|
|
304
|
+
await Promise.all(this.pending);
|
|
305
|
+
}
|
|
306
|
+
bestAnswerText() {
|
|
307
|
+
const eventStreams = this.bodies.filter((b) => /event-stream/i.test(b.ct));
|
|
308
|
+
const answerStreams = eventStreams.filter((b) => this.isAnswerUrl(b.url)).sort((a, b) => b.text.length - a.text.length);
|
|
309
|
+
if (answerStreams.length) return answerStreams.map((b) => b.text).join("\n");
|
|
310
|
+
const answerJson = this.bodies.filter((b) => this.isAnswerUrl(b.url)).sort((a, b) => b.text.length - a.text.length);
|
|
311
|
+
if (answerJson.length) return answerJson[0].text;
|
|
312
|
+
if (eventStreams.length) return eventStreams.sort((a, b) => b.text.length - a.text.length)[0].text;
|
|
313
|
+
return "";
|
|
314
|
+
}
|
|
315
|
+
async detach() {
|
|
316
|
+
try {
|
|
317
|
+
await this.client?.detach();
|
|
318
|
+
} catch {
|
|
319
|
+
this.client = null;
|
|
320
|
+
}
|
|
321
|
+
this.client = null;
|
|
322
|
+
}
|
|
323
|
+
isAnswerUrl(url) {
|
|
324
|
+
let path = "";
|
|
325
|
+
try {
|
|
326
|
+
path = new URL(url).pathname;
|
|
327
|
+
} catch {
|
|
328
|
+
path = String(url || "").split("?")[0];
|
|
329
|
+
}
|
|
330
|
+
return ANSWER_RE.test(path);
|
|
331
|
+
}
|
|
332
|
+
grab(requestId) {
|
|
333
|
+
if (!requestId || !this.client || !this.answerIds.has(requestId)) return;
|
|
334
|
+
const url = this.reqUrl.get(requestId) || "";
|
|
335
|
+
const ct = this.ctById.get(requestId) || "";
|
|
336
|
+
const client = this.client;
|
|
337
|
+
const p = client.send("Network.getResponseBody", { requestId }).then((r) => {
|
|
338
|
+
const text = r.base64Encoded ? Buffer.from(String(r.body), "base64").toString("utf8") : String(r.body || "");
|
|
339
|
+
if (text) {
|
|
340
|
+
this.bodies.push({ url, ct, text });
|
|
341
|
+
if (this.isAnswerUrl(url)) this.answerStreams++;
|
|
342
|
+
}
|
|
343
|
+
}).catch(() => void 0);
|
|
344
|
+
this.pending.push(p);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// src/services/fanout/parse-helpers.ts
|
|
349
|
+
var INTERNAL = /chatgpt\.com|openai\.com|oaiusercontent|oaistatic|claude\.ai|anthropic\.com/i;
|
|
350
|
+
var TRACK = /^(utm_|oai|fbclid|gclid|ref$|ref_|sa$|usg$|ved$|sca_esv$|sources$)/i;
|
|
351
|
+
function domainOf(u) {
|
|
352
|
+
try {
|
|
353
|
+
return new URL(u).hostname.replace(/^www\./, "");
|
|
354
|
+
} catch {
|
|
355
|
+
return "";
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function normUrl(u) {
|
|
359
|
+
try {
|
|
360
|
+
const x = new URL(u);
|
|
361
|
+
x.hash = "";
|
|
362
|
+
for (const k of [...x.searchParams.keys()]) {
|
|
363
|
+
if (TRACK.test(k)) x.searchParams.delete(k);
|
|
364
|
+
}
|
|
365
|
+
return x.toString().replace(/\/$/, "").toLowerCase();
|
|
366
|
+
} catch {
|
|
367
|
+
return String(u || "").toLowerCase();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function isExternal(u) {
|
|
371
|
+
const h = domainOf(u);
|
|
372
|
+
return Boolean(h) && !INTERNAL.test(h);
|
|
373
|
+
}
|
|
374
|
+
function unesc(s) {
|
|
375
|
+
try {
|
|
376
|
+
return JSON.parse('"' + s + '"');
|
|
377
|
+
} catch {
|
|
378
|
+
return String(s).replace(/\\(.)/g, "$1");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function stripCitationMarkers(s) {
|
|
382
|
+
return String(s || "").replace(/[-]/g, "").replace(/cite(?:turn\d+\w+?\d+)+/g, "").replace(/\s+/g, " ").trim();
|
|
383
|
+
}
|
|
384
|
+
function sliceBalanced(text, openIdx) {
|
|
385
|
+
const open = text[openIdx];
|
|
386
|
+
const close = open === "[" ? "]" : "}";
|
|
387
|
+
let depth = 0;
|
|
388
|
+
let inStr = false;
|
|
389
|
+
let esc2 = false;
|
|
390
|
+
for (let i = openIdx; i < text.length; i++) {
|
|
391
|
+
const ch = text[i];
|
|
392
|
+
if (inStr) {
|
|
393
|
+
if (esc2) esc2 = false;
|
|
394
|
+
else if (ch === "\\") esc2 = true;
|
|
395
|
+
else if (ch === '"') inStr = false;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (ch === '"') {
|
|
399
|
+
inStr = true;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (ch === open) depth++;
|
|
403
|
+
else if (ch === close) {
|
|
404
|
+
depth--;
|
|
405
|
+
if (depth === 0) return text.slice(openIdx, i + 1);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
function findJsonValues(text, field) {
|
|
411
|
+
const out = [];
|
|
412
|
+
const re = new RegExp('"' + field + '"\\s*:\\s*(\\[|\\{)', "g");
|
|
413
|
+
let m;
|
|
414
|
+
while (m = re.exec(text)) {
|
|
415
|
+
const openIdx = m.index + m[0].length - 1;
|
|
416
|
+
const slice = sliceBalanced(text, openIdx);
|
|
417
|
+
if (slice) {
|
|
418
|
+
try {
|
|
419
|
+
out.push(JSON.parse(slice));
|
|
420
|
+
} catch {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
function stringsInArrayBody(arrBody) {
|
|
428
|
+
const out = [];
|
|
429
|
+
const re = /"((?:[^"\\]|\\.){2,300}?)"/g;
|
|
430
|
+
let m;
|
|
431
|
+
while (m = re.exec(arrBody)) out.push(unesc(m[1]));
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
function cleanQuery(q) {
|
|
435
|
+
const t = (q || "").trim();
|
|
436
|
+
if (t.length < 2 || t.length > 300) return null;
|
|
437
|
+
return t;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/services/fanout/adapters/chatgpt.ts
|
|
441
|
+
function roundOf(refId) {
|
|
442
|
+
const m = /^turn(\d+)/.exec(refId || "");
|
|
443
|
+
return m ? Number(m[1]) : null;
|
|
444
|
+
}
|
|
445
|
+
function normRef(r) {
|
|
446
|
+
if (typeof r === "string") {
|
|
447
|
+
const m = /turn(\d+)(search|news|ref|video|image|product)(\d+)/.exec(r);
|
|
448
|
+
return m ? `turn${m[1]}${m[2]}${m[3]}` : null;
|
|
449
|
+
}
|
|
450
|
+
if (r && typeof r === "object") {
|
|
451
|
+
const o = r;
|
|
452
|
+
if (o.turn_index != null) {
|
|
453
|
+
return `turn${o.turn_index}${o.ref_type || "search"}${o.ref_index != null ? o.ref_index : ""}`;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
function refsInText(s) {
|
|
459
|
+
return (String(s || "").match(/turn\d+(?:search|news|ref|video|image|product)\d+/g) || []).map(normRef).filter((x) => Boolean(x));
|
|
460
|
+
}
|
|
461
|
+
function collectInto(arr, v) {
|
|
462
|
+
if (Array.isArray(v)) arr.push(...v);
|
|
463
|
+
else if (v && typeof v === "object") arr.push(v);
|
|
464
|
+
}
|
|
465
|
+
function reconstructFromSsePatches(raw) {
|
|
466
|
+
const contentReferences = [];
|
|
467
|
+
const searchResultGroups = [];
|
|
468
|
+
const citations = [];
|
|
469
|
+
const citedUrls = /* @__PURE__ */ new Set();
|
|
470
|
+
const state = { found: false };
|
|
471
|
+
const handleOp = (op) => {
|
|
472
|
+
if (!op || typeof op !== "object") return;
|
|
473
|
+
const o = op;
|
|
474
|
+
const v = o.v;
|
|
475
|
+
if (o.o === "patch" && Array.isArray(v)) {
|
|
476
|
+
state.found = true;
|
|
477
|
+
v.forEach(handleOp);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const path = typeof o.p === "string" ? o.p : "";
|
|
481
|
+
if ((path === "" || path === "/message") && v && typeof v === "object") {
|
|
482
|
+
const root = v;
|
|
483
|
+
const msg = root.message || root;
|
|
484
|
+
const md = msg && msg.metadata;
|
|
485
|
+
if (md) {
|
|
486
|
+
collectInto(contentReferences, md.content_references);
|
|
487
|
+
collectInto(searchResultGroups, md.search_result_groups);
|
|
488
|
+
collectInto(citations, md.citations);
|
|
489
|
+
state.found = true;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (!path) return;
|
|
493
|
+
if (/\/safe_urls$/.test(path) && Array.isArray(v)) {
|
|
494
|
+
for (const u of v) if (typeof u === "string") citedUrls.add(u);
|
|
495
|
+
state.found = true;
|
|
496
|
+
} else if (path.includes("content_references")) {
|
|
497
|
+
collectInto(contentReferences, v);
|
|
498
|
+
state.found = true;
|
|
499
|
+
} else if (path.includes("search_result_groups")) {
|
|
500
|
+
collectInto(searchResultGroups, v);
|
|
501
|
+
state.found = true;
|
|
502
|
+
} else if (path.includes("citations")) {
|
|
503
|
+
collectInto(citations, v);
|
|
504
|
+
state.found = true;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
for (const line of raw.split("\n")) {
|
|
508
|
+
const s = (line.startsWith("data:") ? line.slice(5) : line).trim();
|
|
509
|
+
if (!s || s === "[DONE]" || s[0] !== "{" && s[0] !== "[") continue;
|
|
510
|
+
let parsed;
|
|
511
|
+
try {
|
|
512
|
+
parsed = JSON.parse(s);
|
|
513
|
+
} catch {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const ops = Array.isArray(parsed) ? parsed : [parsed];
|
|
517
|
+
ops.forEach(handleOp);
|
|
518
|
+
}
|
|
519
|
+
if (!state.found) return raw;
|
|
520
|
+
const citedRefs = [...citedUrls].map((url) => ({ type: "grouped_webpages", items: [{ url }] }));
|
|
521
|
+
const synthetic = JSON.stringify({
|
|
522
|
+
content_references: [...contentReferences, ...citedRefs],
|
|
523
|
+
search_result_groups: searchResultGroups,
|
|
524
|
+
citations
|
|
525
|
+
});
|
|
526
|
+
return raw + "\n" + synthetic;
|
|
527
|
+
}
|
|
528
|
+
var chatgptAdapter = {
|
|
529
|
+
platform: "ChatGPT",
|
|
530
|
+
matchesHost(hostname) {
|
|
531
|
+
return /(^|\.)chatgpt\.com$|(^|\.)openai\.com$/i.test(hostname);
|
|
532
|
+
},
|
|
533
|
+
parse(rawInput, prompt) {
|
|
534
|
+
const raw = reconstructFromSsePatches(rawInput);
|
|
535
|
+
const sources = /* @__PURE__ */ new Map();
|
|
536
|
+
const snippets = /* @__PURE__ */ new Map();
|
|
537
|
+
const queries = /* @__PURE__ */ new Set();
|
|
538
|
+
const unmapped = /* @__PURE__ */ new Set();
|
|
539
|
+
const meta = { model: "", finishType: "", title: "", rounds: 0 };
|
|
540
|
+
const ensure = (url) => {
|
|
541
|
+
if (!isExternal(url)) return null;
|
|
542
|
+
const k = normUrl(url);
|
|
543
|
+
let s = sources.get(k);
|
|
544
|
+
if (!s) {
|
|
545
|
+
s = { url, domain: domainOf(url), title: "", cited: false, timesCited: 0, firstOrder: Infinity, snippet: "", round: null };
|
|
546
|
+
sources.set(k, s);
|
|
547
|
+
}
|
|
548
|
+
return s;
|
|
549
|
+
};
|
|
550
|
+
const addSnippet = (url, title, text) => {
|
|
551
|
+
const clean = stripCitationMarkers(text);
|
|
552
|
+
if (!isExternal(url) || clean.length < 2) return;
|
|
553
|
+
snippets.set(normUrl(url), { url, domain: domainOf(url), title: title || domainOf(url), text: clean.slice(0, 400) });
|
|
554
|
+
};
|
|
555
|
+
for (const m of raw.matchAll(/"([a-z_]*(?:quer|search)[a-z_]*)"\s*:/gi)) unmapped.add(m[1]);
|
|
556
|
+
for (const m of raw.matchAll(/"search_model_queries"\s*:\s*\[([^\]]*)\]/g)) {
|
|
557
|
+
for (const q of stringsInArrayBody(m[1])) {
|
|
558
|
+
const c = cleanQuery(q);
|
|
559
|
+
if (c) queries.add(c);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
for (const m of raw.matchAll(/"(?:search_queries|queries|search_query)"\s*:\s*\[([^\]]*)\]/g)) {
|
|
563
|
+
for (const q of stringsInArrayBody(m[1])) {
|
|
564
|
+
const c = cleanQuery(q);
|
|
565
|
+
if (c) queries.add(c);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
for (const v of findJsonValues(raw, "search_result_groups")) {
|
|
569
|
+
if (!Array.isArray(v)) continue;
|
|
570
|
+
for (const g of v) {
|
|
571
|
+
const entries = g && typeof g === "object" && Array.isArray(g.entries) ? g.entries : [];
|
|
572
|
+
for (const e of entries) {
|
|
573
|
+
const url = typeof e.url === "string" ? e.url : "";
|
|
574
|
+
const s = ensure(url);
|
|
575
|
+
if (!s) continue;
|
|
576
|
+
if (!s.title && typeof e.title === "string") s.title = e.title.slice(0, 200);
|
|
577
|
+
const ref = normRef(e.ref_id);
|
|
578
|
+
if (ref && s.round == null) s.round = roundOf(ref);
|
|
579
|
+
if (typeof e.snippet === "string" && e.snippet) addSnippet(url, s.title, e.snippet);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
for (const m of raw.matchAll(/"url"\s*:\s*"(https?:\/\/[^"\\]+)"(?:[\s\S]{0,120}?"(?:title|name)"\s*:\s*"((?:[^"\\]|\\.){0,200})")?/g)) {
|
|
584
|
+
const s = ensure(m[1]);
|
|
585
|
+
if (s && !s.title && m[2]) s.title = unesc(m[2]).slice(0, 200);
|
|
586
|
+
}
|
|
587
|
+
let spanTotal = 0;
|
|
588
|
+
for (const cr of findJsonValues(raw, "content_references")) {
|
|
589
|
+
if (!Array.isArray(cr)) continue;
|
|
590
|
+
for (const ref of cr) {
|
|
591
|
+
const type = typeof ref.type === "string" ? ref.type : "";
|
|
592
|
+
const isBundle = /sources_footnote|nav_list|alt_text/.test(type);
|
|
593
|
+
const refs = refsInText(typeof ref.matched_text === "string" ? ref.matched_text : "");
|
|
594
|
+
if (!isBundle) spanTotal++;
|
|
595
|
+
const ord = typeof ref.start_idx === "number" ? ref.start_idx : isBundle ? Infinity : spanTotal;
|
|
596
|
+
const items = Array.isArray(ref.items) ? ref.items : [];
|
|
597
|
+
items.forEach((it, idx) => {
|
|
598
|
+
const refForItem = refs.length === items.length ? refs[idx] : refs[0];
|
|
599
|
+
const urls = [it.url].concat((Array.isArray(it.supporting_websites) ? it.supporting_websites : []).map((s) => s?.url)).filter((u) => typeof u === "string");
|
|
600
|
+
for (const u of urls) {
|
|
601
|
+
const s = ensure(u);
|
|
602
|
+
if (!s) continue;
|
|
603
|
+
if (!isBundle) {
|
|
604
|
+
s.cited = true;
|
|
605
|
+
s.timesCited++;
|
|
606
|
+
if (ord < s.firstOrder) s.firstOrder = ord;
|
|
607
|
+
} else if (!s.cited) {
|
|
608
|
+
s.cited = true;
|
|
609
|
+
}
|
|
610
|
+
if (refForItem && s.round == null) s.round = roundOf(refForItem);
|
|
611
|
+
}
|
|
612
|
+
if (typeof it.url === "string" && typeof it.snippet === "string") addSnippet(it.url, typeof it.title === "string" ? it.title : "", it.snippet);
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
for (const v of findJsonValues(raw, "citations")) {
|
|
617
|
+
if (!Array.isArray(v)) continue;
|
|
618
|
+
v.forEach((c, i) => {
|
|
619
|
+
const md = c && typeof c === "object" ? c.metadata : null;
|
|
620
|
+
if (!md || typeof md.url !== "string") return;
|
|
621
|
+
const s = ensure(md.url);
|
|
622
|
+
if (!s) return;
|
|
623
|
+
s.cited = true;
|
|
624
|
+
s.timesCited++;
|
|
625
|
+
const ord = typeof c.start_ix === "number" ? c.start_ix : i;
|
|
626
|
+
if (ord < s.firstOrder) s.firstOrder = ord;
|
|
627
|
+
if (typeof md.text === "string") addSnippet(md.url, typeof md.title === "string" ? md.title : "", md.text);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
for (const m of raw.matchAll(/"content_type"\s*:\s*"tether_quote"([\s\S]{0,4000}?)"text"\s*:\s*"((?:[^"\\]|\\.)*)"/g)) {
|
|
631
|
+
const url = (m[1].match(/"url"\s*:\s*"([^"]+)"/) || [])[1];
|
|
632
|
+
const title = (m[1].match(/"title"\s*:\s*"((?:[^"\\]|\\.)*)"/) || [])[1];
|
|
633
|
+
if (url) {
|
|
634
|
+
ensure(url);
|
|
635
|
+
addSnippet(url, title ? unesc(title) : "", unesc(m[2]));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const modelMatches = [...raw.matchAll(/"resolved_model_slug"\s*:\s*"([^"]+)"/g)];
|
|
639
|
+
const slugMatches = modelMatches.length ? modelMatches : [...raw.matchAll(/"model_slug"\s*:\s*"([^"]+)"/g)];
|
|
640
|
+
if (slugMatches.length) meta.model = slugMatches[slugMatches.length - 1][1];
|
|
641
|
+
const finish = raw.match(/"finish_details"\s*:\s*\{[^}]*"type"\s*:\s*"([^"]+)"/);
|
|
642
|
+
if (finish) meta.finishType = finish[1];
|
|
643
|
+
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;
|
|
644
|
+
const sourceRounds = /* @__PURE__ */ new Set();
|
|
645
|
+
for (const s of sources.values()) if (s.round != null) sourceRounds.add(s.round);
|
|
646
|
+
meta.rounds = Math.max(searchRounds, sourceRounds.size);
|
|
647
|
+
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) => ({
|
|
648
|
+
url: s.url,
|
|
649
|
+
domain: s.domain,
|
|
650
|
+
title: s.title || s.domain,
|
|
651
|
+
cited: s.cited,
|
|
652
|
+
timesCited: s.timesCited,
|
|
653
|
+
snippet: snippets.get(normUrl(s.url))?.text || "",
|
|
654
|
+
round: s.round
|
|
655
|
+
}));
|
|
656
|
+
return {
|
|
657
|
+
subQueries: [...queries],
|
|
658
|
+
sources: ordered,
|
|
659
|
+
snippets: [...snippets.values()],
|
|
660
|
+
meta,
|
|
661
|
+
unmappedKeys: [...unmapped].slice(0, 50)
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// src/services/fanout/adapters/claude.ts
|
|
667
|
+
function isFavicon(u) {
|
|
668
|
+
return /\/s2\/favicons/i.test(u) || /favicon/i.test(u);
|
|
669
|
+
}
|
|
670
|
+
var claudeAdapter = {
|
|
671
|
+
platform: "Claude",
|
|
672
|
+
matchesHost(hostname) {
|
|
673
|
+
return /(^|\.)claude\.ai$/i.test(hostname);
|
|
674
|
+
},
|
|
675
|
+
parse(raw, prompt) {
|
|
676
|
+
const sources = /* @__PURE__ */ new Map();
|
|
677
|
+
const snippets = /* @__PURE__ */ new Map();
|
|
678
|
+
const queries = /* @__PURE__ */ new Set();
|
|
679
|
+
const unmapped = /* @__PURE__ */ new Set();
|
|
680
|
+
const meta = { model: "", finishType: "", title: "", rounds: 0 };
|
|
681
|
+
const ensure = (url) => {
|
|
682
|
+
if (!url || !isExternal(url) || isFavicon(url)) return null;
|
|
683
|
+
const k = normUrl(url);
|
|
684
|
+
let s = sources.get(k);
|
|
685
|
+
if (!s) {
|
|
686
|
+
s = { url, domain: domainOf(url), title: "", cited: false, timesCited: 0, snippet: "", round: null };
|
|
687
|
+
sources.set(k, s);
|
|
688
|
+
}
|
|
689
|
+
return s;
|
|
690
|
+
};
|
|
691
|
+
const toolInputs = /* @__PURE__ */ new Map();
|
|
692
|
+
const searchBlocks = /* @__PURE__ */ new Set();
|
|
693
|
+
const finalizeTool = (index) => {
|
|
694
|
+
const t = toolInputs.get(index);
|
|
695
|
+
if (!t || !/web_search|web_fetch|search|browse/i.test(t.name)) return;
|
|
696
|
+
try {
|
|
697
|
+
const input = JSON.parse(t.buf);
|
|
698
|
+
if (typeof input.query === "string") {
|
|
699
|
+
const q = cleanQuery(input.query);
|
|
700
|
+
if (q) queries.add(q);
|
|
701
|
+
}
|
|
702
|
+
} catch {
|
|
703
|
+
const m = t.buf.match(/"query"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
704
|
+
if (m) {
|
|
705
|
+
const q = cleanQuery(unesc(m[1]));
|
|
706
|
+
if (q) queries.add(q);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
for (const line of raw.split("\n")) {
|
|
711
|
+
const s = (line.startsWith("data:") ? line.slice(5) : line).trim();
|
|
712
|
+
if (!s || s[0] !== "{" && s[0] !== "[") continue;
|
|
713
|
+
let ev;
|
|
714
|
+
try {
|
|
715
|
+
ev = JSON.parse(s);
|
|
716
|
+
} catch {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const type = ev.type;
|
|
720
|
+
if (type === "content_block_start") {
|
|
721
|
+
const cb = ev.content_block || {};
|
|
722
|
+
const index = typeof ev.index === "number" ? ev.index : -1;
|
|
723
|
+
if (cb.type === "tool_use" && index >= 0) {
|
|
724
|
+
const name = typeof cb.name === "string" ? cb.name : "";
|
|
725
|
+
toolInputs.set(index, { name, buf: "" });
|
|
726
|
+
if (/web_search|web_fetch|search|browse/i.test(name)) searchBlocks.add(index);
|
|
727
|
+
}
|
|
728
|
+
} else if (type === "content_block_delta") {
|
|
729
|
+
const d = ev.delta || {};
|
|
730
|
+
const index = typeof ev.index === "number" ? ev.index : -1;
|
|
731
|
+
if (d.type === "input_json_delta" && typeof d.partial_json === "string") {
|
|
732
|
+
const t = toolInputs.get(index);
|
|
733
|
+
if (t) t.buf += d.partial_json;
|
|
734
|
+
} else if (d.type === "citation_start_delta") {
|
|
735
|
+
const c = d.citation || {};
|
|
736
|
+
if (typeof c.url === "string") {
|
|
737
|
+
const src = ensure(c.url);
|
|
738
|
+
if (src) {
|
|
739
|
+
src.cited = true;
|
|
740
|
+
src.timesCited++;
|
|
741
|
+
if (!src.title && typeof c.title === "string") src.title = c.title.slice(0, 200);
|
|
742
|
+
if (typeof c.cited_text === "string" && c.cited_text) {
|
|
743
|
+
snippets.set(normUrl(c.url), { url: c.url, domain: domainOf(c.url), title: src.title, text: String(c.cited_text).slice(0, 400) });
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
} else if (type === "content_block_stop") {
|
|
749
|
+
const index = typeof ev.index === "number" ? ev.index : -1;
|
|
750
|
+
if (index >= 0) finalizeTool(index);
|
|
751
|
+
} else if (type === "message_delta" || type === "message_stop" || type === "message_start") {
|
|
752
|
+
const d = ev.delta || ev.message || {};
|
|
753
|
+
if (typeof d.stop_reason === "string" && !meta.finishType) meta.finishType = d.stop_reason;
|
|
754
|
+
if (typeof d.model === "string" && !meta.model) meta.model = d.model;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
for (const index of searchBlocks) finalizeTool(index);
|
|
758
|
+
for (const m of raw.matchAll(/"title"\s*:\s*"((?:[^"\\]|\\.){1,200})"\s*,\s*"url"\s*:\s*"(https?:\/\/[^"\\]+)"/g)) {
|
|
759
|
+
const src = ensure(m[2]);
|
|
760
|
+
if (src && !src.title) src.title = unesc(m[1]).slice(0, 200);
|
|
761
|
+
}
|
|
762
|
+
for (const m of raw.matchAll(/"url"\s*:\s*"(https?:\/\/[^"\\]+)"/g)) ensure(m[1]);
|
|
763
|
+
for (const m of raw.matchAll(/\\"title\\"\s*:\s*\\"((?:[^"\\]|\\.){1,160}?)\\"[\s\S]{0,160}?\\"url\\"\s*:\s*\\"(https?:[^"\\]+)\\"/g)) {
|
|
764
|
+
const src = ensure(m[2]);
|
|
765
|
+
if (src && !src.title) src.title = unesc(unesc(m[1])).slice(0, 200);
|
|
766
|
+
}
|
|
767
|
+
for (const m of raw.matchAll(/\\"url\\"\s*:\s*\\"(https?:[^"\\]+)\\"/g)) ensure(m[1]);
|
|
768
|
+
if (!meta.model) {
|
|
769
|
+
const mm = raw.match(/"model"\s*:\s*"(claude[^"]+)"/);
|
|
770
|
+
if (mm) meta.model = mm[1];
|
|
771
|
+
}
|
|
772
|
+
meta.rounds = queries.size > 0 ? searchBlocks.size : 0;
|
|
773
|
+
for (const m of raw.matchAll(/"([a-z_]*(?:quer|search)[a-z_]*)"\s*:/gi)) unmapped.add(m[1]);
|
|
774
|
+
const ordered = [...sources.values()].sort((a, b) => a.cited === b.cited ? b.timesCited - a.timesCited : a.cited ? -1 : 1).map((s) => ({
|
|
775
|
+
url: s.url,
|
|
776
|
+
domain: s.domain,
|
|
777
|
+
title: s.title || s.domain,
|
|
778
|
+
cited: s.cited,
|
|
779
|
+
timesCited: s.timesCited,
|
|
780
|
+
snippet: snippets.get(normUrl(s.url))?.text || "",
|
|
781
|
+
round: s.round
|
|
782
|
+
}));
|
|
783
|
+
return {
|
|
784
|
+
subQueries: [...queries],
|
|
785
|
+
sources: ordered,
|
|
786
|
+
snippets: [...snippets.values()],
|
|
787
|
+
meta,
|
|
788
|
+
unmappedKeys: [...unmapped].slice(0, 50)
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// src/services/fanout/adapters/index.ts
|
|
794
|
+
var FANOUT_ADAPTERS = [chatgptAdapter, claudeAdapter];
|
|
795
|
+
function adapterForHost(hostname) {
|
|
796
|
+
return FANOUT_ADAPTERS.find((a) => a.matchesHost(hostname)) ?? null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/services/fanout/build.ts
|
|
800
|
+
function buildFanoutResult(ctx) {
|
|
801
|
+
const { platform, parsed, ready, rawBytes } = ctx;
|
|
802
|
+
const browsedUrls = parsed.sources;
|
|
803
|
+
const citedUrls = browsedUrls.filter((s) => s.cited);
|
|
804
|
+
const browsedOnly = browsedUrls.filter((s) => !s.cited);
|
|
805
|
+
const prompt = ctx.prompt || parsed.meta.title || "";
|
|
806
|
+
const result = {
|
|
807
|
+
platform,
|
|
808
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
809
|
+
prompt,
|
|
810
|
+
meta: parsed.meta,
|
|
811
|
+
subQueries: parsed.subQueries,
|
|
812
|
+
browsedUrls,
|
|
813
|
+
citedUrls,
|
|
814
|
+
browsedOnly,
|
|
815
|
+
snippets: parsed.snippets,
|
|
816
|
+
counts: {
|
|
817
|
+
subQueries: parsed.subQueries.length,
|
|
818
|
+
browsed: browsedUrls.length,
|
|
819
|
+
cited: citedUrls.length,
|
|
820
|
+
browsedOnly: browsedOnly.length
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
if (parsed.subQueries.length === 0 && browsedUrls.length === 0) {
|
|
824
|
+
result.debug = {
|
|
825
|
+
interceptorReady: ready,
|
|
826
|
+
rawBytes,
|
|
827
|
+
unmappedKeys: parsed.unmappedKeys,
|
|
828
|
+
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."
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/services/fanout/classify.ts
|
|
835
|
+
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;
|
|
836
|
+
var ENCYCLOPEDIA = /(^|\.)(wikipedia\.org|wikimedia\.org|britannica\.com|merriam-webster\.com|dictionary\.com|fandom\.com|investopedia\.com)$/i;
|
|
837
|
+
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;
|
|
838
|
+
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;
|
|
839
|
+
function classifyDomain(domain, firstPartyDomain) {
|
|
840
|
+
const d = domain.toLowerCase().replace(/^www\./, "");
|
|
841
|
+
if (firstPartyDomain) {
|
|
842
|
+
const fp = firstPartyDomain.toLowerCase().replace(/^www\./, "");
|
|
843
|
+
if (fp && (d === fp || d.endsWith("." + fp))) return "First-party/vendor";
|
|
844
|
+
}
|
|
845
|
+
if (/(^|\.)reddit\.com$/i.test(d)) return "Reddit";
|
|
846
|
+
if (SOCIAL.test(d)) return "Social/video";
|
|
847
|
+
if (ENCYCLOPEDIA.test(d)) return "Encyclopedia";
|
|
848
|
+
if (REVIEW.test(d)) return "Review site";
|
|
849
|
+
if (NEWS.test(d) || /(^|\.)news\./i.test(d)) return "News/media";
|
|
850
|
+
if (/^docs?\.|^developer\.|^developers\.|readthedocs|\.dev$/i.test(d)) return "Docs";
|
|
851
|
+
return "Blog";
|
|
852
|
+
}
|
|
853
|
+
function emptyCategoryCounts() {
|
|
854
|
+
return { "First-party/vendor": 0, "News/media": 0, Reddit: 0, "Social/video": 0, Encyclopedia: 0, "Review site": 0, Docs: 0, Blog: 0 };
|
|
855
|
+
}
|
|
856
|
+
function enrichFanout(result, opts = {}) {
|
|
857
|
+
const addSiteType = (s) => ({ ...s, siteType: classifyDomain(s.domain, opts.firstPartyDomain) });
|
|
858
|
+
const browsedUrls = result.browsedUrls.map(addSiteType);
|
|
859
|
+
const citedUrls = result.citedUrls.map(addSiteType);
|
|
860
|
+
const browsedOnly = result.browsedOnly.map(addSiteType);
|
|
861
|
+
const byCategory = emptyCategoryCounts();
|
|
862
|
+
const domainAgg = /* @__PURE__ */ new Map();
|
|
863
|
+
for (const s of browsedUrls) {
|
|
864
|
+
byCategory[s.siteType]++;
|
|
865
|
+
const cur = domainAgg.get(s.domain);
|
|
866
|
+
if (!cur) domainAgg.set(s.domain, { domain: s.domain, count: 1, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType });
|
|
867
|
+
else {
|
|
868
|
+
cur.count++;
|
|
869
|
+
cur.cited = cur.cited || s.cited;
|
|
870
|
+
cur.timesCited += s.timesCited;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
const topSites = [...domainAgg.values()].sort((a, b) => b.count - a.count || b.timesCited - a.timesCited);
|
|
874
|
+
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 }));
|
|
875
|
+
return {
|
|
876
|
+
platform: result.platform,
|
|
877
|
+
capturedAt: result.capturedAt,
|
|
878
|
+
prompt: result.prompt,
|
|
879
|
+
meta: result.meta,
|
|
880
|
+
queries: result.subQueries,
|
|
881
|
+
browsedUrls,
|
|
882
|
+
citedUrls,
|
|
883
|
+
browsedOnly,
|
|
884
|
+
snippets: result.snippets,
|
|
885
|
+
counts: result.counts,
|
|
886
|
+
aggregates: { topSites, citationOrder, byCategory },
|
|
887
|
+
firstPartyDomain: opts.firstPartyDomain ? opts.firstPartyDomain.toLowerCase().replace(/^www\./, "") : null,
|
|
888
|
+
...result.debug ? { debug: result.debug } : {}
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/services/fanout/export.ts
|
|
236
893
|
var import_node_fs2 = require("fs");
|
|
894
|
+
var import_node_os3 = require("os");
|
|
237
895
|
var import_node_path3 = require("path");
|
|
238
|
-
var
|
|
896
|
+
var import_papaparse = __toESM(require("papaparse"), 1);
|
|
897
|
+
function outputBaseDir() {
|
|
898
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path3.join)((0, import_node_os3.homedir)(), "Downloads", "mcp-scraper");
|
|
899
|
+
}
|
|
900
|
+
function safe(value) {
|
|
901
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "capture";
|
|
902
|
+
}
|
|
903
|
+
function writeTable(path, rows, delimiter) {
|
|
904
|
+
(0, import_node_fs2.writeFileSync)(path, import_papaparse.default.unparse(rows.length ? rows : [{}], { delimiter }));
|
|
905
|
+
}
|
|
906
|
+
function exportFanout(enriched) {
|
|
907
|
+
const stamp = safe(enriched.capturedAt.replace(/[:.]/g, "-"));
|
|
908
|
+
const dir = (0, import_node_path3.join)(outputBaseDir(), "fanout", `${stamp}-${safe(enriched.platform)}`);
|
|
909
|
+
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
910
|
+
const queryRows = enriched.queries.map((q, i) => ({ index: i + 1, query: q }));
|
|
911
|
+
const citationRows = enriched.aggregates.citationOrder.map((c) => {
|
|
912
|
+
const src = enriched.citedUrls.find((s) => s.url === c.url);
|
|
913
|
+
return { rank: c.rank, domain: c.domain, url: c.url, timesCited: c.timesCited, siteType: src?.siteType ?? "", title: src?.title ?? "" };
|
|
914
|
+
});
|
|
915
|
+
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 ?? "" }));
|
|
916
|
+
const domainRows = enriched.aggregates.topSites.map((d) => ({ domain: d.domain, count: d.count, cited: d.cited, timesCited: d.timesCited, siteType: d.siteType }));
|
|
917
|
+
const paths = {
|
|
918
|
+
dir,
|
|
919
|
+
json: (0, import_node_path3.join)(dir, "fanout.json"),
|
|
920
|
+
queriesCsv: (0, import_node_path3.join)(dir, "queries.csv"),
|
|
921
|
+
queriesTsv: (0, import_node_path3.join)(dir, "queries.tsv"),
|
|
922
|
+
citationsCsv: (0, import_node_path3.join)(dir, "citations.csv"),
|
|
923
|
+
sourcesCsv: (0, import_node_path3.join)(dir, "sources.csv"),
|
|
924
|
+
domainsCsv: (0, import_node_path3.join)(dir, "domains.csv"),
|
|
925
|
+
report: (0, import_node_path3.join)(dir, "report.html")
|
|
926
|
+
};
|
|
927
|
+
(0, import_node_fs2.writeFileSync)(paths.json, JSON.stringify(enriched, null, 2));
|
|
928
|
+
writeTable(paths.queriesCsv, queryRows, ",");
|
|
929
|
+
writeTable(paths.queriesTsv, queryRows, " ");
|
|
930
|
+
writeTable(paths.citationsCsv, citationRows, ",");
|
|
931
|
+
writeTable(paths.sourcesCsv, sourceRows, ",");
|
|
932
|
+
writeTable(paths.domainsCsv, domainRows, ",");
|
|
933
|
+
(0, import_node_fs2.writeFileSync)(paths.report, renderReportHtml(enriched));
|
|
934
|
+
return paths;
|
|
935
|
+
}
|
|
936
|
+
function esc(s) {
|
|
937
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
938
|
+
}
|
|
939
|
+
function renderReportHtml(enriched) {
|
|
940
|
+
const data = JSON.stringify(enriched).replace(/</g, "\\u003c");
|
|
941
|
+
return `<!doctype html>
|
|
942
|
+
<html lang="en">
|
|
943
|
+
<head>
|
|
944
|
+
<meta charset="utf-8" />
|
|
945
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
946
|
+
<title>AI Search Fan-Out \u2014 ${esc(enriched.platform)}</title>
|
|
947
|
+
<style>
|
|
948
|
+
:root{--bg:#0b0d12;--panel:#151922;--line:#252b38;--ink:#e8ecf3;--mut:#8b95a7;--acc:#5b9dff;--cite:#39d98a;--browse:#5a6677}
|
|
949
|
+
*{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}
|
|
950
|
+
.wrap{max-width:980px;margin:0 auto;padding:28px 20px 80px}
|
|
951
|
+
h1{font-size:20px;margin:0 0 2px}.sub{color:var(--mut);margin:0 0 20px;font-size:13px}
|
|
952
|
+
.stat{font-size:30px;font-weight:700}.stat small{font-size:13px;font-weight:400;color:var(--mut)}
|
|
953
|
+
.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}
|
|
954
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
|
|
955
|
+
.card b{font-size:22px;display:block}.card span{color:var(--mut);font-size:12px}
|
|
956
|
+
.bars{margin:8px 0 22px}.bar{display:flex;align-items:center;gap:8px;margin:4px 0}
|
|
957
|
+
.bar .lab{width:150px;color:var(--mut);font-size:12px;text-align:right}.bar .track{flex:1;background:#10141c;border-radius:5px;overflow:hidden}
|
|
958
|
+
.bar .fill{background:var(--acc);height:16px}.bar .n{width:34px;font-size:12px;color:var(--mut)}
|
|
959
|
+
.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}
|
|
960
|
+
.tab.on{background:var(--acc);border-color:var(--acc);color:#04101f;font-weight:600}
|
|
961
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
|
|
962
|
+
.chip{background:var(--panel);border:1px solid var(--line);color:var(--mut);padding:4px 10px;border-radius:20px;cursor:pointer;font-size:12px}
|
|
963
|
+
.chip.on{border-color:var(--acc);color:var(--ink)}.chip b{color:var(--ink)}
|
|
964
|
+
.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}
|
|
965
|
+
.row .idx{color:var(--mut);min-width:22px}.row .main{flex:1}
|
|
966
|
+
.tag{font-size:11px;padding:2px 7px;border-radius:6px;border:1px solid var(--line);color:var(--mut);margin-right:5px}
|
|
967
|
+
.dom{font-weight:600}.url{color:var(--mut);font-size:12px;word-break:break-all}
|
|
968
|
+
.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}
|
|
969
|
+
.copy{background:none;border:1px solid var(--line);color:var(--mut);border-radius:6px;cursor:pointer;font-size:11px;padding:2px 8px}
|
|
970
|
+
.hide{display:none}
|
|
971
|
+
</style>
|
|
972
|
+
</head>
|
|
973
|
+
<body><div class="wrap">
|
|
974
|
+
<h1>AI Search Fan-Out</h1>
|
|
975
|
+
<p class="sub" id="sub"></p>
|
|
976
|
+
<div class="stat" id="stat"></div>
|
|
977
|
+
<div class="cards" id="cards"></div>
|
|
978
|
+
<div id="charts"></div>
|
|
979
|
+
<div class="tabs"><button class="tab on" data-tab="q">Queries</button><button class="tab" data-tab="u">URLs</button></div>
|
|
980
|
+
<div class="chips" id="chips"></div>
|
|
981
|
+
<div id="list"></div>
|
|
982
|
+
</div>
|
|
983
|
+
<script>
|
|
984
|
+
const D=${data};
|
|
985
|
+
const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
|
|
986
|
+
document.getElementById('sub').textContent=D.platform+' \xB7 '+(D.meta.model||'model n/a')+' \xB7 '+D.meta.rounds+' search round(s) \xB7 prompt: '+(D.prompt||'');
|
|
987
|
+
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>';
|
|
988
|
+
const cards=[['Sub-queries',D.counts.subQueries],['Researched',D.counts.browsed],['Cited',D.counts.cited],['Browsed-only',D.counts.browsedOnly]];
|
|
989
|
+
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)});
|
|
990
|
+
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}
|
|
991
|
+
const charts=document.getElementById('charts');
|
|
992
|
+
charts.appendChild(barChart('URL Categories',D.aggregates.byCategory));
|
|
993
|
+
let tab='q',filter=null;
|
|
994
|
+
const chips=document.getElementById('chips'),list=document.getElementById('list');
|
|
995
|
+
function render(){
|
|
996
|
+
chips.innerHTML='';list.innerHTML='';
|
|
997
|
+
if(tab==='q'){
|
|
998
|
+
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)});
|
|
999
|
+
} else {
|
|
1000
|
+
const counts={};D.browsedUrls.forEach(s=>{counts[s.siteType]=(counts[s.siteType]||0)+1});
|
|
1001
|
+
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)});
|
|
1002
|
+
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)});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
|
1006
|
+
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()});
|
|
1007
|
+
render();
|
|
1008
|
+
</script>
|
|
1009
|
+
</body></html>`;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/services/fanout/run-capture.ts
|
|
1013
|
+
var COMPOSER_SELECTORS = [
|
|
1014
|
+
"#prompt-textarea",
|
|
1015
|
+
"div#prompt-textarea",
|
|
1016
|
+
'textarea[data-testid="prompt-textarea"]',
|
|
1017
|
+
'div.ProseMirror[contenteditable="true"]',
|
|
1018
|
+
'div[contenteditable="true"]',
|
|
1019
|
+
"textarea"
|
|
1020
|
+
];
|
|
1021
|
+
async function sendPrompt(page, text) {
|
|
1022
|
+
for (const selector of COMPOSER_SELECTORS) {
|
|
1023
|
+
const locator = page.locator(selector).first();
|
|
1024
|
+
try {
|
|
1025
|
+
await locator.waitFor({ state: "visible", timeout: 12e3 });
|
|
1026
|
+
} catch {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
await locator.click({ timeout: 4e3 }).catch(() => void 0);
|
|
1030
|
+
await locator.focus().catch(() => void 0);
|
|
1031
|
+
await page.keyboard.type(text, { delay: 12 });
|
|
1032
|
+
await page.waitForTimeout(300).catch(() => void 0);
|
|
1033
|
+
await page.keyboard.press("Enter");
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
async function waitForAnswer(page, cap, baseline, waitMs) {
|
|
1039
|
+
const start = Date.now();
|
|
1040
|
+
while (Date.now() - start < waitMs && cap.answerStreamCount() <= baseline) {
|
|
1041
|
+
await page.waitForTimeout(400).catch(() => void 0);
|
|
1042
|
+
}
|
|
1043
|
+
await page.waitForTimeout(1e3).catch(() => void 0);
|
|
1044
|
+
}
|
|
1045
|
+
async function runFanoutCapture(page, input) {
|
|
1046
|
+
const cap = new FanoutCdpCapture();
|
|
1047
|
+
await cap.attach(page);
|
|
1048
|
+
try {
|
|
1049
|
+
const baseline = cap.answerStreamCount();
|
|
1050
|
+
if (input.prompt) await sendPrompt(page, input.prompt).catch(() => false);
|
|
1051
|
+
const waitMs = input.wait_ms ?? (input.prompt ? 9e4 : 8e3);
|
|
1052
|
+
await waitForAnswer(page, cap, baseline, waitMs);
|
|
1053
|
+
await cap.drain();
|
|
1054
|
+
let hostname = "";
|
|
1055
|
+
try {
|
|
1056
|
+
hostname = new URL(page.url()).hostname;
|
|
1057
|
+
} catch {
|
|
1058
|
+
hostname = "";
|
|
1059
|
+
}
|
|
1060
|
+
const adapter = adapterForHost(hostname);
|
|
1061
|
+
if (!adapter) {
|
|
1062
|
+
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.`);
|
|
1063
|
+
}
|
|
1064
|
+
const text = cap.bestAnswerText();
|
|
1065
|
+
const parsed = adapter.parse(text, input.prompt || "");
|
|
1066
|
+
const result = buildFanoutResult({ platform: adapter.platform, parsed, prompt: input.prompt || "", ready: true, rawBytes: text.length });
|
|
1067
|
+
const enriched = enrichFanout(result, { firstPartyDomain: input.first_party_domain });
|
|
1068
|
+
const exports2 = input.export ? exportFanout(enriched) : null;
|
|
1069
|
+
return { result: enriched, exports: exports2 };
|
|
1070
|
+
} finally {
|
|
1071
|
+
await cap.detach().catch(() => void 0);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// src/services/browser-agent/local-browser-agent-service.ts
|
|
1076
|
+
import_playwright_extra.chromium.use((0, import_puppeteer_extra_plugin_stealth.default)());
|
|
239
1077
|
function directProfileDir() {
|
|
240
1078
|
return process.env.MCP_SCRAPER_BROWSER_PROFILE_DIR?.trim() || void 0;
|
|
241
1079
|
}
|
|
242
1080
|
function configuredProfileName(profile) {
|
|
243
|
-
return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directProfileDir() ? (0,
|
|
1081
|
+
return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directProfileDir() ? (0, import_node_path4.basename)(directProfileDir()) : "default");
|
|
244
1082
|
}
|
|
245
1083
|
function launchExecutablePath(profile) {
|
|
246
1084
|
const configured = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || profile.browserExecutablePath || defaultChromeExecutablePath();
|
|
247
|
-
return configured && (0,
|
|
1085
|
+
return configured && (0, import_node_fs3.existsSync)(configured) ? configured : void 0;
|
|
248
1086
|
}
|
|
249
1087
|
function ignoreDefaultArgsFor(executablePath) {
|
|
250
1088
|
if (!executablePath) return void 0;
|
|
@@ -517,6 +1355,17 @@ var LocalBrowserAgentService = class {
|
|
|
517
1355
|
list(includeClosed = false) {
|
|
518
1356
|
return Array.from(this.sessions.values()).filter((session) => includeClosed || session.status === "open").map(sessionSummary);
|
|
519
1357
|
}
|
|
1358
|
+
async captureFanout(sessionId, input) {
|
|
1359
|
+
const session = this.requireOpenSession(sessionId);
|
|
1360
|
+
this.touch(session);
|
|
1361
|
+
return runFanoutCapture(session.page, input);
|
|
1362
|
+
}
|
|
1363
|
+
async screenshotOnly(sessionId) {
|
|
1364
|
+
const session = this.requireOpenSession(sessionId);
|
|
1365
|
+
await this.waitForPage(session.page);
|
|
1366
|
+
const img = await session.page.screenshot({ type: "png", fullPage: false });
|
|
1367
|
+
return img.toString("base64");
|
|
1368
|
+
}
|
|
520
1369
|
async resolveProfile(profile) {
|
|
521
1370
|
const directDir = directProfileDir();
|
|
522
1371
|
const requestedName = configuredProfileName(profile);
|
|
@@ -540,7 +1389,7 @@ var LocalBrowserAgentService = class {
|
|
|
540
1389
|
const existing = this.contexts.get(profile.userDataDir);
|
|
541
1390
|
if (existing) return existing;
|
|
542
1391
|
const executablePath = launchExecutablePath(profile);
|
|
543
|
-
const context = await
|
|
1392
|
+
const context = await import_playwright_extra.chromium.launchPersistentContext(profile.userDataDir, {
|
|
544
1393
|
headless: false,
|
|
545
1394
|
viewport: { width: 1440, height: 900 },
|
|
546
1395
|
acceptDownloads: true,
|
|
@@ -587,7 +1436,7 @@ var LocalBrowserAgentService = class {
|
|
|
587
1436
|
};
|
|
588
1437
|
|
|
589
1438
|
// src/version.ts
|
|
590
|
-
var PACKAGE_VERSION = "0.
|
|
1439
|
+
var PACKAGE_VERSION = "0.3.1";
|
|
591
1440
|
|
|
592
1441
|
// src/mcp/browser-agent-tool-schemas.ts
|
|
593
1442
|
var import_zod = require("zod");
|
|
@@ -730,6 +1579,70 @@ var BrowserReplayAnnotateInputSchema = {
|
|
|
730
1579
|
var BrowserListInputSchema = {
|
|
731
1580
|
include_closed: import_zod.z.boolean().default(false).describe("Include closed sessions in the list.")
|
|
732
1581
|
};
|
|
1582
|
+
var BrowserCaptureFanoutInputSchema = {
|
|
1583
|
+
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."),
|
|
1584
|
+
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."),
|
|
1585
|
+
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."),
|
|
1586
|
+
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."),
|
|
1587
|
+
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."),
|
|
1588
|
+
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.")
|
|
1589
|
+
};
|
|
1590
|
+
var FanoutSourceOutput = import_zod.z.object({
|
|
1591
|
+
url: import_zod.z.string(),
|
|
1592
|
+
domain: import_zod.z.string(),
|
|
1593
|
+
title: import_zod.z.string(),
|
|
1594
|
+
cited: import_zod.z.boolean(),
|
|
1595
|
+
timesCited: import_zod.z.number().int().min(0),
|
|
1596
|
+
snippet: import_zod.z.string(),
|
|
1597
|
+
round: import_zod.z.number().int().nullable(),
|
|
1598
|
+
siteType: import_zod.z.string().describe("URL category: First-party/vendor, News/media, Reddit, Social/video, Encyclopedia, Review site, Docs, or Blog.")
|
|
1599
|
+
});
|
|
1600
|
+
var BrowserCaptureFanoutOutputSchema = {
|
|
1601
|
+
...BrowserBaseOutput,
|
|
1602
|
+
tool: import_zod.z.literal("browser_capture_fanout"),
|
|
1603
|
+
platform: import_zod.z.enum(["ChatGPT", "Claude"]).describe("Which AI-search surface the fan-out was captured from."),
|
|
1604
|
+
captured_at: import_zod.z.string(),
|
|
1605
|
+
prompt: import_zod.z.string().describe("The user prompt that triggered the captured fan-out, when recoverable."),
|
|
1606
|
+
meta: import_zod.z.object({
|
|
1607
|
+
model: import_zod.z.string(),
|
|
1608
|
+
finishType: import_zod.z.string(),
|
|
1609
|
+
title: import_zod.z.string(),
|
|
1610
|
+
rounds: import_zod.z.number().int().min(0)
|
|
1611
|
+
}),
|
|
1612
|
+
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."),
|
|
1613
|
+
browsed_urls: import_zod.z.array(FanoutSourceOutput).describe("Every researched URL (cited and browsed-only), cited first."),
|
|
1614
|
+
cited_urls: import_zod.z.array(FanoutSourceOutput).describe("The subset of researched URLs actually cited in the final answer."),
|
|
1615
|
+
browsed_only: import_zod.z.array(FanoutSourceOutput).describe("Researched URLs the model pulled but did not cite."),
|
|
1616
|
+
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() })),
|
|
1617
|
+
counts: import_zod.z.object({
|
|
1618
|
+
subQueries: import_zod.z.number().int().min(0),
|
|
1619
|
+
browsed: import_zod.z.number().int().min(0),
|
|
1620
|
+
cited: import_zod.z.number().int().min(0),
|
|
1621
|
+
browsedOnly: import_zod.z.number().int().min(0)
|
|
1622
|
+
}),
|
|
1623
|
+
aggregates: import_zod.z.object({
|
|
1624
|
+
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() })),
|
|
1625
|
+
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() })),
|
|
1626
|
+
byCategory: import_zod.z.record(import_zod.z.number().int())
|
|
1627
|
+
}).describe("Objective aggregates: top sourced sites by frequency, citation order, and URL-category counts."),
|
|
1628
|
+
first_party_domain: import_zod.z.string().nullable(),
|
|
1629
|
+
exports: import_zod.z.object({
|
|
1630
|
+
dir: import_zod.z.string(),
|
|
1631
|
+
json: import_zod.z.string(),
|
|
1632
|
+
queriesCsv: import_zod.z.string(),
|
|
1633
|
+
queriesTsv: import_zod.z.string(),
|
|
1634
|
+
citationsCsv: import_zod.z.string(),
|
|
1635
|
+
sourcesCsv: import_zod.z.string(),
|
|
1636
|
+
domainsCsv: import_zod.z.string(),
|
|
1637
|
+
report: import_zod.z.string()
|
|
1638
|
+
}).nullable().describe("Local file paths when export=true, otherwise null."),
|
|
1639
|
+
debug: import_zod.z.object({
|
|
1640
|
+
interceptorReady: import_zod.z.boolean(),
|
|
1641
|
+
rawBytes: import_zod.z.number().int().min(0),
|
|
1642
|
+
unmappedKeys: import_zod.z.array(import_zod.z.string()),
|
|
1643
|
+
note: import_zod.z.string()
|
|
1644
|
+
}).optional()
|
|
1645
|
+
};
|
|
733
1646
|
var BrowserOpenOutputSchema = {
|
|
734
1647
|
ok: import_zod.z.boolean(),
|
|
735
1648
|
tool: import_zod.z.literal("browser_open"),
|
|
@@ -878,8 +1791,8 @@ var BrowserListSessionsOutputSchema = {
|
|
|
878
1791
|
// src/mcp/replay-annotator.ts
|
|
879
1792
|
var import_node_child_process = require("child_process");
|
|
880
1793
|
var import_promises3 = require("fs/promises");
|
|
881
|
-
var
|
|
882
|
-
var
|
|
1794
|
+
var import_node_os4 = require("os");
|
|
1795
|
+
var import_node_path5 = require("path");
|
|
883
1796
|
var import_node_util = require("util");
|
|
884
1797
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
885
1798
|
function finiteNumber(value) {
|
|
@@ -889,11 +1802,11 @@ function clamp(value, min, max) {
|
|
|
889
1802
|
return Math.min(max, Math.max(min, value));
|
|
890
1803
|
}
|
|
891
1804
|
function formatAssTime(seconds) {
|
|
892
|
-
const
|
|
893
|
-
const hours = Math.floor(
|
|
894
|
-
const minutes = Math.floor(
|
|
895
|
-
const wholeSeconds = Math.floor(
|
|
896
|
-
const centiseconds = Math.floor((
|
|
1805
|
+
const safe2 = Math.max(0, seconds);
|
|
1806
|
+
const hours = Math.floor(safe2 / 3600);
|
|
1807
|
+
const minutes = Math.floor(safe2 % 3600 / 60);
|
|
1808
|
+
const wholeSeconds = Math.floor(safe2 % 60);
|
|
1809
|
+
const centiseconds = Math.floor((safe2 - Math.floor(safe2)) * 100);
|
|
897
1810
|
return `${hours}:${String(minutes).padStart(2, "0")}:${String(wholeSeconds).padStart(2, "0")}.${String(centiseconds).padStart(2, "0")}`;
|
|
898
1811
|
}
|
|
899
1812
|
function cssHexToAssColor(color) {
|
|
@@ -1102,8 +2015,8 @@ function ffmpegFilterPath(path) {
|
|
|
1102
2015
|
async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
1103
2016
|
if (!options.annotations.length) throw new Error("annotations must include at least one item");
|
|
1104
2017
|
const size = await videoSize(inputFilePath);
|
|
1105
|
-
const tmp = await (0, import_promises3.mkdtemp)((0,
|
|
1106
|
-
const assPath = (0,
|
|
2018
|
+
const tmp = await (0, import_promises3.mkdtemp)((0, import_node_path5.join)((0, import_node_os4.tmpdir)(), "mcp-scraper-ass-"));
|
|
2019
|
+
const assPath = (0, import_node_path5.join)(tmp, "annotations.ass");
|
|
1107
2020
|
try {
|
|
1108
2021
|
await (0, import_promises3.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
|
|
1109
2022
|
await execFileAsync("ffmpeg", [
|
|
@@ -1171,8 +2084,8 @@ function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_
|
|
|
1171
2084
|
nextRecommendedTool
|
|
1172
2085
|
});
|
|
1173
2086
|
}
|
|
1174
|
-
function
|
|
1175
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
2087
|
+
function outputBaseDir2() {
|
|
2088
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path6.join)((0, import_node_os5.homedir)(), "Downloads", "mcp-scraper");
|
|
1176
2089
|
}
|
|
1177
2090
|
function safeFilePart(value) {
|
|
1178
2091
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
|
|
@@ -1181,7 +2094,7 @@ function replayFilePath(sessionId, replayId, filename) {
|
|
|
1181
2094
|
const requested = filename?.trim();
|
|
1182
2095
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1183
2096
|
const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
|
|
1184
|
-
return (0,
|
|
2097
|
+
return (0, import_node_path6.join)(outputBaseDir2(), "browser-replays", `${name}.mp4`);
|
|
1185
2098
|
}
|
|
1186
2099
|
function annotatedReplayFilePath(sessionId, replayId, filename) {
|
|
1187
2100
|
const requested = filename?.trim();
|
|
@@ -1246,8 +2159,8 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1246
2159
|
}
|
|
1247
2160
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
1248
2161
|
const filePath = replayFilePath(sessionId, replayId, filename);
|
|
1249
|
-
(0,
|
|
1250
|
-
(0,
|
|
2162
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path6.join)(outputBaseDir2(), "browser-replays"), { recursive: true });
|
|
2163
|
+
(0, import_node_fs4.writeFileSync)(filePath, bytes);
|
|
1251
2164
|
return {
|
|
1252
2165
|
ok: true,
|
|
1253
2166
|
data: {
|
|
@@ -1898,7 +2811,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1898
2811
|
try {
|
|
1899
2812
|
const sourcePath = String(downloaded.data.file_path);
|
|
1900
2813
|
const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
|
|
1901
|
-
(0,
|
|
2814
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path6.join)(outputBaseDir2(), "browser-replays"), { recursive: true });
|
|
1902
2815
|
const result = await annotateReplayVideo(sourcePath, outputPath, {
|
|
1903
2816
|
annotations: input.annotations,
|
|
1904
2817
|
sourceWidth: input.source_width,
|
|
@@ -1991,15 +2904,63 @@ function registerBrowserAgentMcpTools(server2, opts) {
|
|
|
1991
2904
|
});
|
|
1992
2905
|
}
|
|
1993
2906
|
);
|
|
2907
|
+
server2.registerTool(
|
|
2908
|
+
"browser_capture_fanout",
|
|
2909
|
+
{
|
|
2910
|
+
title: "Capture AI Search Fan-Out",
|
|
2911
|
+
description: `Capture the query fan-out behind a ChatGPT or Claude web-search answer for Answer Engine Optimization (AEO): 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 category (First-party/vendor, News/media, Reddit, Social/video, Encyclopedia, Review site, Docs, Blog), plus top sourced sites and citation order. This returns the raw structured data; YOU analyze it \u2014 classify each sub-query by funnel stage (Problem-aware, Solution-aware, Decision-aware, Retention) and type (BoFu, Branded, Comparison, How-to, Search operator), name the brands the model researched, and give AEO insights. Sample asks: "capture the fan-out for this ChatGPT answer", "what did Claude search and cite for best CRM", "show the sub-queries and sources behind this AI answer". WRITE NOTE: passing prompt submits a real message in the user's logged-in account (a new conversation turn) \u2014 only send when the user wants that; omit prompt to capture a prompt the user just ran. Setup, if not already done: import a logged-in session with browser_profile_import (local Chrome) or browser_profile_onboard (hosted Kernel), then browser_open and browser_goto to chatgpt.com or claude.ai. Fan-out is captured only as it streams, so the session must be open when the prompt runs. NOT for Google AI Overview citations \u2014 use harvest_paa for those; this tool is ChatGPT and Claude only.`,
|
|
2912
|
+
inputSchema: BrowserCaptureFanoutInputSchema,
|
|
2913
|
+
outputSchema: BrowserCaptureFanoutOutputSchema,
|
|
2914
|
+
annotations: annotations("Capture AI Search Fan-Out")
|
|
2915
|
+
},
|
|
2916
|
+
async (input) => {
|
|
2917
|
+
const emit = (result, exports2) => structuredResult({
|
|
2918
|
+
ok: true,
|
|
2919
|
+
tool: "browser_capture_fanout",
|
|
2920
|
+
session_id: input.session_id,
|
|
2921
|
+
platform: result.platform,
|
|
2922
|
+
captured_at: result.capturedAt,
|
|
2923
|
+
prompt: result.prompt,
|
|
2924
|
+
meta: result.meta,
|
|
2925
|
+
queries: result.queries,
|
|
2926
|
+
browsed_urls: result.browsedUrls,
|
|
2927
|
+
cited_urls: result.citedUrls,
|
|
2928
|
+
browsed_only: result.browsedOnly,
|
|
2929
|
+
snippets: result.snippets,
|
|
2930
|
+
counts: result.counts,
|
|
2931
|
+
aggregates: result.aggregates,
|
|
2932
|
+
first_party_domain: result.firstPartyDomain,
|
|
2933
|
+
exports: exports2 ?? null,
|
|
2934
|
+
...result.debug ? { debug: result.debug } : {}
|
|
2935
|
+
});
|
|
2936
|
+
if (localBrowserModeEnabled()) {
|
|
2937
|
+
try {
|
|
2938
|
+
const { result, exports: exports2 } = await localBrowser.captureFanout(input.session_id, input);
|
|
2939
|
+
return emit(result, exports2);
|
|
2940
|
+
} catch (err) {
|
|
2941
|
+
return errorResult("browser_capture_fanout", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/capture-fanout`, {
|
|
2945
|
+
prompt: input.prompt,
|
|
2946
|
+
wait_ms: input.wait_ms,
|
|
2947
|
+
first_party_domain: input.first_party_domain,
|
|
2948
|
+
export: false
|
|
2949
|
+
});
|
|
2950
|
+
if (!res.ok) return errorResult("browser_capture_fanout", res.data, input.session_id);
|
|
2951
|
+
const hosted = res.data?.result ?? res.data;
|
|
2952
|
+
return emit(hosted, res.data?.exports ?? null);
|
|
2953
|
+
}
|
|
2954
|
+
);
|
|
1994
2955
|
}
|
|
1995
2956
|
|
|
1996
2957
|
// bin/browser-agent-stdio-server.ts
|
|
1997
2958
|
function readApiKeyFile() {
|
|
1998
2959
|
const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
|
|
1999
|
-
const paths = [explicitPath, (0,
|
|
2960
|
+
const paths = [explicitPath, (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".mcp-scraper-key")].filter(Boolean);
|
|
2000
2961
|
for (const path of paths) {
|
|
2001
2962
|
try {
|
|
2002
|
-
const value = (0,
|
|
2963
|
+
const value = (0, import_node_fs5.readFileSync)(path, "utf8").trim();
|
|
2003
2964
|
if (value) return value;
|
|
2004
2965
|
} catch {
|
|
2005
2966
|
}
|