mcp-scraper 0.3.11 → 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 +2 -2
  2. package/dist/bin/api-server.cjs +86 -22
  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 +198 -27
  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 +254 -56
  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-VMH7SRWY.js → chunk-7R7VBQRV.js} +39 -12
  30. package/dist/chunk-7R7VBQRV.js.map +1 -0
  31. package/dist/{chunk-L27GJQV7.js → chunk-BW3DGFNQ.js} +28 -5
  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-3SA5Q4OF.js → server-CUPDJLWM.js} +42 -151
  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 +1 -1
  43. package/package.json +1 -1
  44. package/dist/chunk-L27GJQV7.js.map +0 -1
  45. package/dist/chunk-RRE7WVHQ.js +0 -7
  46. package/dist/chunk-RRE7WVHQ.js.map +0 -1
  47. package/dist/chunk-UWSG3C5J.js.map +0 -1
  48. package/dist/chunk-VMH7SRWY.js.map +0 -1
  49. package/dist/server-3SA5Q4OF.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-3SA5Q4OF.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.11";
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");
@@ -369,8 +520,8 @@ var BrowserListSessionsOutputSchema = {
369
520
  // src/mcp/replay-annotator.ts
370
521
  var import_node_child_process = require("child_process");
371
522
  var import_promises = require("fs/promises");
372
- var import_node_os = require("os");
373
- var import_node_path = require("path");
523
+ var import_node_os2 = require("os");
524
+ var import_node_path2 = require("path");
374
525
  var import_node_util = require("util");
375
526
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
376
527
  function finiteNumber(value) {
@@ -380,11 +531,11 @@ function clamp(value, min, max) {
380
531
  return Math.min(max, Math.max(min, value));
381
532
  }
382
533
  function formatAssTime(seconds) {
383
- const safe = Math.max(0, seconds);
384
- const hours = Math.floor(safe / 3600);
385
- const minutes = Math.floor(safe % 3600 / 60);
386
- const wholeSeconds = Math.floor(safe % 60);
387
- 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);
388
539
  return `${hours}:${String(minutes).padStart(2, "0")}:${String(wholeSeconds).padStart(2, "0")}.${String(centiseconds).padStart(2, "0")}`;
389
540
  }
390
541
  function cssHexToAssColor(color) {
@@ -593,8 +744,8 @@ function ffmpegFilterPath(path) {
593
744
  async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
594
745
  if (!options.annotations.length) throw new Error("annotations must include at least one item");
595
746
  const size = await videoSize(inputFilePath);
596
- const tmp = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "mcp-scraper-ass-"));
597
- 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");
598
749
  try {
599
750
  await (0, import_promises.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
600
751
  await execFileAsync("ffmpeg", [
@@ -662,8 +813,8 @@ function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_
662
813
  nextRecommendedTool
663
814
  });
664
815
  }
665
- function outputBaseDir() {
666
- 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");
667
818
  }
668
819
  function safeFilePart(value) {
669
820
  return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
@@ -672,7 +823,7 @@ function replayFilePath(sessionId, replayId, filename) {
672
823
  const requested = filename?.trim();
673
824
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
674
825
  const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
675
- return (0, import_node_path2.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
826
+ return (0, import_node_path3.join)(outputBaseDir2(), "browser-replays", `${name}.mp4`);
676
827
  }
677
828
  function slugPart(value) {
678
829
  const trimmed = value?.trim().toLowerCase();
@@ -685,6 +836,11 @@ function savedProfileNameFromEmail(email, domain) {
685
836
  const domainPart = slugPart(domain);
686
837
  return `${value}${domainPart ? `-${domainPart}` : ""}`.slice(0, 80) || null;
687
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
+ }
688
844
  function annotatedReplayFilePath(sessionId, replayId, filename) {
689
845
  const requested = filename?.trim();
690
846
  if (requested) return replayFilePath(sessionId, replayId, requested);
@@ -747,8 +903,8 @@ function registerBrowserAgentMcpTools(server2, opts) {
747
903
  }
748
904
  const bytes = Buffer.from(await res.arrayBuffer());
749
905
  const filePath = replayFilePath(sessionId, replayId, filename);
750
- (0, import_node_fs.mkdirSync)((0, import_node_path2.join)(outputBaseDir(), "browser-replays"), { recursive: true });
751
- (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);
752
908
  return {
753
909
  ok: true,
754
910
  data: {
@@ -1207,7 +1363,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1207
1363
  try {
1208
1364
  const sourcePath = String(downloaded.data.file_path);
1209
1365
  const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
1210
- (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 });
1211
1367
  const result = await annotateReplayVideo(sourcePath, outputPath, {
1212
1368
  annotations: input.annotations,
1213
1369
  sourceWidth: input.source_width,
@@ -1286,7 +1442,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1286
1442
  annotations: annotations("Capture AI Search Fan-Out")
1287
1443
  },
1288
1444
  async (input) => {
1289
- const emit = (result, exports2) => structuredResult({
1445
+ const emit = (result, exports3) => structuredResult({
1290
1446
  ok: true,
1291
1447
  tool: "browser_capture_fanout",
1292
1448
  session_id: input.session_id,
@@ -1302,7 +1458,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1302
1458
  counts: result.counts,
1303
1459
  aggregates: result.aggregates,
1304
1460
  first_party_domain: result.firstPartyDomain,
1305
- exports: exports2 ?? null,
1461
+ exports: exports3 ?? null,
1306
1462
  ...result.debug ? { debug: result.debug } : {}
1307
1463
  });
1308
1464
  const res = await req("POST", `/agent/sessions/${input.session_id}/capture-fanout`, {
@@ -1314,7 +1470,22 @@ function registerBrowserAgentMcpTools(server2, opts) {
1314
1470
  });
1315
1471
  if (!res.ok) return errorResult("browser_capture_fanout", res.data, input.session_id);
1316
1472
  const hosted = res.data?.result ?? res.data;
1317
- 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);
1318
1489
  }
1319
1490
  );
1320
1491
  }
@@ -1322,10 +1493,10 @@ function registerBrowserAgentMcpTools(server2, opts) {
1322
1493
  // bin/browser-agent-stdio-server.ts
1323
1494
  function readApiKeyFile() {
1324
1495
  const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
1325
- 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);
1326
1497
  for (const path of paths) {
1327
1498
  try {
1328
- const value = (0, import_node_fs2.readFileSync)(path, "utf8").trim();
1499
+ const value = (0, import_node_fs3.readFileSync)(path, "utf8").trim();
1329
1500
  if (value) return value;
1330
1501
  } catch {
1331
1502
  }