mcp-scraper 0.3.10 → 0.3.12

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 (50) hide show
  1. package/README.md +4 -3
  2. package/dist/bin/api-server.cjs +115 -40
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +2 -2
  5. package/dist/bin/browser-agent-stdio-server.cjs +206 -31
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +3 -2
  8. package/dist/bin/browser-agent-stdio-server.js.map +1 -1
  9. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  10. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  11. package/dist/bin/mcp-scraper-cli.js +1 -1
  12. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +262 -60
  13. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  14. package/dist/bin/mcp-scraper-combined-stdio-server.js +4 -3
  15. package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
  16. package/dist/bin/mcp-scraper-install.cjs +1 -1
  17. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  18. package/dist/bin/mcp-scraper-install.js +1 -1
  19. package/dist/bin/mcp-stdio-server.cjs +38 -11
  20. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  21. package/dist/bin/mcp-stdio-server.js +2 -2
  22. package/dist/bin/paa-harvest.cjs +21 -1
  23. package/dist/bin/paa-harvest.cjs.map +1 -1
  24. package/dist/bin/paa-harvest.js +1 -1
  25. package/dist/chunk-3YGKXXUG.js +133 -0
  26. package/dist/chunk-3YGKXXUG.js.map +1 -0
  27. package/dist/{chunk-UWSG3C5J.js → chunk-4OPKIDON.js} +22 -2
  28. package/dist/chunk-4OPKIDON.js.map +1 -0
  29. package/dist/{chunk-CED7X4WB.js → chunk-7R7VBQRV.js} +39 -12
  30. package/dist/chunk-7R7VBQRV.js.map +1 -0
  31. package/dist/{chunk-QPFF3V2R.js → chunk-BW3DGFNQ.js} +36 -9
  32. package/dist/chunk-BW3DGFNQ.js.map +1 -0
  33. package/dist/chunk-FB5ZJGFN.js +7 -0
  34. package/dist/chunk-FB5ZJGFN.js.map +1 -0
  35. package/dist/index.cjs +21 -1
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/{server-QTV2EUKA.js → server-CUPDJLWM.js} +44 -142
  39. package/dist/server-CUPDJLWM.js.map +1 -0
  40. package/dist/{worker-56IXWOQU.js → worker-FG7ZWEGA.js} +2 -2
  41. package/docs/mcp-tool-craft-lint.generated.md +1 -1
  42. package/docs/mcp-tool-manifest.generated.json +3 -3
  43. package/package.json +1 -1
  44. package/dist/chunk-CED7X4WB.js.map +0 -1
  45. package/dist/chunk-NEGW2ZEJ.js +0 -7
  46. package/dist/chunk-NEGW2ZEJ.js.map +0 -1
  47. package/dist/chunk-QPFF3V2R.js.map +0 -1
  48. package/dist/chunk-UWSG3C5J.js.map +0 -1
  49. package/dist/server-QTV2EUKA.js.map +0 -1
  50. /package/dist/{worker-56IXWOQU.js.map → worker-FG7ZWEGA.js.map} +0 -0
@@ -17,8 +17,8 @@ loadDotEnv();
17
17
  async function main() {
18
18
  const [{ serve }, { app }, { startWorker }, { migrate }] = await Promise.all([
19
19
  import("@hono/node-server"),
20
- import("../server-QTV2EUKA.js"),
21
- import("../worker-56IXWOQU.js"),
20
+ import("../server-CUPDJLWM.js"),
21
+ import("../worker-FG7ZWEGA.js"),
22
22
  import("../db-BE4JVB3V.js")
23
23
  ]);
24
24
  const PORT = parseInt(process.env.PORT ?? "3001");
@@ -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 import_node_fs2 = require("fs");
6
- var import_node_os3 = require("os");
7
- var import_node_path3 = require("path");
27
+ var import_node_fs3 = require("fs");
28
+ var import_node_os4 = require("os");
29
+ var import_node_path4 = 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 import_node_fs = require("fs");
13
- var import_node_os2 = require("os");
14
- var import_node_path2 = require("path");
34
+ var import_node_fs2 = require("fs");
35
+ var import_node_os3 = require("os");
36
+ var import_node_path3 = require("path");
15
37
 
16
38
  // src/lib/browser-service-env.ts
17
39
  function browserServiceProfileName() {
@@ -26,8 +48,137 @@ function browserServiceProfileSaveChanges() {
26
48
  return void 0;
27
49
  }
28
50
 
51
+ // src/services/fanout/export.ts
52
+ var import_node_fs = require("fs");
53
+ var import_node_os = require("os");
54
+ var import_node_path = require("path");
55
+ var import_papaparse = __toESM(require("papaparse"), 1);
56
+ function outputBaseDir() {
57
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path.join)((0, import_node_os.homedir)(), "Downloads", "mcp-scraper");
58
+ }
59
+ function safe(value) {
60
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "capture";
61
+ }
62
+ function writeTable(path, rows, delimiter) {
63
+ (0, import_node_fs.writeFileSync)(path, import_papaparse.default.unparse(rows.length ? rows : [{}], { delimiter }));
64
+ }
65
+ function exportFanout(enriched) {
66
+ const stamp = safe(enriched.capturedAt.replace(/[:.]/g, "-"));
67
+ const outputDir = outputBaseDir();
68
+ const relativeDir = (0, import_node_path.join)("fanout", `${stamp}-${safe(enriched.platform)}`);
69
+ const dir = (0, import_node_path.join)(outputDir, relativeDir);
70
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
71
+ const queryRows = enriched.queries.map((q, i) => ({ index: i + 1, query: q }));
72
+ const citationRows = enriched.aggregates.citationOrder.map((c) => {
73
+ const src = enriched.citedUrls.find((s) => s.url === c.url);
74
+ return { rank: c.rank, domain: c.domain, url: c.url, timesCited: c.timesCited, siteType: src?.siteType ?? "", title: src?.title ?? "" };
75
+ });
76
+ 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 ?? "" }));
77
+ const browsedOnlyRows = enriched.browsedOnly.map((s) => ({ domain: s.domain, url: s.url, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType, title: s.title, round: s.round ?? "" }));
78
+ const snippetRows = enriched.snippets.map((s) => ({ domain: s.domain, url: s.url, title: s.title, text: s.text }));
79
+ const domainRows = enriched.aggregates.topSites.map((d) => ({ domain: d.domain, count: d.count, cited: d.cited, timesCited: d.timesCited, siteType: d.siteType }));
80
+ const relativePaths = {
81
+ relativeTo: "MCP_SCRAPER_OUTPUT_DIR or ~/Downloads/mcp-scraper",
82
+ dir: relativeDir,
83
+ json: (0, import_node_path.join)(relativeDir, "fanout.json"),
84
+ queriesCsv: (0, import_node_path.join)(relativeDir, "queries.csv"),
85
+ queriesTsv: (0, import_node_path.join)(relativeDir, "queries.tsv"),
86
+ citationsCsv: (0, import_node_path.join)(relativeDir, "citations.csv"),
87
+ sourcesCsv: (0, import_node_path.join)(relativeDir, "sources.csv"),
88
+ browsedOnlyCsv: (0, import_node_path.join)(relativeDir, "browsed-only.csv"),
89
+ snippetsCsv: (0, import_node_path.join)(relativeDir, "snippets.csv"),
90
+ domainsCsv: (0, import_node_path.join)(relativeDir, "domains.csv"),
91
+ report: (0, import_node_path.join)(relativeDir, "report.html")
92
+ };
93
+ (0, import_node_fs.writeFileSync)((0, import_node_path.join)(outputDir, relativePaths.json), JSON.stringify(enriched, null, 2));
94
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.queriesCsv), queryRows, ",");
95
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.queriesTsv), queryRows, " ");
96
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.citationsCsv), citationRows, ",");
97
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.sourcesCsv), sourceRows, ",");
98
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.browsedOnlyCsv), browsedOnlyRows, ",");
99
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.snippetsCsv), snippetRows, ",");
100
+ writeTable((0, import_node_path.join)(outputDir, relativePaths.domainsCsv), domainRows, ",");
101
+ (0, import_node_fs.writeFileSync)((0, import_node_path.join)(outputDir, relativePaths.report), renderReportHtml(enriched));
102
+ return relativePaths;
103
+ }
104
+ function esc(s) {
105
+ return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
106
+ }
107
+ function renderReportHtml(enriched) {
108
+ const data = JSON.stringify(enriched).replace(/</g, "\\u003c");
109
+ return `<!doctype html>
110
+ <html lang="en">
111
+ <head>
112
+ <meta charset="utf-8" />
113
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
114
+ <title>AI Search Fan-Out \u2014 ${esc(enriched.platform)}</title>
115
+ <style>
116
+ :root{--bg:#0b0d12;--panel:#151922;--line:#252b38;--ink:#e8ecf3;--mut:#8b95a7;--acc:#5b9dff;--cite:#39d98a;--browse:#5a6677}
117
+ *{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}
118
+ .wrap{max-width:980px;margin:0 auto;padding:28px 20px 80px}
119
+ h1{font-size:20px;margin:0 0 2px}.sub{color:var(--mut);margin:0 0 20px;font-size:13px}
120
+ .stat{font-size:30px;font-weight:700}.stat small{font-size:13px;font-weight:400;color:var(--mut)}
121
+ .cards{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}
122
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
123
+ .card b{font-size:22px;display:block}.card span{color:var(--mut);font-size:12px}
124
+ .bars{margin:8px 0 22px}.bar{display:flex;align-items:center;gap:8px;margin:4px 0}
125
+ .bar .lab{width:150px;color:var(--mut);font-size:12px;text-align:right}.bar .track{flex:1;background:#10141c;border-radius:5px;overflow:hidden}
126
+ .bar .fill{background:var(--acc);height:16px}.bar .n{width:34px;font-size:12px;color:var(--mut)}
127
+ .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}
128
+ .tab.on{background:var(--acc);border-color:var(--acc);color:#04101f;font-weight:600}
129
+ .chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
130
+ .chip{background:var(--panel);border:1px solid var(--line);color:var(--mut);padding:4px 10px;border-radius:20px;cursor:pointer;font-size:12px}
131
+ .chip.on{border-color:var(--acc);color:var(--ink)}.chip b{color:var(--ink)}
132
+ .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}
133
+ .row .idx{color:var(--mut);min-width:22px}.row .main{flex:1}
134
+ .tag{font-size:11px;padding:2px 7px;border-radius:6px;border:1px solid var(--line);color:var(--mut);margin-right:5px}
135
+ .dom{font-weight:600}.url{color:var(--mut);font-size:12px;word-break:break-all}
136
+ .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}
137
+ .copy{background:none;border:1px solid var(--line);color:var(--mut);border-radius:6px;cursor:pointer;font-size:11px;padding:2px 8px}
138
+ .hide{display:none}
139
+ </style>
140
+ </head>
141
+ <body><div class="wrap">
142
+ <h1>AI Search Fan-Out</h1>
143
+ <p class="sub" id="sub"></p>
144
+ <div class="stat" id="stat"></div>
145
+ <div class="cards" id="cards"></div>
146
+ <div id="charts"></div>
147
+ <div class="tabs"><button class="tab on" data-tab="q">Queries</button><button class="tab" data-tab="u">URLs</button></div>
148
+ <div class="chips" id="chips"></div>
149
+ <div id="list"></div>
150
+ </div>
151
+ <script>
152
+ const D=${data};
153
+ const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
154
+ document.getElementById('sub').textContent=D.platform+' \xB7 '+(D.meta.model||'model n/a')+' \xB7 '+D.meta.rounds+' search round(s) \xB7 prompt: '+(D.prompt||'');
155
+ 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>';
156
+ const cards=[['Sub-queries',D.counts.subQueries],['Researched',D.counts.browsed],['Cited',D.counts.cited],['Browsed-only',D.counts.browsedOnly]];
157
+ 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)});
158
+ 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}
159
+ const charts=document.getElementById('charts');
160
+ charts.appendChild(barChart('URL Categories',D.aggregates.byCategory));
161
+ let tab='q',filter=null;
162
+ const chips=document.getElementById('chips'),list=document.getElementById('list');
163
+ function render(){
164
+ chips.innerHTML='';list.innerHTML='';
165
+ if(tab==='q'){
166
+ 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)});
167
+ } else {
168
+ const counts={};D.browsedUrls.forEach(s=>{counts[s.siteType]=(counts[s.siteType]||0)+1});
169
+ 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)});
170
+ 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)});
171
+ }
172
+ }
173
+ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
174
+ 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()});
175
+ render();
176
+ </script>
177
+ </body></html>`;
178
+ }
179
+
29
180
  // src/version.ts
30
- var PACKAGE_VERSION = "0.3.10";
181
+ var PACKAGE_VERSION = "0.3.12";
31
182
 
32
183
  // src/mcp/browser-agent-tool-schemas.ts
33
184
  var import_zod = require("zod");
@@ -164,7 +315,7 @@ var BrowserCaptureFanoutInputSchema = {
164
315
  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."),
165
316
  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."),
166
317
  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."),
167
- 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.")
318
+ export: import_zod.z.boolean().default(false).describe("When true, write JSON, CSV, TSV exports (queries, citations, all sources, browsed-only sources, snippets, domains) and a self-contained HTML report to MCP_SCRAPER_OUTPUT_DIR/fanout, returning paths relative to MCP_SCRAPER_OUTPUT_DIR.")
168
319
  };
169
320
  var FanoutSourceOutput = import_zod.z.object({
170
321
  url: import_zod.z.string(),
@@ -206,15 +357,18 @@ var BrowserCaptureFanoutOutputSchema = {
206
357
  }).describe("Objective aggregates: top sourced sites by frequency, citation order, and URL-category counts."),
207
358
  first_party_domain: import_zod.z.string().nullable(),
208
359
  exports: import_zod.z.object({
360
+ relativeTo: import_zod.z.string(),
209
361
  dir: import_zod.z.string(),
210
362
  json: import_zod.z.string(),
211
363
  queriesCsv: import_zod.z.string(),
212
364
  queriesTsv: import_zod.z.string(),
213
365
  citationsCsv: import_zod.z.string(),
214
366
  sourcesCsv: import_zod.z.string(),
367
+ browsedOnlyCsv: import_zod.z.string(),
368
+ snippetsCsv: import_zod.z.string(),
215
369
  domainsCsv: import_zod.z.string(),
216
370
  report: import_zod.z.string()
217
- }).nullable().describe("Local file paths when export=true, otherwise null."),
371
+ }).nullable().describe("Relative export paths when export=true, otherwise null. Paths are relative to MCP_SCRAPER_OUTPUT_DIR, or ~/Downloads/mcp-scraper when that env var is not set."),
218
372
  debug: import_zod.z.object({
219
373
  interceptorReady: import_zod.z.boolean(),
220
374
  rawBytes: import_zod.z.number().int().min(0),
@@ -366,8 +520,8 @@ var BrowserListSessionsOutputSchema = {
366
520
  // src/mcp/replay-annotator.ts
367
521
  var import_node_child_process = require("child_process");
368
522
  var import_promises = require("fs/promises");
369
- var import_node_os = require("os");
370
- var import_node_path = require("path");
523
+ var import_node_os2 = require("os");
524
+ var import_node_path2 = require("path");
371
525
  var import_node_util = require("util");
372
526
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
373
527
  function finiteNumber(value) {
@@ -377,11 +531,11 @@ function clamp(value, min, max) {
377
531
  return Math.min(max, Math.max(min, value));
378
532
  }
379
533
  function formatAssTime(seconds) {
380
- const safe = Math.max(0, seconds);
381
- const hours = Math.floor(safe / 3600);
382
- const minutes = Math.floor(safe % 3600 / 60);
383
- const wholeSeconds = Math.floor(safe % 60);
384
- const centiseconds = Math.floor((safe - Math.floor(safe)) * 100);
534
+ const safe2 = Math.max(0, seconds);
535
+ const hours = Math.floor(safe2 / 3600);
536
+ const minutes = Math.floor(safe2 % 3600 / 60);
537
+ const wholeSeconds = Math.floor(safe2 % 60);
538
+ const centiseconds = Math.floor((safe2 - Math.floor(safe2)) * 100);
385
539
  return `${hours}:${String(minutes).padStart(2, "0")}:${String(wholeSeconds).padStart(2, "0")}.${String(centiseconds).padStart(2, "0")}`;
386
540
  }
387
541
  function cssHexToAssColor(color) {
@@ -590,8 +744,8 @@ function ffmpegFilterPath(path) {
590
744
  async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
591
745
  if (!options.annotations.length) throw new Error("annotations must include at least one item");
592
746
  const size = await videoSize(inputFilePath);
593
- const tmp = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "mcp-scraper-ass-"));
594
- const assPath = (0, import_node_path.join)(tmp, "annotations.ass");
747
+ const tmp = await (0, import_promises.mkdtemp)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "mcp-scraper-ass-"));
748
+ const assPath = (0, import_node_path2.join)(tmp, "annotations.ass");
595
749
  try {
596
750
  await (0, import_promises.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
597
751
  await execFileAsync("ffmpeg", [
@@ -659,8 +813,8 @@ function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_
659
813
  nextRecommendedTool
660
814
  });
661
815
  }
662
- function outputBaseDir() {
663
- return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), "Downloads", "mcp-scraper");
816
+ function outputBaseDir2() {
817
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path3.join)((0, import_node_os3.homedir)(), "Downloads", "mcp-scraper");
664
818
  }
665
819
  function safeFilePart(value) {
666
820
  return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
@@ -669,7 +823,7 @@ function replayFilePath(sessionId, replayId, filename) {
669
823
  const requested = filename?.trim();
670
824
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
671
825
  const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
672
- return (0, import_node_path2.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
826
+ return (0, import_node_path3.join)(outputBaseDir2(), "browser-replays", `${name}.mp4`);
673
827
  }
674
828
  function slugPart(value) {
675
829
  const trimmed = value?.trim().toLowerCase();
@@ -682,6 +836,11 @@ function savedProfileNameFromEmail(email, domain) {
682
836
  const domainPart = slugPart(domain);
683
837
  return `${value}${domainPart ? `-${domainPart}` : ""}`.slice(0, 80) || null;
684
838
  }
839
+ function isEnrichedFanout(value) {
840
+ if (!value || typeof value !== "object") return false;
841
+ const candidate = value;
842
+ return typeof candidate.platform === "string" && typeof candidate.capturedAt === "string" && typeof candidate.prompt === "string" && Array.isArray(candidate.queries) && Array.isArray(candidate.browsedUrls) && Array.isArray(candidate.citedUrls) && Array.isArray(candidate.browsedOnly) && Array.isArray(candidate.snippets) && !!candidate.counts && !!candidate.aggregates;
843
+ }
685
844
  function annotatedReplayFilePath(sessionId, replayId, filename) {
686
845
  const requested = filename?.trim();
687
846
  if (requested) return replayFilePath(sessionId, replayId, requested);
@@ -744,8 +903,8 @@ function registerBrowserAgentMcpTools(server2, opts) {
744
903
  }
745
904
  const bytes = Buffer.from(await res.arrayBuffer());
746
905
  const filePath = replayFilePath(sessionId, replayId, filename);
747
- (0, import_node_fs.mkdirSync)((0, import_node_path2.join)(outputBaseDir(), "browser-replays"), { recursive: true });
748
- (0, import_node_fs.writeFileSync)(filePath, bytes);
906
+ (0, import_node_fs2.mkdirSync)((0, import_node_path3.join)(outputBaseDir2(), "browser-replays"), { recursive: true });
907
+ (0, import_node_fs2.writeFileSync)(filePath, bytes);
749
908
  return {
750
909
  ok: true,
751
910
  data: {
@@ -1204,7 +1363,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1204
1363
  try {
1205
1364
  const sourcePath = String(downloaded.data.file_path);
1206
1365
  const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
1207
- (0, import_node_fs.mkdirSync)((0, import_node_path2.join)(outputBaseDir(), "browser-replays"), { recursive: true });
1366
+ (0, import_node_fs2.mkdirSync)((0, import_node_path3.join)(outputBaseDir2(), "browser-replays"), { recursive: true });
1208
1367
  const result = await annotateReplayVideo(sourcePath, outputPath, {
1209
1368
  annotations: input.annotations,
1210
1369
  sourceWidth: input.source_width,
@@ -1277,13 +1436,13 @@ function registerBrowserAgentMcpTools(server2, opts) {
1277
1436
  "browser_capture_fanout",
1278
1437
  {
1279
1438
  title: "Capture AI Search Fan-Out",
1280
- 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 connected or if the profile disconnected: call browser_profile_onboard, give the user the watch_url, let them complete login, then call browser_profile_status until status is AUTHENTICATED. After that, browser_open the saved direct no-proxy profile and go 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.`,
1439
+ 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. Set export=true for durable JSON, CSV, TSV, and HTML artifacts; export paths are returned relative to MCP_SCRAPER_OUTPUT_DIR. 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 connected or if the profile disconnected: call browser_profile_onboard, give the user the watch_url, let them complete login, then call browser_profile_status until status is AUTHENTICATED. After that, browser_open the saved direct no-proxy profile and go 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.`,
1281
1440
  inputSchema: BrowserCaptureFanoutInputSchema,
1282
1441
  outputSchema: BrowserCaptureFanoutOutputSchema,
1283
1442
  annotations: annotations("Capture AI Search Fan-Out")
1284
1443
  },
1285
1444
  async (input) => {
1286
- const emit = (result, exports2) => structuredResult({
1445
+ const emit = (result, exports3) => structuredResult({
1287
1446
  ok: true,
1288
1447
  tool: "browser_capture_fanout",
1289
1448
  session_id: input.session_id,
@@ -1299,18 +1458,34 @@ function registerBrowserAgentMcpTools(server2, opts) {
1299
1458
  counts: result.counts,
1300
1459
  aggregates: result.aggregates,
1301
1460
  first_party_domain: result.firstPartyDomain,
1302
- exports: exports2 ?? null,
1461
+ exports: exports3 ?? null,
1303
1462
  ...result.debug ? { debug: result.debug } : {}
1304
1463
  });
1305
1464
  const res = await req("POST", `/agent/sessions/${input.session_id}/capture-fanout`, {
1306
1465
  prompt: input.prompt,
1307
1466
  wait_ms: input.wait_ms,
1308
1467
  first_party_domain: input.first_party_domain,
1309
- export: false
1468
+ reset: input.reset,
1469
+ export: input.export
1310
1470
  });
1311
1471
  if (!res.ok) return errorResult("browser_capture_fanout", res.data, input.session_id);
1312
1472
  const hosted = res.data?.result ?? res.data;
1313
- return emit(hosted, res.data?.exports ?? null);
1473
+ let exports2 = res.data?.exports ?? null;
1474
+ if (input.export && !exports2 && isEnrichedFanout(hosted)) {
1475
+ try {
1476
+ exports2 = exportFanout(hosted);
1477
+ } catch (err) {
1478
+ return errorResult(
1479
+ "browser_capture_fanout",
1480
+ {
1481
+ error: `Fan-out captured but local export failed: ${err instanceof Error ? err.message : String(err)}`,
1482
+ result: hosted
1483
+ },
1484
+ input.session_id
1485
+ );
1486
+ }
1487
+ }
1488
+ return emit(hosted, exports2);
1314
1489
  }
1315
1490
  );
1316
1491
  }
@@ -1318,10 +1493,10 @@ function registerBrowserAgentMcpTools(server2, opts) {
1318
1493
  // bin/browser-agent-stdio-server.ts
1319
1494
  function readApiKeyFile() {
1320
1495
  const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
1321
- const paths = [explicitPath, (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".mcp-scraper-key")].filter(Boolean);
1496
+ const paths = [explicitPath, (0, import_node_path4.join)((0, import_node_os4.homedir)(), ".mcp-scraper-key")].filter(Boolean);
1322
1497
  for (const path of paths) {
1323
1498
  try {
1324
- const value = (0, import_node_fs2.readFileSync)(path, "utf8").trim();
1499
+ const value = (0, import_node_fs3.readFileSync)(path, "utf8").trim();
1325
1500
  if (value) return value;
1326
1501
  } catch {
1327
1502
  }