mcp-scraper 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +18 -26
  2. package/dist/bin/api-server.cjs +158 -321
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +1 -1
  5. package/dist/bin/browser-agent-stdio-server.cjs +39 -1750
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +2 -4
  8. package/dist/bin/browser-agent-stdio-server.js.map +1 -1
  9. package/dist/bin/mcp-scraper-cli.cjs +47 -328
  10. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  11. package/dist/bin/mcp-scraper-cli.js +7 -91
  12. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  13. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +78 -1796
  14. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  15. package/dist/bin/mcp-scraper-combined-stdio-server.js +4 -6
  16. package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
  17. package/dist/bin/mcp-scraper-install.cjs +4 -4
  18. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  19. package/dist/bin/mcp-scraper-install.js +2 -2
  20. package/dist/bin/mcp-stdio-server.cjs +18 -25
  21. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  22. package/dist/bin/mcp-stdio-server.js +2 -2
  23. package/dist/chunk-2NEVRPK3.js +7 -0
  24. package/dist/chunk-2NEVRPK3.js.map +1 -0
  25. package/dist/{chunk-ROZEDHAA.js → chunk-2O7RAK6E.js} +19 -26
  26. package/dist/chunk-2O7RAK6E.js.map +1 -0
  27. package/dist/{chunk-RVYHP7QN.js → chunk-ELI6NDEG.js} +4 -4
  28. package/dist/{chunk-RVYHP7QN.js.map → chunk-ELI6NDEG.js.map} +1 -1
  29. package/dist/{chunk-TK4D2BZ5.js → chunk-IWNLHCKA.js} +13 -694
  30. package/dist/chunk-IWNLHCKA.js.map +1 -0
  31. package/dist/{server-YSKBD2VX.js → server-HRVD52CX.js} +836 -114
  32. package/dist/server-HRVD52CX.js.map +1 -0
  33. package/docs/mcp-tool-craft-lint.generated.md +2 -5
  34. package/docs/mcp-tool-manifest.generated.json +9 -101
  35. package/package.json +1 -1
  36. package/dist/chunk-DUEW4EOO.js +0 -214
  37. package/dist/chunk-DUEW4EOO.js.map +0 -1
  38. package/dist/chunk-IHXHQL53.js +0 -7
  39. package/dist/chunk-IHXHQL53.js.map +0 -1
  40. package/dist/chunk-NXRWFOEZ.js +0 -816
  41. package/dist/chunk-NXRWFOEZ.js.map +0 -1
  42. package/dist/chunk-ROZEDHAA.js.map +0 -1
  43. package/dist/chunk-TK4D2BZ5.js.map +0 -1
  44. package/dist/server-YSKBD2VX.js.map +0 -1
@@ -99,15 +99,12 @@ import {
99
99
  harvestTimeoutBudget,
100
100
  liveWebToolAnnotations,
101
101
  outputBaseDir
102
- } from "./chunk-ROZEDHAA.js";
102
+ } from "./chunk-2O7RAK6E.js";
103
103
  import {
104
104
  CaptchaError,
105
105
  RECAPTCHA_INSTRUCTIONS,
106
106
  sanitizeVendorName
107
107
  } from "./chunk-M2S27J6Z.js";
108
- import {
109
- runFanoutCapture
110
- } from "./chunk-NXRWFOEZ.js";
111
108
  import {
112
109
  browserServiceApiKey,
113
110
  browserServiceProfileName,
@@ -135,12 +132,7 @@ import {
135
132
  RawMapsOverviewSchema,
136
133
  RawMapsReviewStatsSchema
137
134
  } from "./chunk-LFATOGDF.js";
138
- import {
139
- defaultChromeExecutablePath,
140
- loadImportedBrowserProfile,
141
- localBrowserModeEnabled
142
- } from "./chunk-DUEW4EOO.js";
143
- import "./chunk-IHXHQL53.js";
135
+ import "./chunk-2NEVRPK3.js";
144
136
 
145
137
  // src/api/outbound-sanitize.ts
146
138
  var KEY_RENAMES = {
@@ -8374,10 +8366,10 @@ function buildTranscriptMarkdown(result) {
8374
8366
  return lines.join("\n");
8375
8367
  }
8376
8368
  function buildTranscriptHtml(result) {
8377
- const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
8369
+ const esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
8378
8370
  const fmtSecs = (n) => `${Math.floor(n / 60)}:${String(Math.floor(n % 60)).padStart(2, "0")}`;
8379
8371
  const chunks = (result.chunks ?? []).map(
8380
- (ch) => `<div class="chunk"><span class="ts">${fmtSecs(ch.timestamp[0])} \u2192 ${fmtSecs(ch.timestamp[1])}</span><span class="ct">${esc(ch.text.trim())}</span></div>`
8372
+ (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>`
8381
8373
  ).join("\n");
8382
8374
  return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
8383
8375
  ${ATTRIBUTION_PIXEL_SCRIPT}
@@ -8388,10 +8380,10 @@ h1{font-size:1.2rem;margin-bottom:.25rem}.meta{color:#666;font-size:.85rem;margi
8388
8380
  .ts{font-family:monospace;font-size:.75rem;color:#888;white-space:nowrap;flex-shrink:0}
8389
8381
  .ct{font-size:.875rem;flex:1;min-width:0;word-break:break-word}
8390
8382
  </style></head><body>
8391
- <h1>Transcript \u2014 ${esc(result.videoId)}</h1>
8383
+ <h1>Transcript \u2014 ${esc2(result.videoId)}</h1>
8392
8384
  <div class="meta">Transcribed in ${(result.durationMs / 1e3).toFixed(1)}s</div>
8393
8385
  <h2 style="font-size:1.05rem">Full Text</h2>
8394
- <div class="full-text">${esc(result.text)}</div>
8386
+ <div class="full-text">${esc2(result.text)}</div>
8395
8387
  ${chunks.length ? `<h2 style="font-size:1.05rem">Timestamped Segments</h2>
8396
8388
  ${chunks}` : ""}
8397
8389
  </body></html>`;
@@ -9779,9 +9771,9 @@ facebookAdApp.post("/media", createApiKeyAuth(), async (c) => {
9779
9771
  // src/api/instagram-routes.ts
9780
9772
  import { Hono as Hono5 } from "hono";
9781
9773
  import { z as z14 } from "zod";
9782
- import { createWriteStream as createWriteStream2, existsSync, mkdirSync as mkdirSync2, statSync, writeFileSync } from "fs";
9774
+ import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync2, statSync, writeFileSync } from "fs";
9783
9775
  import { homedir as homedir2 } from "os";
9784
- import { basename as basename2, join as join4 } from "path";
9776
+ import { join as join4 } from "path";
9785
9777
  import { Readable as Readable2 } from "stream";
9786
9778
  import { pipeline as pipeline2 } from "stream/promises";
9787
9779
  import { spawn } from "child_process";
@@ -10142,19 +10134,13 @@ async function extractInstagramMediaFromPage(page, sourceUrl, capturedMediaUrls
10142
10134
  }
10143
10135
 
10144
10136
  // src/api/instagram-routes.ts
10145
- var InstagramBrowserModeSchema = z14.enum(["auto", "hosted", "local"]);
10146
- var InstagramBrowserOptionsSchema = {
10147
- browserMode: InstagramBrowserModeSchema.default("auto"),
10148
- browserProfile: z14.string().trim().min(1).optional()
10149
- };
10150
10137
  var InstagramProfileContentBodySchema = z14.object({
10151
10138
  handle: z14.string().trim().optional(),
10152
10139
  url: z14.string().trim().optional(),
10153
10140
  maxItems: z14.number().int().min(1).max(500).default(50),
10154
10141
  maxScrolls: z14.number().int().min(0).max(100).default(10),
10155
10142
  scrollDelayMs: z14.number().int().min(250).max(5e3).default(1200),
10156
- stableScrollLimit: z14.number().int().min(1).max(10).default(4),
10157
- ...InstagramBrowserOptionsSchema
10143
+ stableScrollLimit: z14.number().int().min(1).max(10).default(4)
10158
10144
  }).refine((d) => !!d.handle || !!d.url, {
10159
10145
  message: "handle or url is required"
10160
10146
  });
@@ -10165,108 +10151,30 @@ var InstagramMediaDownloadBodySchema = z14.object({
10165
10151
  downloadMedia: z14.boolean().default(true),
10166
10152
  downloadAllTracks: z14.boolean().default(false),
10167
10153
  includeTranscript: z14.boolean().default(false),
10168
- mux: z14.boolean().default(true),
10169
- ...InstagramBrowserOptionsSchema
10154
+ mux: z14.boolean().default(true)
10170
10155
  });
10171
10156
  var instagramApp = new Hono5();
10172
10157
  function invalidRequest2(message) {
10173
10158
  return { error_code: "invalid_request", message };
10174
10159
  }
10175
- function directProfileDir() {
10176
- return process.env.MCP_SCRAPER_BROWSER_PROFILE_DIR?.trim() || void 0;
10177
- }
10178
- function configuredProfileName(profile) {
10179
- const directDir = directProfileDir();
10180
- return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directDir ? basename2(directDir) : "default");
10181
- }
10182
- function launchExecutablePath(manifestExecutablePath) {
10183
- const configured = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || manifestExecutablePath || defaultChromeExecutablePath();
10184
- return configured && existsSync(configured) ? configured : void 0;
10185
- }
10186
- function ignoreDefaultArgsFor(executablePath) {
10187
- if (!executablePath) return void 0;
10188
- if (process.platform !== "darwin") return void 0;
10189
- if (!/Google Chrome\.app\/Contents\/MacOS\/Google Chrome$/.test(executablePath)) return void 0;
10190
- return ["--password-store=basic", "--use-mock-keychain"];
10191
- }
10192
- async function kernelLaunchOptsResidential2() {
10193
- let proxyId = browserServiceProxyId();
10194
- try {
10195
- const resolution = await resolveKernelProxyId({
10196
- kernelApiKey: browserServiceApiKey(),
10197
- proxyMode: DEFAULT_PROXY_MODE,
10198
- configuredKernelProxyId: browserServiceProxyId(),
10199
- gl: "us"
10200
- });
10201
- if (resolution.kernelProxyId) proxyId = resolution.kernelProxyId;
10202
- } catch {
10203
- proxyId = browserServiceProxyId();
10204
- }
10160
+ async function kernelLaunchOptsDirect() {
10205
10161
  return {
10206
10162
  headless: true,
10207
10163
  kernelApiKey: browserServiceApiKey(),
10208
- kernelProxyId: proxyId,
10209
10164
  viewport: { width: 1280, height: 900 },
10210
10165
  locale: "en-US"
10211
10166
  };
10212
10167
  }
10213
- async function resolveInstagramLaunch(input) {
10214
- const requestedMode = input.browserMode ?? "auto";
10215
- const directDir = directProfileDir();
10216
- const useLocal = requestedMode === "local" || requestedMode === "auto" && (Boolean(input.browserProfile) || Boolean(directDir) || localBrowserModeEnabled());
10217
- if (!useLocal) {
10218
- return {
10219
- config: await kernelLaunchOptsResidential2(),
10220
- browser: {
10221
- mode: "hosted",
10222
- requestedMode,
10223
- profileName: null,
10224
- profileSource: "hosted",
10225
- profileDirConfigured: false,
10226
- executablePathConfigured: false
10227
- }
10228
- };
10229
- }
10230
- const requestedName = configuredProfileName(input.browserProfile);
10231
- if (directDir && (!input.browserProfile || requestedName === process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim())) {
10232
- const executablePath2 = launchExecutablePath(null);
10233
- return {
10234
- config: {
10235
- headless: false,
10236
- profileDir: directDir,
10237
- browserExecutablePath: executablePath2,
10238
- ignoreDefaultArgs: ignoreDefaultArgsFor(executablePath2),
10239
- viewport: { width: 1280, height: 900 },
10240
- locale: "en-US"
10241
- },
10242
- browser: {
10243
- mode: "local",
10244
- requestedMode,
10245
- profileName: requestedName,
10246
- profileSource: "direct_profile_dir",
10247
- profileDirConfigured: true,
10248
- executablePathConfigured: Boolean(executablePath2)
10249
- }
10250
- };
10251
- }
10252
- const manifest = await loadImportedBrowserProfile(requestedName);
10253
- const executablePath = launchExecutablePath(manifest.browserExecutablePath);
10168
+ async function resolveInstagramLaunch() {
10254
10169
  return {
10255
- config: {
10256
- headless: false,
10257
- profileDir: manifest.userDataDir,
10258
- browserExecutablePath: executablePath,
10259
- ignoreDefaultArgs: ignoreDefaultArgsFor(executablePath),
10260
- viewport: { width: 1280, height: 900 },
10261
- locale: "en-US"
10262
- },
10170
+ config: await kernelLaunchOptsDirect(),
10263
10171
  browser: {
10264
- mode: "local",
10265
- requestedMode,
10266
- profileName: manifest.name,
10267
- profileSource: "managed_profile",
10268
- profileDirConfigured: true,
10269
- executablePathConfigured: Boolean(executablePath)
10172
+ mode: "hosted",
10173
+ requestedMode: "hosted",
10174
+ profileName: null,
10175
+ profileSource: "hosted",
10176
+ profileDirConfigured: false,
10177
+ executablePathConfigured: false
10270
10178
  }
10271
10179
  };
10272
10180
  }
@@ -10354,7 +10262,7 @@ instagramApp.post("/profile-content", createApiKeyAuth(), async (c) => {
10354
10262
  const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.instagram_profile, LedgerOperation.INSTAGRAM_PROFILE, target.profileUrl);
10355
10263
  if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.instagram_profile), 402);
10356
10264
  debited = true;
10357
- const launch = await resolveInstagramLaunch(body);
10265
+ const launch = await resolveInstagramLaunch();
10358
10266
  await driver.launch(launch.config);
10359
10267
  await driver.navigateTo(target.profileUrl);
10360
10268
  const page = driver.getPage();
@@ -10401,7 +10309,7 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
10401
10309
  const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.instagram_media, LedgerOperation.INSTAGRAM_MEDIA, sourceUrl.href);
10402
10310
  if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.instagram_media), 402);
10403
10311
  mediaDebited = true;
10404
- const launch = await resolveInstagramLaunch(body);
10312
+ const launch = await resolveInstagramLaunch();
10405
10313
  await driver.launch(launch.config);
10406
10314
  const page = driver.getPage();
10407
10315
  const capturedMediaUrls = [];
@@ -13560,6 +13468,820 @@ async function listReplayRows(sessionId) {
13560
13468
  // src/services/browser-agent/browser-agent-service.ts
13561
13469
  import Kernel3 from "@onkernel/sdk";
13562
13470
  import { chromium as playwrightChromium } from "playwright";
13471
+
13472
+ // src/services/fanout/cdp-capture.ts
13473
+ var ANSWER_RE = /\/backend-api\/(f\/)?conversation$|\/chat_conversations\/[^/]+\/(completion|retry_completion)$|\/completion$/i;
13474
+ var FanoutCdpCapture = class {
13475
+ client = null;
13476
+ reqUrl = /* @__PURE__ */ new Map();
13477
+ ctById = /* @__PURE__ */ new Map();
13478
+ answerIds = /* @__PURE__ */ new Set();
13479
+ bodies = [];
13480
+ pending = [];
13481
+ answerStreams = 0;
13482
+ async attach(page) {
13483
+ if (this.client) return;
13484
+ const client2 = await page.context().newCDPSession(page);
13485
+ this.client = client2;
13486
+ await client2.send("Network.enable", { maxTotalBufferSize: 2e8, maxResourceBufferSize: 2e8 });
13487
+ client2.on("Network.requestWillBeSent", (e) => {
13488
+ const id = e.requestId;
13489
+ const req = e.request;
13490
+ if (id) this.reqUrl.set(id, req?.url || "");
13491
+ });
13492
+ client2.on("Network.responseReceived", (e) => {
13493
+ const id = e.requestId;
13494
+ const res = e.response;
13495
+ const url = res?.url || this.reqUrl.get(id) || "";
13496
+ const headers = res?.headers || {};
13497
+ const ct = String(headers["content-type"] || headers["Content-Type"] || "");
13498
+ if (id) this.ctById.set(id, ct);
13499
+ if (id && (this.isAnswerUrl(url) || /event-stream/i.test(ct))) this.answerIds.add(id);
13500
+ });
13501
+ client2.on("Network.loadingFinished", (e) => this.grab(e.requestId));
13502
+ }
13503
+ reset() {
13504
+ this.bodies = [];
13505
+ this.answerStreams = 0;
13506
+ this.answerIds.clear();
13507
+ this.pending = [];
13508
+ }
13509
+ answerStreamCount() {
13510
+ return this.answerStreams;
13511
+ }
13512
+ async drain() {
13513
+ await Promise.all(this.pending);
13514
+ }
13515
+ bestAnswerText() {
13516
+ const eventStreams = this.bodies.filter((b) => /event-stream/i.test(b.ct));
13517
+ const answerStreams = eventStreams.filter((b) => this.isAnswerUrl(b.url)).sort((a, b) => b.text.length - a.text.length);
13518
+ if (answerStreams.length) return answerStreams.map((b) => b.text).join("\n");
13519
+ const answerJson = this.bodies.filter((b) => this.isAnswerUrl(b.url)).sort((a, b) => b.text.length - a.text.length);
13520
+ if (answerJson.length) return answerJson[0].text;
13521
+ if (eventStreams.length) return eventStreams.sort((a, b) => b.text.length - a.text.length)[0].text;
13522
+ return "";
13523
+ }
13524
+ async detach() {
13525
+ try {
13526
+ await this.client?.detach();
13527
+ } catch {
13528
+ this.client = null;
13529
+ }
13530
+ this.client = null;
13531
+ }
13532
+ isAnswerUrl(url) {
13533
+ let path5 = "";
13534
+ try {
13535
+ path5 = new URL(url).pathname;
13536
+ } catch {
13537
+ path5 = String(url || "").split("?")[0];
13538
+ }
13539
+ return ANSWER_RE.test(path5);
13540
+ }
13541
+ grab(requestId) {
13542
+ if (!requestId || !this.client || !this.answerIds.has(requestId)) return;
13543
+ const url = this.reqUrl.get(requestId) || "";
13544
+ const ct = this.ctById.get(requestId) || "";
13545
+ const client2 = this.client;
13546
+ const p = client2.send("Network.getResponseBody", { requestId }).then((r) => {
13547
+ const text = r.base64Encoded ? Buffer.from(String(r.body), "base64").toString("utf8") : String(r.body || "");
13548
+ if (text) {
13549
+ this.bodies.push({ url, ct, text });
13550
+ if (this.isAnswerUrl(url)) this.answerStreams++;
13551
+ }
13552
+ }).catch(() => void 0);
13553
+ this.pending.push(p);
13554
+ }
13555
+ };
13556
+
13557
+ // src/services/fanout/parse-helpers.ts
13558
+ var INTERNAL = /chatgpt\.com|openai\.com|oaiusercontent|oaistatic|claude\.ai|anthropic\.com/i;
13559
+ var TRACK = /^(utm_|oai|fbclid|gclid|ref$|ref_|sa$|usg$|ved$|sca_esv$|sources$)/i;
13560
+ function domainOf(u) {
13561
+ try {
13562
+ return new URL(u).hostname.replace(/^www\./, "");
13563
+ } catch {
13564
+ return "";
13565
+ }
13566
+ }
13567
+ function normUrl(u) {
13568
+ try {
13569
+ const x = new URL(u);
13570
+ x.hash = "";
13571
+ for (const k of [...x.searchParams.keys()]) {
13572
+ if (TRACK.test(k)) x.searchParams.delete(k);
13573
+ }
13574
+ return x.toString().replace(/\/$/, "").toLowerCase();
13575
+ } catch {
13576
+ return String(u || "").toLowerCase();
13577
+ }
13578
+ }
13579
+ function isExternal(u) {
13580
+ const h = domainOf(u);
13581
+ return Boolean(h) && !INTERNAL.test(h);
13582
+ }
13583
+ function unesc(s) {
13584
+ try {
13585
+ return JSON.parse('"' + s + '"');
13586
+ } catch {
13587
+ return String(s).replace(/\\(.)/g, "$1");
13588
+ }
13589
+ }
13590
+ function stripCitationMarkers(s) {
13591
+ return String(s || "").replace(/[-]/g, "").replace(/cite(?:turn\d+\w+?\d+)+/g, "").replace(/\s+/g, " ").trim();
13592
+ }
13593
+ function sliceBalanced(text, openIdx) {
13594
+ const open = text[openIdx];
13595
+ const close = open === "[" ? "]" : "}";
13596
+ let depth = 0;
13597
+ let inStr = false;
13598
+ let esc2 = false;
13599
+ for (let i = openIdx; i < text.length; i++) {
13600
+ const ch = text[i];
13601
+ if (inStr) {
13602
+ if (esc2) esc2 = false;
13603
+ else if (ch === "\\") esc2 = true;
13604
+ else if (ch === '"') inStr = false;
13605
+ continue;
13606
+ }
13607
+ if (ch === '"') {
13608
+ inStr = true;
13609
+ continue;
13610
+ }
13611
+ if (ch === open) depth++;
13612
+ else if (ch === close) {
13613
+ depth--;
13614
+ if (depth === 0) return text.slice(openIdx, i + 1);
13615
+ }
13616
+ }
13617
+ return null;
13618
+ }
13619
+ function findJsonValues(text, field) {
13620
+ const out = [];
13621
+ const re = new RegExp('"' + field + '"\\s*:\\s*(\\[|\\{)', "g");
13622
+ let m;
13623
+ while (m = re.exec(text)) {
13624
+ const openIdx = m.index + m[0].length - 1;
13625
+ const slice = sliceBalanced(text, openIdx);
13626
+ if (slice) {
13627
+ try {
13628
+ out.push(JSON.parse(slice));
13629
+ } catch {
13630
+ continue;
13631
+ }
13632
+ }
13633
+ }
13634
+ return out;
13635
+ }
13636
+ function stringsInArrayBody(arrBody) {
13637
+ const out = [];
13638
+ const re = /"((?:[^"\\]|\\.){2,300}?)"/g;
13639
+ let m;
13640
+ while (m = re.exec(arrBody)) out.push(unesc(m[1]));
13641
+ return out;
13642
+ }
13643
+ function cleanQuery(q) {
13644
+ const t = (q || "").trim();
13645
+ if (t.length < 2 || t.length > 300) return null;
13646
+ return t;
13647
+ }
13648
+
13649
+ // src/services/fanout/adapters/chatgpt.ts
13650
+ function roundOf(refId) {
13651
+ const m = /^turn(\d+)/.exec(refId || "");
13652
+ return m ? Number(m[1]) : null;
13653
+ }
13654
+ function normRef(r) {
13655
+ if (typeof r === "string") {
13656
+ const m = /turn(\d+)(search|news|ref|video|image|product)(\d+)/.exec(r);
13657
+ return m ? `turn${m[1]}${m[2]}${m[3]}` : null;
13658
+ }
13659
+ if (r && typeof r === "object") {
13660
+ const o = r;
13661
+ if (o.turn_index != null) {
13662
+ return `turn${o.turn_index}${o.ref_type || "search"}${o.ref_index != null ? o.ref_index : ""}`;
13663
+ }
13664
+ }
13665
+ return null;
13666
+ }
13667
+ function refsInText(s) {
13668
+ return (String(s || "").match(/turn\d+(?:search|news|ref|video|image|product)\d+/g) || []).map(normRef).filter((x) => Boolean(x));
13669
+ }
13670
+ function collectInto(arr, v) {
13671
+ if (Array.isArray(v)) arr.push(...v);
13672
+ else if (v && typeof v === "object") arr.push(v);
13673
+ }
13674
+ function reconstructFromSsePatches(raw) {
13675
+ const contentReferences = [];
13676
+ const searchResultGroups = [];
13677
+ const citations = [];
13678
+ const citedUrls = /* @__PURE__ */ new Set();
13679
+ const state = { found: false };
13680
+ const handleOp = (op) => {
13681
+ if (!op || typeof op !== "object") return;
13682
+ const o = op;
13683
+ const v = o.v;
13684
+ if (o.o === "patch" && Array.isArray(v)) {
13685
+ state.found = true;
13686
+ v.forEach(handleOp);
13687
+ return;
13688
+ }
13689
+ const path5 = typeof o.p === "string" ? o.p : "";
13690
+ if ((path5 === "" || path5 === "/message") && v && typeof v === "object") {
13691
+ const root = v;
13692
+ const msg = root.message || root;
13693
+ const md = msg && msg.metadata;
13694
+ if (md) {
13695
+ collectInto(contentReferences, md.content_references);
13696
+ collectInto(searchResultGroups, md.search_result_groups);
13697
+ collectInto(citations, md.citations);
13698
+ state.found = true;
13699
+ }
13700
+ }
13701
+ if (!path5) return;
13702
+ if (/\/safe_urls$/.test(path5) && Array.isArray(v)) {
13703
+ for (const u of v) if (typeof u === "string") citedUrls.add(u);
13704
+ state.found = true;
13705
+ } else if (path5.includes("content_references")) {
13706
+ collectInto(contentReferences, v);
13707
+ state.found = true;
13708
+ } else if (path5.includes("search_result_groups")) {
13709
+ collectInto(searchResultGroups, v);
13710
+ state.found = true;
13711
+ } else if (path5.includes("citations")) {
13712
+ collectInto(citations, v);
13713
+ state.found = true;
13714
+ }
13715
+ };
13716
+ for (const line of raw.split("\n")) {
13717
+ const s = (line.startsWith("data:") ? line.slice(5) : line).trim();
13718
+ if (!s || s === "[DONE]" || s[0] !== "{" && s[0] !== "[") continue;
13719
+ let parsed;
13720
+ try {
13721
+ parsed = JSON.parse(s);
13722
+ } catch {
13723
+ continue;
13724
+ }
13725
+ const ops = Array.isArray(parsed) ? parsed : [parsed];
13726
+ ops.forEach(handleOp);
13727
+ }
13728
+ if (!state.found) return raw;
13729
+ const citedRefs = [...citedUrls].map((url) => ({ type: "grouped_webpages", items: [{ url }] }));
13730
+ const synthetic = JSON.stringify({
13731
+ content_references: [...contentReferences, ...citedRefs],
13732
+ search_result_groups: searchResultGroups,
13733
+ citations
13734
+ });
13735
+ return raw + "\n" + synthetic;
13736
+ }
13737
+ var chatgptAdapter = {
13738
+ platform: "ChatGPT",
13739
+ matchesHost(hostname) {
13740
+ return /(^|\.)chatgpt\.com$|(^|\.)openai\.com$/i.test(hostname);
13741
+ },
13742
+ parse(rawInput, prompt) {
13743
+ const raw = reconstructFromSsePatches(rawInput);
13744
+ const sources = /* @__PURE__ */ new Map();
13745
+ const snippets = /* @__PURE__ */ new Map();
13746
+ const queries = /* @__PURE__ */ new Set();
13747
+ const unmapped = /* @__PURE__ */ new Set();
13748
+ const meta = { model: "", finishType: "", title: "", rounds: 0 };
13749
+ const ensure = (url) => {
13750
+ if (!isExternal(url)) return null;
13751
+ const k = normUrl(url);
13752
+ let s = sources.get(k);
13753
+ if (!s) {
13754
+ s = { url, domain: domainOf(url), title: "", cited: false, timesCited: 0, firstOrder: Infinity, snippet: "", round: null };
13755
+ sources.set(k, s);
13756
+ }
13757
+ return s;
13758
+ };
13759
+ const addSnippet = (url, title, text) => {
13760
+ const clean2 = stripCitationMarkers(text);
13761
+ if (!isExternal(url) || clean2.length < 2) return;
13762
+ snippets.set(normUrl(url), { url, domain: domainOf(url), title: title || domainOf(url), text: clean2.slice(0, 400) });
13763
+ };
13764
+ for (const m of raw.matchAll(/"([a-z_]*(?:quer|search)[a-z_]*)"\s*:/gi)) unmapped.add(m[1]);
13765
+ for (const m of raw.matchAll(/"search_model_queries"\s*:\s*\[([^\]]*)\]/g)) {
13766
+ for (const q of stringsInArrayBody(m[1])) {
13767
+ const c = cleanQuery(q);
13768
+ if (c) queries.add(c);
13769
+ }
13770
+ }
13771
+ for (const m of raw.matchAll(/"(?:search_queries|queries|search_query)"\s*:\s*\[([^\]]*)\]/g)) {
13772
+ for (const q of stringsInArrayBody(m[1])) {
13773
+ const c = cleanQuery(q);
13774
+ if (c) queries.add(c);
13775
+ }
13776
+ }
13777
+ for (const v of findJsonValues(raw, "search_result_groups")) {
13778
+ if (!Array.isArray(v)) continue;
13779
+ for (const g of v) {
13780
+ const entries = g && typeof g === "object" && Array.isArray(g.entries) ? g.entries : [];
13781
+ for (const e of entries) {
13782
+ const url = typeof e.url === "string" ? e.url : "";
13783
+ const s = ensure(url);
13784
+ if (!s) continue;
13785
+ if (!s.title && typeof e.title === "string") s.title = e.title.slice(0, 200);
13786
+ const ref = normRef(e.ref_id);
13787
+ if (ref && s.round == null) s.round = roundOf(ref);
13788
+ if (typeof e.snippet === "string" && e.snippet) addSnippet(url, s.title, e.snippet);
13789
+ }
13790
+ }
13791
+ }
13792
+ for (const m of raw.matchAll(/"url"\s*:\s*"(https?:\/\/[^"\\]+)"(?:[\s\S]{0,120}?"(?:title|name)"\s*:\s*"((?:[^"\\]|\\.){0,200})")?/g)) {
13793
+ const s = ensure(m[1]);
13794
+ if (s && !s.title && m[2]) s.title = unesc(m[2]).slice(0, 200);
13795
+ }
13796
+ let spanTotal = 0;
13797
+ for (const cr of findJsonValues(raw, "content_references")) {
13798
+ if (!Array.isArray(cr)) continue;
13799
+ for (const ref of cr) {
13800
+ const type = typeof ref.type === "string" ? ref.type : "";
13801
+ const isBundle = /sources_footnote|nav_list|alt_text/.test(type);
13802
+ const refs = refsInText(typeof ref.matched_text === "string" ? ref.matched_text : "");
13803
+ if (!isBundle) spanTotal++;
13804
+ const ord = typeof ref.start_idx === "number" ? ref.start_idx : isBundle ? Infinity : spanTotal;
13805
+ const items = Array.isArray(ref.items) ? ref.items : [];
13806
+ items.forEach((it, idx) => {
13807
+ const refForItem = refs.length === items.length ? refs[idx] : refs[0];
13808
+ const urls = [it.url].concat((Array.isArray(it.supporting_websites) ? it.supporting_websites : []).map((s) => s?.url)).filter((u) => typeof u === "string");
13809
+ for (const u of urls) {
13810
+ const s = ensure(u);
13811
+ if (!s) continue;
13812
+ if (!isBundle) {
13813
+ s.cited = true;
13814
+ s.timesCited++;
13815
+ if (ord < s.firstOrder) s.firstOrder = ord;
13816
+ } else if (!s.cited) {
13817
+ s.cited = true;
13818
+ }
13819
+ if (refForItem && s.round == null) s.round = roundOf(refForItem);
13820
+ }
13821
+ if (typeof it.url === "string" && typeof it.snippet === "string") addSnippet(it.url, typeof it.title === "string" ? it.title : "", it.snippet);
13822
+ });
13823
+ }
13824
+ }
13825
+ for (const v of findJsonValues(raw, "citations")) {
13826
+ if (!Array.isArray(v)) continue;
13827
+ v.forEach((c, i) => {
13828
+ const md = c && typeof c === "object" ? c.metadata : null;
13829
+ if (!md || typeof md.url !== "string") return;
13830
+ const s = ensure(md.url);
13831
+ if (!s) return;
13832
+ s.cited = true;
13833
+ s.timesCited++;
13834
+ const ord = typeof c.start_ix === "number" ? c.start_ix : i;
13835
+ if (ord < s.firstOrder) s.firstOrder = ord;
13836
+ if (typeof md.text === "string") addSnippet(md.url, typeof md.title === "string" ? md.title : "", md.text);
13837
+ });
13838
+ }
13839
+ for (const m of raw.matchAll(/"content_type"\s*:\s*"tether_quote"([\s\S]{0,4000}?)"text"\s*:\s*"((?:[^"\\]|\\.)*)"/g)) {
13840
+ const url = (m[1].match(/"url"\s*:\s*"([^"]+)"/) || [])[1];
13841
+ const title = (m[1].match(/"title"\s*:\s*"((?:[^"\\]|\\.)*)"/) || [])[1];
13842
+ if (url) {
13843
+ ensure(url);
13844
+ addSnippet(url, title ? unesc(title) : "", unesc(m[2]));
13845
+ }
13846
+ }
13847
+ const modelMatches = [...raw.matchAll(/"resolved_model_slug"\s*:\s*"([^"]+)"/g)];
13848
+ const slugMatches = modelMatches.length ? modelMatches : [...raw.matchAll(/"model_slug"\s*:\s*"([^"]+)"/g)];
13849
+ if (slugMatches.length) meta.model = slugMatches[slugMatches.length - 1][1];
13850
+ const finish = raw.match(/"finish_details"\s*:\s*\{[^}]*"type"\s*:\s*"([^"]+)"/);
13851
+ if (finish) meta.finishType = finish[1];
13852
+ 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;
13853
+ const sourceRounds = /* @__PURE__ */ new Set();
13854
+ for (const s of sources.values()) if (s.round != null) sourceRounds.add(s.round);
13855
+ meta.rounds = Math.max(searchRounds, sourceRounds.size);
13856
+ 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) => ({
13857
+ url: s.url,
13858
+ domain: s.domain,
13859
+ title: s.title || s.domain,
13860
+ cited: s.cited,
13861
+ timesCited: s.timesCited,
13862
+ snippet: snippets.get(normUrl(s.url))?.text || "",
13863
+ round: s.round
13864
+ }));
13865
+ return {
13866
+ subQueries: [...queries],
13867
+ sources: ordered,
13868
+ snippets: [...snippets.values()],
13869
+ meta,
13870
+ unmappedKeys: [...unmapped].slice(0, 50)
13871
+ };
13872
+ }
13873
+ };
13874
+
13875
+ // src/services/fanout/adapters/claude.ts
13876
+ function isFavicon(u) {
13877
+ return /\/s2\/favicons/i.test(u) || /favicon/i.test(u);
13878
+ }
13879
+ var claudeAdapter = {
13880
+ platform: "Claude",
13881
+ matchesHost(hostname) {
13882
+ return /(^|\.)claude\.ai$/i.test(hostname);
13883
+ },
13884
+ parse(raw, prompt) {
13885
+ const sources = /* @__PURE__ */ new Map();
13886
+ const snippets = /* @__PURE__ */ new Map();
13887
+ const queries = /* @__PURE__ */ new Set();
13888
+ const unmapped = /* @__PURE__ */ new Set();
13889
+ const meta = { model: "", finishType: "", title: "", rounds: 0 };
13890
+ const ensure = (url) => {
13891
+ if (!url || !isExternal(url) || isFavicon(url)) return null;
13892
+ const k = normUrl(url);
13893
+ let s = sources.get(k);
13894
+ if (!s) {
13895
+ s = { url, domain: domainOf(url), title: "", cited: false, timesCited: 0, snippet: "", round: null };
13896
+ sources.set(k, s);
13897
+ }
13898
+ return s;
13899
+ };
13900
+ const toolInputs = /* @__PURE__ */ new Map();
13901
+ const searchBlocks = /* @__PURE__ */ new Set();
13902
+ const finalizeTool = (index) => {
13903
+ const t = toolInputs.get(index);
13904
+ if (!t || !/web_search|web_fetch|search|browse/i.test(t.name)) return;
13905
+ try {
13906
+ const input = JSON.parse(t.buf);
13907
+ if (typeof input.query === "string") {
13908
+ const q = cleanQuery(input.query);
13909
+ if (q) queries.add(q);
13910
+ }
13911
+ } catch {
13912
+ const m = t.buf.match(/"query"\s*:\s*"((?:[^"\\]|\\.)*)"/);
13913
+ if (m) {
13914
+ const q = cleanQuery(unesc(m[1]));
13915
+ if (q) queries.add(q);
13916
+ }
13917
+ }
13918
+ };
13919
+ for (const line of raw.split("\n")) {
13920
+ const s = (line.startsWith("data:") ? line.slice(5) : line).trim();
13921
+ if (!s || s[0] !== "{" && s[0] !== "[") continue;
13922
+ let ev;
13923
+ try {
13924
+ ev = JSON.parse(s);
13925
+ } catch {
13926
+ continue;
13927
+ }
13928
+ const type = ev.type;
13929
+ if (type === "content_block_start") {
13930
+ const cb = ev.content_block || {};
13931
+ const index = typeof ev.index === "number" ? ev.index : -1;
13932
+ if (cb.type === "tool_use" && index >= 0) {
13933
+ const name = typeof cb.name === "string" ? cb.name : "";
13934
+ toolInputs.set(index, { name, buf: "" });
13935
+ if (/web_search|web_fetch|search|browse/i.test(name)) searchBlocks.add(index);
13936
+ }
13937
+ } else if (type === "content_block_delta") {
13938
+ const d = ev.delta || {};
13939
+ const index = typeof ev.index === "number" ? ev.index : -1;
13940
+ if (d.type === "input_json_delta" && typeof d.partial_json === "string") {
13941
+ const t = toolInputs.get(index);
13942
+ if (t) t.buf += d.partial_json;
13943
+ } else if (d.type === "citation_start_delta") {
13944
+ const c = d.citation || {};
13945
+ if (typeof c.url === "string") {
13946
+ const src = ensure(c.url);
13947
+ if (src) {
13948
+ src.cited = true;
13949
+ src.timesCited++;
13950
+ if (!src.title && typeof c.title === "string") src.title = c.title.slice(0, 200);
13951
+ if (typeof c.cited_text === "string" && c.cited_text) {
13952
+ snippets.set(normUrl(c.url), { url: c.url, domain: domainOf(c.url), title: src.title, text: String(c.cited_text).slice(0, 400) });
13953
+ }
13954
+ }
13955
+ }
13956
+ }
13957
+ } else if (type === "content_block_stop") {
13958
+ const index = typeof ev.index === "number" ? ev.index : -1;
13959
+ if (index >= 0) finalizeTool(index);
13960
+ } else if (type === "message_delta" || type === "message_stop" || type === "message_start") {
13961
+ const d = ev.delta || ev.message || {};
13962
+ if (typeof d.stop_reason === "string" && !meta.finishType) meta.finishType = d.stop_reason;
13963
+ if (typeof d.model === "string" && !meta.model) meta.model = d.model;
13964
+ }
13965
+ }
13966
+ for (const index of searchBlocks) finalizeTool(index);
13967
+ for (const m of raw.matchAll(/"title"\s*:\s*"((?:[^"\\]|\\.){1,200})"\s*,\s*"url"\s*:\s*"(https?:\/\/[^"\\]+)"/g)) {
13968
+ const src = ensure(m[2]);
13969
+ if (src && !src.title) src.title = unesc(m[1]).slice(0, 200);
13970
+ }
13971
+ for (const m of raw.matchAll(/"url"\s*:\s*"(https?:\/\/[^"\\]+)"/g)) ensure(m[1]);
13972
+ for (const m of raw.matchAll(/\\"title\\"\s*:\s*\\"((?:[^"\\]|\\.){1,160}?)\\"[\s\S]{0,160}?\\"url\\"\s*:\s*\\"(https?:[^"\\]+)\\"/g)) {
13973
+ const src = ensure(m[2]);
13974
+ if (src && !src.title) src.title = unesc(unesc(m[1])).slice(0, 200);
13975
+ }
13976
+ for (const m of raw.matchAll(/\\"url\\"\s*:\s*\\"(https?:[^"\\]+)\\"/g)) ensure(m[1]);
13977
+ if (!meta.model) {
13978
+ const mm = raw.match(/"model"\s*:\s*"(claude[^"]+)"/);
13979
+ if (mm) meta.model = mm[1];
13980
+ }
13981
+ meta.rounds = queries.size > 0 ? searchBlocks.size : 0;
13982
+ for (const m of raw.matchAll(/"([a-z_]*(?:quer|search)[a-z_]*)"\s*:/gi)) unmapped.add(m[1]);
13983
+ const ordered = [...sources.values()].sort((a, b) => a.cited === b.cited ? b.timesCited - a.timesCited : a.cited ? -1 : 1).map((s) => ({
13984
+ url: s.url,
13985
+ domain: s.domain,
13986
+ title: s.title || s.domain,
13987
+ cited: s.cited,
13988
+ timesCited: s.timesCited,
13989
+ snippet: snippets.get(normUrl(s.url))?.text || "",
13990
+ round: s.round
13991
+ }));
13992
+ return {
13993
+ subQueries: [...queries],
13994
+ sources: ordered,
13995
+ snippets: [...snippets.values()],
13996
+ meta,
13997
+ unmappedKeys: [...unmapped].slice(0, 50)
13998
+ };
13999
+ }
14000
+ };
14001
+
14002
+ // src/services/fanout/adapters/index.ts
14003
+ var FANOUT_ADAPTERS = [chatgptAdapter, claudeAdapter];
14004
+ function adapterForHost(hostname) {
14005
+ return FANOUT_ADAPTERS.find((a) => a.matchesHost(hostname)) ?? null;
14006
+ }
14007
+
14008
+ // src/services/fanout/build.ts
14009
+ function buildFanoutResult(ctx) {
14010
+ const { platform, parsed, ready, rawBytes } = ctx;
14011
+ const browsedUrls = parsed.sources;
14012
+ const citedUrls = browsedUrls.filter((s) => s.cited);
14013
+ const browsedOnly = browsedUrls.filter((s) => !s.cited);
14014
+ const prompt = ctx.prompt || parsed.meta.title || "";
14015
+ const result = {
14016
+ platform,
14017
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
14018
+ prompt,
14019
+ meta: parsed.meta,
14020
+ subQueries: parsed.subQueries,
14021
+ browsedUrls,
14022
+ citedUrls,
14023
+ browsedOnly,
14024
+ snippets: parsed.snippets,
14025
+ counts: {
14026
+ subQueries: parsed.subQueries.length,
14027
+ browsed: browsedUrls.length,
14028
+ cited: citedUrls.length,
14029
+ browsedOnly: browsedOnly.length
14030
+ }
14031
+ };
14032
+ if (parsed.subQueries.length === 0 && browsedUrls.length === 0) {
14033
+ result.debug = {
14034
+ interceptorReady: ready,
14035
+ rawBytes,
14036
+ unmappedKeys: parsed.unmappedKeys,
14037
+ 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. Open a direct/no-proxy hosted browser session, navigate to chatgpt.com or claude.ai after opening, run a new prompt in that session, then call browser_capture_fanout again."
14038
+ };
14039
+ }
14040
+ return result;
14041
+ }
14042
+
14043
+ // src/services/fanout/classify.ts
14044
+ 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;
14045
+ var ENCYCLOPEDIA = /(^|\.)(wikipedia\.org|wikimedia\.org|britannica\.com|merriam-webster\.com|dictionary\.com|fandom\.com|investopedia\.com)$/i;
14046
+ 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;
14047
+ 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;
14048
+ function classifyDomain(domain, firstPartyDomain) {
14049
+ const d = domain.toLowerCase().replace(/^www\./, "");
14050
+ if (firstPartyDomain) {
14051
+ const fp = firstPartyDomain.toLowerCase().replace(/^www\./, "");
14052
+ if (fp && (d === fp || d.endsWith("." + fp))) return "First-party/vendor";
14053
+ }
14054
+ if (/(^|\.)reddit\.com$/i.test(d)) return "Reddit";
14055
+ if (SOCIAL.test(d)) return "Social/video";
14056
+ if (ENCYCLOPEDIA.test(d)) return "Encyclopedia";
14057
+ if (REVIEW.test(d)) return "Review site";
14058
+ if (NEWS.test(d) || /(^|\.)news\./i.test(d)) return "News/media";
14059
+ if (/^docs?\.|^developer\.|^developers\.|readthedocs|\.dev$/i.test(d)) return "Docs";
14060
+ return "Blog";
14061
+ }
14062
+ function emptyCategoryCounts() {
14063
+ return { "First-party/vendor": 0, "News/media": 0, Reddit: 0, "Social/video": 0, Encyclopedia: 0, "Review site": 0, Docs: 0, Blog: 0 };
14064
+ }
14065
+ function enrichFanout(result, opts = {}) {
14066
+ const addSiteType = (s) => ({ ...s, siteType: classifyDomain(s.domain, opts.firstPartyDomain) });
14067
+ const browsedUrls = result.browsedUrls.map(addSiteType);
14068
+ const citedUrls = result.citedUrls.map(addSiteType);
14069
+ const browsedOnly = result.browsedOnly.map(addSiteType);
14070
+ const byCategory = emptyCategoryCounts();
14071
+ const domainAgg = /* @__PURE__ */ new Map();
14072
+ for (const s of browsedUrls) {
14073
+ byCategory[s.siteType]++;
14074
+ const cur = domainAgg.get(s.domain);
14075
+ if (!cur) domainAgg.set(s.domain, { domain: s.domain, count: 1, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType });
14076
+ else {
14077
+ cur.count++;
14078
+ cur.cited = cur.cited || s.cited;
14079
+ cur.timesCited += s.timesCited;
14080
+ }
14081
+ }
14082
+ const topSites = [...domainAgg.values()].sort((a, b) => b.count - a.count || b.timesCited - a.timesCited);
14083
+ 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 }));
14084
+ return {
14085
+ platform: result.platform,
14086
+ capturedAt: result.capturedAt,
14087
+ prompt: result.prompt,
14088
+ meta: result.meta,
14089
+ queries: result.subQueries,
14090
+ browsedUrls,
14091
+ citedUrls,
14092
+ browsedOnly,
14093
+ snippets: result.snippets,
14094
+ counts: result.counts,
14095
+ aggregates: { topSites, citationOrder, byCategory },
14096
+ firstPartyDomain: opts.firstPartyDomain ? opts.firstPartyDomain.toLowerCase().replace(/^www\./, "") : null,
14097
+ ...result.debug ? { debug: result.debug } : {}
14098
+ };
14099
+ }
14100
+
14101
+ // src/services/fanout/export.ts
14102
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
14103
+ import { homedir as homedir3 } from "os";
14104
+ import { join as join6 } from "path";
14105
+ import Papa2 from "papaparse";
14106
+ function outputBaseDir3() {
14107
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join6(homedir3(), "Downloads", "mcp-scraper");
14108
+ }
14109
+ function safe(value) {
14110
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "capture";
14111
+ }
14112
+ function writeTable(path5, rows, delimiter) {
14113
+ writeFileSync2(path5, Papa2.unparse(rows.length ? rows : [{}], { delimiter }));
14114
+ }
14115
+ function exportFanout(enriched) {
14116
+ const stamp = safe(enriched.capturedAt.replace(/[:.]/g, "-"));
14117
+ const dir = join6(outputBaseDir3(), "fanout", `${stamp}-${safe(enriched.platform)}`);
14118
+ mkdirSync3(dir, { recursive: true });
14119
+ const queryRows = enriched.queries.map((q, i) => ({ index: i + 1, query: q }));
14120
+ const citationRows = enriched.aggregates.citationOrder.map((c) => {
14121
+ const src = enriched.citedUrls.find((s) => s.url === c.url);
14122
+ return { rank: c.rank, domain: c.domain, url: c.url, timesCited: c.timesCited, siteType: src?.siteType ?? "", title: src?.title ?? "" };
14123
+ });
14124
+ 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 ?? "" }));
14125
+ const domainRows = enriched.aggregates.topSites.map((d) => ({ domain: d.domain, count: d.count, cited: d.cited, timesCited: d.timesCited, siteType: d.siteType }));
14126
+ const paths = {
14127
+ dir,
14128
+ json: join6(dir, "fanout.json"),
14129
+ queriesCsv: join6(dir, "queries.csv"),
14130
+ queriesTsv: join6(dir, "queries.tsv"),
14131
+ citationsCsv: join6(dir, "citations.csv"),
14132
+ sourcesCsv: join6(dir, "sources.csv"),
14133
+ domainsCsv: join6(dir, "domains.csv"),
14134
+ report: join6(dir, "report.html")
14135
+ };
14136
+ writeFileSync2(paths.json, JSON.stringify(enriched, null, 2));
14137
+ writeTable(paths.queriesCsv, queryRows, ",");
14138
+ writeTable(paths.queriesTsv, queryRows, " ");
14139
+ writeTable(paths.citationsCsv, citationRows, ",");
14140
+ writeTable(paths.sourcesCsv, sourceRows, ",");
14141
+ writeTable(paths.domainsCsv, domainRows, ",");
14142
+ writeFileSync2(paths.report, renderReportHtml(enriched));
14143
+ return paths;
14144
+ }
14145
+ function esc(s) {
14146
+ return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
14147
+ }
14148
+ function renderReportHtml(enriched) {
14149
+ const data = JSON.stringify(enriched).replace(/</g, "\\u003c");
14150
+ return `<!doctype html>
14151
+ <html lang="en">
14152
+ <head>
14153
+ <meta charset="utf-8" />
14154
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
14155
+ <title>AI Search Fan-Out \u2014 ${esc(enriched.platform)}</title>
14156
+ <style>
14157
+ :root{--bg:#0b0d12;--panel:#151922;--line:#252b38;--ink:#e8ecf3;--mut:#8b95a7;--acc:#5b9dff;--cite:#39d98a;--browse:#5a6677}
14158
+ *{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}
14159
+ .wrap{max-width:980px;margin:0 auto;padding:28px 20px 80px}
14160
+ h1{font-size:20px;margin:0 0 2px}.sub{color:var(--mut);margin:0 0 20px;font-size:13px}
14161
+ .stat{font-size:30px;font-weight:700}.stat small{font-size:13px;font-weight:400;color:var(--mut)}
14162
+ .cards{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}
14163
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
14164
+ .card b{font-size:22px;display:block}.card span{color:var(--mut);font-size:12px}
14165
+ .bars{margin:8px 0 22px}.bar{display:flex;align-items:center;gap:8px;margin:4px 0}
14166
+ .bar .lab{width:150px;color:var(--mut);font-size:12px;text-align:right}.bar .track{flex:1;background:#10141c;border-radius:5px;overflow:hidden}
14167
+ .bar .fill{background:var(--acc);height:16px}.bar .n{width:34px;font-size:12px;color:var(--mut)}
14168
+ .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}
14169
+ .tab.on{background:var(--acc);border-color:var(--acc);color:#04101f;font-weight:600}
14170
+ .chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
14171
+ .chip{background:var(--panel);border:1px solid var(--line);color:var(--mut);padding:4px 10px;border-radius:20px;cursor:pointer;font-size:12px}
14172
+ .chip.on{border-color:var(--acc);color:var(--ink)}.chip b{color:var(--ink)}
14173
+ .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}
14174
+ .row .idx{color:var(--mut);min-width:22px}.row .main{flex:1}
14175
+ .tag{font-size:11px;padding:2px 7px;border-radius:6px;border:1px solid var(--line);color:var(--mut);margin-right:5px}
14176
+ .dom{font-weight:600}.url{color:var(--mut);font-size:12px;word-break:break-all}
14177
+ .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}
14178
+ .copy{background:none;border:1px solid var(--line);color:var(--mut);border-radius:6px;cursor:pointer;font-size:11px;padding:2px 8px}
14179
+ .hide{display:none}
14180
+ </style>
14181
+ </head>
14182
+ <body><div class="wrap">
14183
+ <h1>AI Search Fan-Out</h1>
14184
+ <p class="sub" id="sub"></p>
14185
+ <div class="stat" id="stat"></div>
14186
+ <div class="cards" id="cards"></div>
14187
+ <div id="charts"></div>
14188
+ <div class="tabs"><button class="tab on" data-tab="q">Queries</button><button class="tab" data-tab="u">URLs</button></div>
14189
+ <div class="chips" id="chips"></div>
14190
+ <div id="list"></div>
14191
+ </div>
14192
+ <script>
14193
+ const D=${data};
14194
+ const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
14195
+ document.getElementById('sub').textContent=D.platform+' \xB7 '+(D.meta.model||'model n/a')+' \xB7 '+D.meta.rounds+' search round(s) \xB7 prompt: '+(D.prompt||'');
14196
+ 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>';
14197
+ const cards=[['Sub-queries',D.counts.subQueries],['Researched',D.counts.browsed],['Cited',D.counts.cited],['Browsed-only',D.counts.browsedOnly]];
14198
+ 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)});
14199
+ 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}
14200
+ const charts=document.getElementById('charts');
14201
+ charts.appendChild(barChart('URL Categories',D.aggregates.byCategory));
14202
+ let tab='q',filter=null;
14203
+ const chips=document.getElementById('chips'),list=document.getElementById('list');
14204
+ function render(){
14205
+ chips.innerHTML='';list.innerHTML='';
14206
+ if(tab==='q'){
14207
+ 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)});
14208
+ } else {
14209
+ const counts={};D.browsedUrls.forEach(s=>{counts[s.siteType]=(counts[s.siteType]||0)+1});
14210
+ 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)});
14211
+ 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)});
14212
+ }
14213
+ }
14214
+ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
14215
+ 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()});
14216
+ render();
14217
+ </script>
14218
+ </body></html>`;
14219
+ }
14220
+
14221
+ // src/services/fanout/run-capture.ts
14222
+ var COMPOSER_SELECTORS = [
14223
+ "#prompt-textarea",
14224
+ "div#prompt-textarea",
14225
+ 'textarea[data-testid="prompt-textarea"]',
14226
+ 'div.ProseMirror[contenteditable="true"]',
14227
+ 'div[contenteditable="true"]',
14228
+ "textarea"
14229
+ ];
14230
+ async function sendPrompt(page, text) {
14231
+ for (const selector of COMPOSER_SELECTORS) {
14232
+ const locator = page.locator(selector).first();
14233
+ try {
14234
+ await locator.waitFor({ state: "visible", timeout: 12e3 });
14235
+ } catch {
14236
+ continue;
14237
+ }
14238
+ await locator.click({ timeout: 4e3 }).catch(() => void 0);
14239
+ await locator.focus().catch(() => void 0);
14240
+ await page.keyboard.type(text, { delay: 12 });
14241
+ await page.waitForTimeout(300).catch(() => void 0);
14242
+ await page.keyboard.press("Enter");
14243
+ return true;
14244
+ }
14245
+ return false;
14246
+ }
14247
+ async function waitForAnswer(page, cap, baseline, waitMs) {
14248
+ const start = Date.now();
14249
+ while (Date.now() - start < waitMs && cap.answerStreamCount() <= baseline) {
14250
+ await page.waitForTimeout(400).catch(() => void 0);
14251
+ }
14252
+ await page.waitForTimeout(1e3).catch(() => void 0);
14253
+ }
14254
+ async function runFanoutCapture(page, input) {
14255
+ const cap = new FanoutCdpCapture();
14256
+ await cap.attach(page);
14257
+ try {
14258
+ const baseline = cap.answerStreamCount();
14259
+ if (input.prompt) await sendPrompt(page, input.prompt).catch(() => false);
14260
+ const waitMs = input.wait_ms ?? (input.prompt ? 9e4 : 8e3);
14261
+ await waitForAnswer(page, cap, baseline, waitMs);
14262
+ await cap.drain();
14263
+ let hostname = "";
14264
+ try {
14265
+ hostname = new URL(page.url()).hostname;
14266
+ } catch {
14267
+ hostname = "";
14268
+ }
14269
+ const adapter = adapterForHost(hostname);
14270
+ if (!adapter) {
14271
+ 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.`);
14272
+ }
14273
+ const text = cap.bestAnswerText();
14274
+ const parsed = adapter.parse(text, input.prompt || "");
14275
+ const result = buildFanoutResult({ platform: adapter.platform, parsed, prompt: input.prompt || "", ready: true, rawBytes: text.length });
14276
+ const enriched = enrichFanout(result, { firstPartyDomain: input.first_party_domain });
14277
+ const exports = input.export ? exportFanout(enriched) : null;
14278
+ return { result: enriched, exports };
14279
+ } finally {
14280
+ await cap.detach().catch(() => void 0);
14281
+ }
14282
+ }
14283
+
14284
+ // src/services/browser-agent/browser-agent-service.ts
13563
14285
  function keepHostedBrowserAlive(_browser) {
13564
14286
  }
13565
14287
  var DEFAULT_TIMEOUT_SECONDS = 600;
@@ -15725,4 +16447,4 @@ app.get("/blog/:slug/", (c) => {
15725
16447
  export {
15726
16448
  app
15727
16449
  };
15728
- //# sourceMappingURL=server-YSKBD2VX.js.map
16450
+ //# sourceMappingURL=server-HRVD52CX.js.map