mcp-scraper 0.6.0 → 0.7.0

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 (51) hide show
  1. package/dist/bin/api-server.cjs +296 -198
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +2 -2
  4. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +1 -1
  7. package/dist/bin/mcp-scraper-install.cjs +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-install.js +1 -1
  10. package/dist/bin/mcp-stdio-server.cjs +3 -3
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +3 -3
  13. package/dist/bin/paa-harvest.cjs +166 -74
  14. package/dist/bin/paa-harvest.cjs.map +1 -1
  15. package/dist/bin/paa-harvest.js +1 -1
  16. package/dist/chunk-EGWJ74EX.js +7 -0
  17. package/dist/chunk-EGWJ74EX.js.map +1 -0
  18. package/dist/{chunk-APJO2XV5.js → chunk-HL33CGJF.js} +3 -3
  19. package/dist/{chunk-APJO2XV5.js.map → chunk-HL33CGJF.js.map} +1 -1
  20. package/dist/{chunk-AUCXKRRH.js → chunk-RMPPYKUV.js} +167 -75
  21. package/dist/chunk-RMPPYKUV.js.map +1 -0
  22. package/dist/{chunk-BL7BBSYF.js → chunk-RUGJE5EB.js} +2 -2
  23. package/dist/{chunk-3K5GT6VG.js → chunk-YGRZU7IR.js} +3 -3
  24. package/dist/index.cjs +166 -74
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/{server-WHM446IZ.js → server-JNY4XPZE.js} +81 -76
  28. package/dist/server-JNY4XPZE.js.map +1 -0
  29. package/dist/{site-extract-repository-U476J44K.js → site-extract-repository-NVSZH35Y.js} +3 -3
  30. package/dist/{worker-KJZ3ZN2N.js → worker-JQTS437L.js} +4 -4
  31. package/dist/worker-JQTS437L.js.map +1 -0
  32. package/package.json +1 -1
  33. package/dist/chunk-AUCXKRRH.js.map +0 -1
  34. package/dist/chunk-F2BXETDX.js +0 -7
  35. package/dist/chunk-F2BXETDX.js.map +0 -1
  36. package/dist/server-WHM446IZ.js.map +0 -1
  37. package/dist/worker-KJZ3ZN2N.js.map +0 -1
  38. package/docs/spec-browser-routing.md +0 -108
  39. package/docs/spec-kernel-computer-controls.md +0 -166
  40. package/docs/spec-mcp-tools.md +0 -146
  41. package/docs/spec-server-instructions.md +0 -94
  42. package/docs/specs/agent-ready-seo-packet-spec.md +0 -237
  43. package/docs/specs/audit-visual-demo.md +0 -309
  44. package/docs/specs/cli-agent-wiring-spec.md +0 -206
  45. package/docs/specs/local-competitive-audit-spec.md +0 -311
  46. package/docs/specs/scheduled-workflows-api-spec.md +0 -304
  47. package/docs/specs/seo-cli-growth-roadmap-spec.md +0 -181
  48. package/docs/specs/seo-workflow-runner-and-reports-spec.md +0 -241
  49. /package/dist/{chunk-BL7BBSYF.js.map → chunk-RUGJE5EB.js.map} +0 -0
  50. /package/dist/{chunk-3K5GT6VG.js.map → chunk-YGRZU7IR.js.map} +0 -0
  51. /package/dist/{site-extract-repository-U476J44K.js.map → site-extract-repository-NVSZH35Y.js.map} +0 -0
@@ -6372,8 +6372,8 @@ async function downloadAsset(url, destDir, filename) {
6372
6372
  }
6373
6373
  const writer = (0, import_node_fs.createWriteStream)(dest);
6374
6374
  await (0, import_promises2.pipeline)(import_node_stream.Readable.fromWeb(res.body), writer);
6375
- const { statSync: statSync3 } = await import("fs");
6376
- const sizeBytes = statSync3(dest).size;
6375
+ const { statSync: statSync2 } = await import("fs");
6376
+ const sizeBytes = statSync2(dest).size;
6377
6377
  return { savedPath: dest, sizeBytes, mimeType };
6378
6378
  }
6379
6379
  async function harvestPageMedia(html, pageUrl, options = {}) {
@@ -7992,8 +7992,8 @@ var init_rates = __esm({
7992
7992
  "use strict";
7993
7993
  MC_COSTS = {
7994
7994
  serp: 100,
7995
- paa: 200,
7996
- paa_base: 1e3,
7995
+ paa: 300,
7996
+ paa_base: 5500,
7997
7997
  page_scrape: 100,
7998
7998
  url_map: 500,
7999
7999
  yt_channel: 200,
@@ -10648,8 +10648,8 @@ var init_blob_store = __esm({
10648
10648
  async get(key) {
10649
10649
  const path6 = (0, import_node_path2.join)(this.baseDir, "blobs", key);
10650
10650
  try {
10651
- const { readFileSync: readFileSync5 } = await import("fs");
10652
- return readFileSync5(path6);
10651
+ const { readFileSync: readFileSync6 } = await import("fs");
10652
+ return readFileSync6(path6);
10653
10653
  } catch {
10654
10654
  return null;
10655
10655
  }
@@ -15788,6 +15788,12 @@ var init_facebook_ad_routes = __esm({
15788
15788
  }
15789
15789
  }
15790
15790
  const results = [...advertiserMap.values()].sort((a, b) => b.adCount - a.adCount).slice(0, maxResults).map((a) => ({ name: a.pageName, pageName: a.pageName, libraryId: a.sampleLibraryId, sampleLibraryId: a.sampleLibraryId, adCount: a.adCount }));
15791
+ if (results.length === 0) {
15792
+ await creditMc(fbUser.id, MC_COSTS.fb_search, LedgerOperation.FB_SEARCH_REFUND, "empty result");
15793
+ searchRefunded = true;
15794
+ await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "failed", query: body.query.trim(), error: "empty result refunded" });
15795
+ return c.json({ error: "no advertisers found (refunded)" }, 503);
15796
+ }
15791
15797
  const searchResult = { query: body.query.trim(), searchUrl, results };
15792
15798
  await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "done", query: body.query.trim(), resultCount: results.length, result: searchResult });
15793
15799
  return c.json(searchResult);
@@ -16600,20 +16606,15 @@ async function resolveInstagramLaunch(body) {
16600
16606
  }
16601
16607
  };
16602
16608
  }
16603
- function outputBaseDir() {
16604
- return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path7.join)((0, import_node_os5.homedir)(), "Downloads", "mcp-scraper");
16605
- }
16606
16609
  function safeFilePart(input) {
16607
16610
  return input.replace(/^https?:\/\//, "").replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "instagram";
16608
16611
  }
16609
- function mediaOutputDir(shortcode, sourceUrl) {
16612
+ function blobKeyPrefix(shortcode, sourceUrl) {
16610
16613
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
16611
16614
  const slug = shortcode ? `instagram-${shortcode}` : safeFilePart(sourceUrl);
16612
- const dir = (0, import_node_path7.join)(outputBaseDir(), "instagram", `${stamp}-${slug}`);
16613
- (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
16614
- return dir;
16615
+ return `instagram/${stamp}-${slug}`;
16615
16616
  }
16616
- async function downloadToFile(url, destPath, referer) {
16617
+ async function fetchMediaBytes(url, referer) {
16617
16618
  const res = await fetch(url, {
16618
16619
  headers: {
16619
16620
  "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
@@ -16623,12 +16624,8 @@ async function downloadToFile(url, destPath, referer) {
16623
16624
  });
16624
16625
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
16625
16626
  if (!res.body) throw new Error("Empty response body");
16626
- await (0, import_promises4.pipeline)(import_node_stream2.Readable.fromWeb(res.body), (0, import_node_fs5.createWriteStream)(destPath));
16627
- return {
16628
- savedPath: destPath,
16629
- sizeBytes: (0, import_node_fs5.statSync)(destPath).size,
16630
- mimeType: res.headers.get("content-type")?.split(";")[0]?.trim() ?? null
16631
- };
16627
+ const bytes = Buffer.from(await res.arrayBuffer());
16628
+ return { bytes, mimeType: res.headers.get("content-type")?.split(";")[0]?.trim() ?? null };
16632
16629
  }
16633
16630
  function extFromMime(mimeType, fallback) {
16634
16631
  const map = {
@@ -16661,7 +16658,7 @@ function trackFilename(track, index, shortcode) {
16661
16658
  const bitrate = track.bitrate ? `-${track.bitrate}` : "";
16662
16659
  return `${label}-${type}-${index}${bitrate}.mp4`;
16663
16660
  }
16664
- var import_hono7, import_zod18, import_node_fs5, import_node_os5, import_node_path7, import_node_stream2, import_promises4, import_node_child_process2, InstagramProfileContentBodySchema, InstagramMediaTypeSchema, InstagramMediaDownloadBodySchema, instagramApp;
16661
+ var import_hono7, import_zod18, import_node_fs5, import_node_os5, import_node_path7, import_node_child_process2, InstagramProfileContentBodySchema, InstagramMediaTypeSchema, InstagramMediaDownloadBodySchema, instagramApp;
16665
16662
  var init_instagram_routes = __esm({
16666
16663
  "src/api/instagram-routes.ts"() {
16667
16664
  "use strict";
@@ -16670,8 +16667,6 @@ var init_instagram_routes = __esm({
16670
16667
  import_node_fs5 = require("fs");
16671
16668
  import_node_os5 = require("os");
16672
16669
  import_node_path7 = require("path");
16673
- import_node_stream2 = require("stream");
16674
- import_promises4 = require("stream/promises");
16675
16670
  import_node_child_process2 = require("child_process");
16676
16671
  init_browser_service_env();
16677
16672
  init_BrowserDriver();
@@ -16680,6 +16675,7 @@ var init_instagram_routes = __esm({
16680
16675
  init_db();
16681
16676
  init_rates();
16682
16677
  init_concurrency_gates();
16678
+ init_blob_store();
16683
16679
  init_InstagramContentExtractor();
16684
16680
  init_media_transcription();
16685
16681
  InstagramProfileContentBodySchema = import_zod18.z.object({
@@ -16794,55 +16790,65 @@ var init_instagram_routes = __esm({
16794
16790
  const mediaTypes = new Set(body.mediaTypes);
16795
16791
  const downloads = [];
16796
16792
  const warnings = [];
16797
- let outputDir = null;
16798
16793
  if (body.downloadMedia) {
16799
- outputDir = mediaOutputDir(extraction.shortcode, sourceUrl.href);
16800
- const textPath = (0, import_node_path7.join)(outputDir, `${extraction.shortcode ?? "instagram"}-text.txt`);
16801
- (0, import_node_fs5.writeFileSync)(textPath, [
16802
- `URL: ${extraction.pageUrl}`,
16803
- extraction.caption ? `Caption: ${extraction.caption}` : "",
16804
- "",
16805
- extraction.bodyText
16806
- ].filter(Boolean).join("\n"), "utf8");
16807
- downloads.push({ kind: "text", url: null, savedPath: textPath, sizeBytes: (0, import_node_fs5.statSync)(textPath).size, mimeType: "text/plain", error: null });
16808
- if (mediaTypes.has("image") && extraction.imageUrl) {
16809
- const imagePathBase = (0, import_node_path7.join)(outputDir, `${extraction.shortcode ?? "instagram"}-image`);
16810
- try {
16811
- const downloaded = await downloadToFile(extraction.imageUrl, imagePathBase, extraction.pageUrl);
16812
- const ext = extFromMime(downloaded.mimeType, "jpg");
16813
- const finalPath = downloaded.savedPath.endsWith(`.${ext}`) ? downloaded.savedPath : `${downloaded.savedPath}.${ext}`;
16814
- if (finalPath !== downloaded.savedPath) {
16815
- const { renameSync } = await import("fs");
16816
- renameSync(downloaded.savedPath, finalPath);
16794
+ let muxWorkDir = null;
16795
+ try {
16796
+ const store = getBlobStore();
16797
+ const prefix = blobKeyPrefix(extraction.shortcode, sourceUrl.href);
16798
+ const textContent = [
16799
+ `URL: ${extraction.pageUrl}`,
16800
+ extraction.caption ? `Caption: ${extraction.caption}` : "",
16801
+ "",
16802
+ extraction.bodyText
16803
+ ].filter(Boolean).join("\n");
16804
+ const storedText = await store.put(`${prefix}/text.txt`, textContent, "text/plain");
16805
+ downloads.push({ kind: "text", url: null, savedPath: storedText.url, sizeBytes: storedText.bytes, mimeType: "text/plain", error: null });
16806
+ if (mediaTypes.has("image") && extraction.imageUrl) {
16807
+ try {
16808
+ const { bytes, mimeType } = await fetchMediaBytes(extraction.imageUrl, extraction.pageUrl);
16809
+ const ext = extFromMime(mimeType, "jpg");
16810
+ const stored = await store.put(`${prefix}/image.${ext}`, bytes, mimeType ?? "application/octet-stream");
16811
+ downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: stored.url, sizeBytes: stored.bytes, mimeType, error: null });
16812
+ } catch (err) {
16813
+ downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
16817
16814
  }
16818
- downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: finalPath, sizeBytes: (0, import_node_fs5.statSync)(finalPath).size, mimeType: downloaded.mimeType, error: null });
16819
- } catch (err) {
16820
- downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
16821
16815
  }
16822
- }
16823
- const tracksToDownload = body.downloadAllTracks ? extraction.tracks : [extraction.selectedVideoTrack, extraction.selectedAudioTrack].filter((track) => Boolean(track));
16824
- const savedByUrl = /* @__PURE__ */ new Map();
16825
- for (const [index, track] of tracksToDownload.entries()) {
16826
- if (track.streamType === "video" && !mediaTypes.has("video")) continue;
16827
- if (track.streamType === "audio" && !mediaTypes.has("audio")) continue;
16828
- const kind = track.streamType === "audio" ? "audio" : "video";
16829
- const target = (0, import_node_path7.join)(outputDir, trackFilename(track, index, extraction.shortcode));
16830
- try {
16831
- const downloaded = await downloadToFile(track.url, target, extraction.pageUrl);
16832
- downloads.push({ kind, url: track.url, savedPath: downloaded.savedPath, sizeBytes: downloaded.sizeBytes, mimeType: downloaded.mimeType, error: null });
16833
- savedByUrl.set(track.url, downloaded.savedPath);
16834
- } catch (err) {
16835
- downloads.push({ kind, url: track.url, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
16816
+ const tracksToDownload = body.downloadAllTracks ? extraction.tracks : [extraction.selectedVideoTrack, extraction.selectedAudioTrack].filter((track) => Boolean(track));
16817
+ const muxEligible = Boolean(body.mux && mediaTypes.has("video") && mediaTypes.has("audio") && extraction.selectedVideoTrack && extraction.selectedAudioTrack);
16818
+ const muxInputs = /* @__PURE__ */ new Map();
16819
+ for (const [index, track] of tracksToDownload.entries()) {
16820
+ if (track.streamType === "video" && !mediaTypes.has("video")) continue;
16821
+ if (track.streamType === "audio" && !mediaTypes.has("audio")) continue;
16822
+ const kind = track.streamType === "audio" ? "audio" : "video";
16823
+ try {
16824
+ const { bytes, mimeType } = await fetchMediaBytes(track.url, extraction.pageUrl);
16825
+ const stored = await store.put(`${prefix}/${trackFilename(track, index, extraction.shortcode)}`, bytes, mimeType ?? "video/mp4");
16826
+ downloads.push({ kind, url: track.url, savedPath: stored.url, sizeBytes: stored.bytes, mimeType, error: null });
16827
+ const isMuxInput = muxEligible && (track.url === extraction.selectedVideoTrack.url || track.url === extraction.selectedAudioTrack.url);
16828
+ if (isMuxInput) {
16829
+ if (!muxWorkDir) muxWorkDir = (0, import_node_fs5.mkdtempSync)((0, import_node_path7.join)((0, import_node_os5.tmpdir)(), "ig-mux-"));
16830
+ const localPath = (0, import_node_path7.join)(muxWorkDir, trackFilename(track, index, extraction.shortcode));
16831
+ (0, import_node_fs5.writeFileSync)(localPath, bytes);
16832
+ muxInputs.set(track.url, localPath);
16833
+ }
16834
+ } catch (err) {
16835
+ downloads.push({ kind, url: track.url, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
16836
+ }
16836
16837
  }
16837
- }
16838
- if (body.mux && mediaTypes.has("video") && mediaTypes.has("audio") && extraction.selectedVideoTrack && extraction.selectedAudioTrack && savedByUrl.has(extraction.selectedVideoTrack.url) && savedByUrl.has(extraction.selectedAudioTrack.url)) {
16839
- const outPath = (0, import_node_path7.join)(outputDir, `${extraction.shortcode ?? "instagram"}-muxed.mp4`);
16840
- const muxed = await runFfmpegMux(savedByUrl.get(extraction.selectedVideoTrack.url), savedByUrl.get(extraction.selectedAudioTrack.url), outPath);
16841
- if (muxed.ok) {
16842
- downloads.push({ kind: "muxed_video", url: null, savedPath: outPath, sizeBytes: (0, import_node_fs5.statSync)(outPath).size, mimeType: "video/mp4", error: null });
16843
- } else {
16844
- warnings.push(`Mux skipped/failed: ${muxed.error}`);
16838
+ if (muxEligible && muxWorkDir && muxInputs.has(extraction.selectedVideoTrack.url) && muxInputs.has(extraction.selectedAudioTrack.url)) {
16839
+ const outPath = (0, import_node_path7.join)(muxWorkDir, "muxed.mp4");
16840
+ const muxed = await runFfmpegMux(muxInputs.get(extraction.selectedVideoTrack.url), muxInputs.get(extraction.selectedAudioTrack.url), outPath);
16841
+ if (muxed.ok) {
16842
+ const stored = await store.put(`${prefix}/muxed.mp4`, (0, import_node_fs5.readFileSync)(outPath), "video/mp4");
16843
+ downloads.push({ kind: "muxed_video", url: null, savedPath: stored.url, sizeBytes: stored.bytes, mimeType: "video/mp4", error: null });
16844
+ } else {
16845
+ warnings.push(`Mux skipped/failed: ${muxed.error}`);
16846
+ }
16845
16847
  }
16848
+ } catch (err) {
16849
+ warnings.push(`Media download unavailable: ${err instanceof Error ? err.message : String(err)}`);
16850
+ } finally {
16851
+ if (muxWorkDir) (0, import_node_fs5.rmSync)(muxWorkDir, { recursive: true, force: true });
16846
16852
  }
16847
16853
  }
16848
16854
  let transcript = null;
@@ -16865,7 +16871,7 @@ var init_instagram_routes = __esm({
16865
16871
  const result = {
16866
16872
  ...extraction,
16867
16873
  browser: launch.browser,
16868
- outputDir,
16874
+ outputDir: null,
16869
16875
  downloads,
16870
16876
  warnings,
16871
16877
  transcript
@@ -19064,12 +19070,12 @@ function reportTitle(full) {
19064
19070
  const title = full.split("\n").find((line) => line.startsWith("# "));
19065
19071
  return title?.replace(/^#\s+/, "").trim() || "MCP Scraper Report";
19066
19072
  }
19067
- function outputBaseDir2() {
19073
+ function outputBaseDir() {
19068
19074
  return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path8.join)((0, import_node_os6.homedir)(), "Downloads", "mcp-scraper");
19069
19075
  }
19070
19076
  function saveFullReport(full) {
19071
19077
  if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
19072
- const outDir = outputBaseDir2();
19078
+ const outDir = outputBaseDir();
19073
19079
  try {
19074
19080
  (0, import_node_fs6.mkdirSync)(outDir, { recursive: true });
19075
19081
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -19087,7 +19093,7 @@ function saveBulkSite(siteUrl, pages, seo, imageAudit) {
19087
19093
  if (!reportSavingActive()) return null;
19088
19094
  try {
19089
19095
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
19090
- const dir = (0, import_node_path8.join)(outputBaseDir2(), `extract-${slugifyReportName(siteUrl)}-${stamp}`);
19096
+ const dir = (0, import_node_path8.join)(outputBaseDir(), `extract-${slugifyReportName(siteUrl)}-${stamp}`);
19091
19097
  const pagesDir = (0, import_node_path8.join)(dir, "pages");
19092
19098
  (0, import_node_fs6.mkdirSync)(pagesDir, { recursive: true });
19093
19099
  const indexRows = pages.map((p, i) => {
@@ -19180,7 +19186,7 @@ ${renderImageSection(imageAudit)}` : ""), "utf8");
19180
19186
  function saveUrlInventory(siteUrl, urls) {
19181
19187
  if (!reportSavingActive()) return null;
19182
19188
  try {
19183
- const outDir = outputBaseDir2();
19189
+ const outDir = outputBaseDir();
19184
19190
  (0, import_node_fs6.mkdirSync)(outDir, { recursive: true });
19185
19191
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
19186
19192
  const file = (0, import_node_path8.join)(outDir, `${stamp}-urlmap-${slugifyReportName(siteUrl.replace(/^https?:\/\//, ""))}.csv`);
@@ -19195,7 +19201,7 @@ function saveUrlInventory(siteUrl, urls) {
19195
19201
  function persistScreenshotLocally(base64, url) {
19196
19202
  if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
19197
19203
  try {
19198
- const dir = (0, import_node_path8.join)(outputBaseDir2(), "screenshots");
19204
+ const dir = (0, import_node_path8.join)(outputBaseDir(), "screenshots");
19199
19205
  (0, import_node_fs6.mkdirSync)(dir, { recursive: true });
19200
19206
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
19201
19207
  const slug = url.replace(/^https?:\/\//, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 60);
@@ -21475,7 +21481,7 @@ function localLocationFileAllowed() {
21475
21481
  async function existingPath(value) {
21476
21482
  const trimmed = value?.trim();
21477
21483
  if (!trimmed) return null;
21478
- await (0, import_promises5.access)(trimmed);
21484
+ await (0, import_promises4.access)(trimmed);
21479
21485
  return trimmed;
21480
21486
  }
21481
21487
  async function resolveUsZipsPath(requestedPath) {
@@ -21493,7 +21499,7 @@ async function loadZipGroups(stateAbbr, requestedPath, warnings) {
21493
21499
  }
21494
21500
  const path6 = await resolveUsZipsPath(requestedPath);
21495
21501
  if (!path6) return { path: null, groups: /* @__PURE__ */ new Map() };
21496
- const records = csvRecords(await (0, import_promises5.readFile)(path6, "utf8"));
21502
+ const records = csvRecords(await (0, import_promises4.readFile)(path6, "utf8"));
21497
21503
  const groups = /* @__PURE__ */ new Map();
21498
21504
  for (const record of records) {
21499
21505
  const state = (record.state_abbr ?? record.state ?? "").trim().toUpperCase();
@@ -21544,11 +21550,11 @@ async function resolveDirectoryMarkets(options) {
21544
21550
  }
21545
21551
  return { markets, censusSourceUrl: sourceUrl, usZipsSourcePath: zipData.path, warnings };
21546
21552
  }
21547
- var import_promises5, POPULATION_YEARS, STATE_META, STATE_BY_NAME;
21553
+ var import_promises4, POPULATION_YEARS, STATE_META, STATE_BY_NAME;
21548
21554
  var init_location_db = __esm({
21549
21555
  "src/directory/location-db.ts"() {
21550
21556
  "use strict";
21551
- import_promises5 = require("fs/promises");
21557
+ import_promises4 = require("fs/promises");
21552
21558
  init_csv();
21553
21559
  POPULATION_YEARS = [2020, 2021, 2022, 2023, 2024, 2025];
21554
21560
  STATE_META = {
@@ -21764,8 +21770,8 @@ function csvRowsFor(result) {
21764
21770
  return rows;
21765
21771
  }
21766
21772
  async function saveDirectoryCsv(result) {
21767
- const outDir = (0, import_node_path9.join)(outputBaseDir2(), "directory-workflows");
21768
- await (0, import_promises6.mkdir)(outDir, { recursive: true });
21773
+ const outDir = (0, import_node_path9.join)(outputBaseDir(), "directory-workflows");
21774
+ await (0, import_promises5.mkdir)(outDir, { recursive: true });
21769
21775
  const stamp = result.extractedAt.replace(/[:.]/g, "-");
21770
21776
  const slug = `${result.state}-${result.query}`.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
21771
21777
  const path6 = (0, import_node_path9.join)(outDir, `${stamp}-${slug}-directory-workflow.csv`);
@@ -21800,7 +21806,7 @@ async function saveDirectoryCsv(result) {
21800
21806
  "extracted_at",
21801
21807
  "duration_ms"
21802
21808
  ];
21803
- await (0, import_promises6.writeFile)(path6, rowsToCsv(headers, csvRowsFor(result)), "utf8");
21809
+ await (0, import_promises5.writeFile)(path6, rowsToCsv(headers, csvRowsFor(result)), "utf8");
21804
21810
  return path6;
21805
21811
  }
21806
21812
  async function runDirectoryWorkflowFromPlan(options, plan) {
@@ -21826,11 +21832,11 @@ async function runDirectoryWorkflowFromPlan(options, plan) {
21826
21832
  const csvPath = options.saveCsv ? await saveDirectoryCsv(base) : null;
21827
21833
  return { ...base, csvPath };
21828
21834
  }
21829
- var import_promises6, import_node_path9, import_zod24, DirectoryWorkflowOptionsSchema;
21835
+ var import_promises5, import_node_path9, import_zod24, DirectoryWorkflowOptionsSchema;
21830
21836
  var init_directory_workflow = __esm({
21831
21837
  "src/directory/directory-workflow.ts"() {
21832
21838
  "use strict";
21833
- import_promises6 = require("fs/promises");
21839
+ import_promises5 = require("fs/promises");
21834
21840
  import_node_path9 = require("path");
21835
21841
  import_zod24 = require("zod");
21836
21842
  init_mcp_response_formatter();
@@ -21998,7 +22004,7 @@ async function readIndex(baseDir) {
21998
22004
  const path6 = indexPath(baseDir);
21999
22005
  if (!(0, import_node_fs7.existsSync)(path6)) return { runs: [] };
22000
22006
  try {
22001
- return JSON.parse(await (0, import_promises7.readFile)(path6, "utf8"));
22007
+ return JSON.parse(await (0, import_promises6.readFile)(path6, "utf8"));
22002
22008
  } catch {
22003
22009
  return { runs: [] };
22004
22010
  }
@@ -22018,14 +22024,14 @@ async function updateWorkflowIndex(manifest, manifestPath, baseDir) {
22018
22024
  summary: `${manifest.title} (${manifest.status})`
22019
22025
  };
22020
22026
  index.runs = [entry, ...index.runs.filter((r) => r.runId !== manifest.runId)].slice(0, 200);
22021
- await (0, import_promises7.mkdir)((0, import_node_path10.dirname)(path6), { recursive: true });
22022
- await (0, import_promises7.writeFile)(path6, JSON.stringify(index, null, 2), "utf8");
22027
+ await (0, import_promises6.mkdir)((0, import_node_path10.dirname)(path6), { recursive: true });
22028
+ await (0, import_promises6.writeFile)(path6, JSON.stringify(index, null, 2), "utf8");
22023
22029
  }
22024
- var import_promises7, import_node_fs7, import_node_os7, import_node_path10, import_node_child_process3, ArtifactWriter;
22030
+ var import_promises6, import_node_fs7, import_node_os7, import_node_path10, import_node_child_process3, ArtifactWriter;
22025
22031
  var init_artifact_writer = __esm({
22026
22032
  "src/workflows/artifact-writer.ts"() {
22027
22033
  "use strict";
22028
- import_promises7 = require("fs/promises");
22034
+ import_promises6 = require("fs/promises");
22029
22035
  import_node_fs7 = require("fs");
22030
22036
  import_node_os7 = require("os");
22031
22037
  import_node_path10 = require("path");
@@ -22057,31 +22063,31 @@ var init_artifact_writer = __esm({
22057
22063
  const nameSource = String(input.keyword ?? input.query ?? input.domain ?? input.state ?? workflowId);
22058
22064
  const baseDir = workflowOutputBaseDir(outputDir);
22059
22065
  const runDir = (0, import_node_path10.join)(baseDir, "workflows", workflowId, `${timestamp()}-${safeSlug(nameSource)}-${safeSlug(runId).slice(0, 20)}`);
22060
- await (0, import_promises7.mkdir)(runDir, { recursive: true });
22066
+ await (0, import_promises6.mkdir)(runDir, { recursive: true });
22061
22067
  return new _ArtifactWriter(workflowId, title, runId, baseDir, runDir, startedAt, input);
22062
22068
  }
22063
22069
  async remember(kind, label, path6, rows) {
22064
- const size = await (0, import_promises7.stat)(path6).then((s) => s.size).catch(() => void 0);
22070
+ const size = await (0, import_promises6.stat)(path6).then((s) => s.size).catch(() => void 0);
22065
22071
  this.artifacts.push({ kind, label, path: path6, bytes: size, rows });
22066
22072
  await postToMemoryLibrary({ title: `${this.title} ${label}`, content: `artifact: ${path6}`, source: `mcp-scraper-workflow:${this.workflowId}:${this.runId}` });
22067
22073
  return path6;
22068
22074
  }
22069
22075
  async writeJson(label, relativePath, data) {
22070
22076
  const path6 = (0, import_node_path10.join)(this.runDir, relativePath);
22071
- await (0, import_promises7.mkdir)((0, import_node_path10.dirname)(path6), { recursive: true });
22072
- await (0, import_promises7.writeFile)(path6, JSON.stringify(data, null, 2), "utf8");
22077
+ await (0, import_promises6.mkdir)((0, import_node_path10.dirname)(path6), { recursive: true });
22078
+ await (0, import_promises6.writeFile)(path6, JSON.stringify(data, null, 2), "utf8");
22073
22079
  return this.remember("json", label, path6);
22074
22080
  }
22075
22081
  async writeText(label, relativePath, text, kind = "markdown") {
22076
22082
  const path6 = (0, import_node_path10.join)(this.runDir, relativePath);
22077
- await (0, import_promises7.mkdir)((0, import_node_path10.dirname)(path6), { recursive: true });
22078
- await (0, import_promises7.writeFile)(path6, text, "utf8");
22083
+ await (0, import_promises6.mkdir)((0, import_node_path10.dirname)(path6), { recursive: true });
22084
+ await (0, import_promises6.writeFile)(path6, text, "utf8");
22079
22085
  return this.remember(kind, label, path6);
22080
22086
  }
22081
22087
  async writeCsv(label, relativePath, headers, rows) {
22082
22088
  const path6 = (0, import_node_path10.join)(this.runDir, relativePath);
22083
- await (0, import_promises7.mkdir)((0, import_node_path10.dirname)(path6), { recursive: true });
22084
- await (0, import_promises7.writeFile)(path6, rowsToCsv(headers, rows), "utf8");
22089
+ await (0, import_promises6.mkdir)((0, import_node_path10.dirname)(path6), { recursive: true });
22090
+ await (0, import_promises6.writeFile)(path6, rowsToCsv(headers, rows), "utf8");
22085
22091
  return this.remember("csv", label, path6, rows.length);
22086
22092
  }
22087
22093
  async writeHtml(label, relativePath, html) {
@@ -22102,7 +22108,7 @@ var init_artifact_writer = __esm({
22102
22108
  counts
22103
22109
  };
22104
22110
  const path6 = (0, import_node_path10.join)(this.runDir, "manifest.json");
22105
- await (0, import_promises7.writeFile)(path6, JSON.stringify(manifest, null, 2), "utf8");
22111
+ await (0, import_promises6.writeFile)(path6, JSON.stringify(manifest, null, 2), "utf8");
22106
22112
  await updateWorkflowIndex(manifest, path6, this.baseDir);
22107
22113
  return path6;
22108
22114
  }
@@ -23828,7 +23834,7 @@ async function runWorkflowStep(id, rawInput, opts) {
23828
23834
  const writtenArtifacts = await Promise.all(
23829
23835
  artifacts.artifacts.map(async (artifact) => ({
23830
23836
  ...artifact,
23831
- content: await (0, import_promises8.readFile)(artifact.path, "utf8").catch(() => "")
23837
+ content: await (0, import_promises7.readFile)(artifact.path, "utf8").catch(() => "")
23832
23838
  }))
23833
23839
  );
23834
23840
  return {
@@ -23847,11 +23853,11 @@ async function runWorkflowStep(id, rawInput, opts) {
23847
23853
  artifacts: writtenArtifacts
23848
23854
  };
23849
23855
  }
23850
- var import_promises8, import_zod30, DEFINITIONS;
23856
+ var import_promises7, import_zod30, DEFINITIONS;
23851
23857
  var init_registry2 = __esm({
23852
23858
  "src/workflows/registry.ts"() {
23853
23859
  "use strict";
23854
- import_promises8 = require("fs/promises");
23860
+ import_promises7 = require("fs/promises");
23855
23861
  import_zod30 = require("zod");
23856
23862
  init_artifact_writer();
23857
23863
  init_http_client2();
@@ -23922,7 +23928,7 @@ async function readManifestFromSummary(summary) {
23922
23928
  if (!summary.reportPath) return null;
23923
23929
  const manifestPath = summary.reportPath.replace(/report\.html$/, "manifest.json");
23924
23930
  try {
23925
- return JSON.parse(await (0, import_promises9.readFile)(manifestPath, "utf8"));
23931
+ return JSON.parse(await (0, import_promises8.readFile)(manifestPath, "utf8"));
23926
23932
  } catch {
23927
23933
  return null;
23928
23934
  }
@@ -24137,12 +24143,12 @@ async function dispatchDueWorkflowSchedules(apiUrl, limit = 3) {
24137
24143
  }
24138
24144
  return { dispatched: results.length, results };
24139
24145
  }
24140
- var import_node_crypto8, import_promises9, import_hono14, import_zod31, workflowApp, WorkflowInputSchema, WorkflowIdSchema, CadenceSchema, ScheduleStatusSchema, RunBodySchema, ScheduleCreateSchema, SchedulePatchSchema, TERMINAL_RUN_STATUSES;
24146
+ var import_node_crypto8, import_promises8, import_hono14, import_zod31, workflowApp, WorkflowInputSchema, WorkflowIdSchema, CadenceSchema, ScheduleStatusSchema, RunBodySchema, ScheduleCreateSchema, SchedulePatchSchema, TERMINAL_RUN_STATUSES;
24141
24147
  var init_workflow_routes = __esm({
24142
24148
  "src/api/workflow-routes.ts"() {
24143
24149
  "use strict";
24144
24150
  import_node_crypto8 = require("crypto");
24145
- import_promises9 = require("fs/promises");
24151
+ import_promises8 = require("fs/promises");
24146
24152
  import_hono14 = require("hono");
24147
24153
  import_zod31 = require("zod");
24148
24154
  init_artifact_writer();
@@ -24312,7 +24318,7 @@ var init_workflow_routes = __esm({
24312
24318
  return new Response(artifact.content, { headers });
24313
24319
  }
24314
24320
  try {
24315
- const content = await (0, import_promises9.readFile)(artifact.path);
24321
+ const content = await (0, import_promises8.readFile)(artifact.path);
24316
24322
  return new Response(content, { headers });
24317
24323
  } catch {
24318
24324
  return c.json({ error: "Artifact file is no longer available" }, 410);
@@ -25251,13 +25257,41 @@ var init_PAAExtractor = __esm({
25251
25257
  }
25252
25258
  return parts.join(" ").replace(/\s+/g, " ").trim();
25253
25259
  }
25254
- return Array.from(document.querySelectorAll(selectors.item)).map((pair) => ({
25255
- question: pair.getAttribute(selectors.itemDataQ) || pair.getAttribute(selectors.itemDataInitQ) || pair.querySelector(selectors.itemQuestionEl)?.innerText?.trim() || "",
25256
- answer: cleanText(pair.querySelector(selectors.answerContainer)) || void 0,
25257
- sourceTitle: pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0,
25258
- sourceSite: pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0,
25259
- sourceCite: pair.querySelector(selectors.sourceCite)?.innerText?.trim() || void 0
25260
- }));
25260
+ return Array.from(document.querySelectorAll(selectors.item)).map((pair) => {
25261
+ let sourceTitle = pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0;
25262
+ let sourceSite = pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0;
25263
+ let sourceCite = pair.querySelector(selectors.sourceCite)?.innerText?.trim() || void 0;
25264
+ if (!sourceCite) {
25265
+ const seenHrefs = /* @__PURE__ */ new Set();
25266
+ const anchors = [];
25267
+ for (const a of Array.from(pair.querySelectorAll("a[href]"))) {
25268
+ const href = a.href;
25269
+ if (!/^https?:\/\//.test(href) || seenHrefs.has(href)) continue;
25270
+ let host = "";
25271
+ try {
25272
+ host = new URL(href).hostname;
25273
+ } catch {
25274
+ continue;
25275
+ }
25276
+ if (/(^|\.)google(usercontent)?\.[a-z.]+$/.test(host)) continue;
25277
+ seenHrefs.add(href);
25278
+ anchors.push({ text: (a.textContent || "").trim(), href, host });
25279
+ }
25280
+ const primary = anchors.find((x) => x.text.length > 2) ?? anchors[0];
25281
+ if (primary) {
25282
+ sourceTitle = sourceTitle || primary.text || void 0;
25283
+ sourceSite = sourceSite || primary.host.replace(/^www\./, "");
25284
+ sourceCite = primary.href;
25285
+ }
25286
+ }
25287
+ return {
25288
+ question: pair.getAttribute(selectors.itemDataQ) || pair.getAttribute(selectors.itemDataInitQ) || pair.querySelector(selectors.itemQuestionEl)?.innerText?.trim() || "",
25289
+ answer: cleanText(pair.querySelector(selectors.answerContainer)) || void 0,
25290
+ sourceTitle,
25291
+ sourceSite,
25292
+ sourceCite
25293
+ };
25294
+ });
25261
25295
  }, sels);
25262
25296
  return raw.flatMap((item) => {
25263
25297
  const cleaned = {
@@ -25299,54 +25333,115 @@ var init_PAAExtractor = __esm({
25299
25333
  const seenQs = /* @__PURE__ */ new Set();
25300
25334
  const orderedQs = [];
25301
25335
  const results = [];
25302
- const readAllQs = () => page.evaluate(
25303
- ({ sel, dataQ, dataInitQ, questionEl }) => Array.from(document.querySelectorAll(sel)).map(
25304
- (el2) => el2.getAttribute(dataQ) || el2.getAttribute(dataInitQ) || el2.querySelector(questionEl)?.innerText?.trim() || ""
25305
- ).filter(Boolean),
25306
- { sel: PAASelectors.item, dataQ: PAASelectors.itemDataQ, dataInitQ: PAASelectors.itemDataInitQ, questionEl: PAASelectors.itemQuestionEl }
25336
+ const abandonedNeverReclick = /* @__PURE__ */ new Set();
25337
+ const clickedOnceEver = /* @__PURE__ */ new Set();
25338
+ const itemArgs = {
25339
+ sel: PAASelectors.item,
25340
+ dataQ: PAASelectors.itemDataQ,
25341
+ dataInitQ: PAASelectors.itemDataInitQ,
25342
+ questionEl: PAASelectors.itemQuestionEl,
25343
+ answerSel: PAASelectors.answerContainer,
25344
+ expandedClass: PAASelectors.expandedClass,
25345
+ clickTarget: PAASelectors.clickTarget
25346
+ };
25347
+ const readItemStates = () => page.evaluate(
25348
+ ({ sel, dataQ, dataInitQ, questionEl, expandedClass, answerSel }) => {
25349
+ const answerText = (root) => {
25350
+ if (!root) return "";
25351
+ let out = "";
25352
+ const walk = (n) => {
25353
+ for (const c of Array.from(n.childNodes)) {
25354
+ if (c.nodeType === Node.TEXT_NODE) out += c.textContent || "";
25355
+ else if (c.nodeType === Node.ELEMENT_NODE && c.tagName !== "STYLE" && c.tagName !== "SCRIPT") walk(c);
25356
+ }
25357
+ };
25358
+ walk(root);
25359
+ return out.replace(/\s+/g, " ").trim();
25360
+ };
25361
+ return Array.from(document.querySelectorAll(sel)).map((el2) => ({
25362
+ q: el2.getAttribute(dataQ) || el2.getAttribute(dataInitQ) || el2.querySelector(questionEl)?.innerText?.trim() || "",
25363
+ done: el2.classList.contains(expandedClass) || answerText(el2.querySelector(answerSel)).length > 30
25364
+ })).filter((s) => s.q);
25365
+ },
25366
+ itemArgs
25307
25367
  );
25368
+ const expandOneItemSerially = async (q) => {
25369
+ const clicked = await page.evaluate(
25370
+ ({ sel, dataQ, dataInitQ, clickTarget, q: target }) => {
25371
+ const item = Array.from(document.querySelectorAll(sel)).find((el2) => (el2.getAttribute(dataQ) || el2.getAttribute(dataInitQ)) === target);
25372
+ const btn = item?.querySelector(clickTarget);
25373
+ if (!btn) return false;
25374
+ btn.scrollIntoView({ block: "center" });
25375
+ btn.click();
25376
+ return true;
25377
+ },
25378
+ { ...itemArgs, q }
25379
+ ).catch(() => false);
25380
+ if (!clicked) return "failed";
25381
+ try {
25382
+ const outcome = await page.waitForFunction(
25383
+ ({ sel, dataQ, dataInitQ, answerSel, q: target }) => {
25384
+ const answerText = (root) => {
25385
+ if (!root) return "";
25386
+ let out = "";
25387
+ const walk = (n) => {
25388
+ for (const c of Array.from(n.childNodes)) {
25389
+ if (c.nodeType === Node.TEXT_NODE) out += c.textContent || "";
25390
+ else if (c.nodeType === Node.ELEMENT_NODE && c.tagName !== "STYLE" && c.tagName !== "SCRIPT") walk(c);
25391
+ }
25392
+ };
25393
+ walk(root);
25394
+ return out.replace(/\s+/g, " ").trim();
25395
+ };
25396
+ const item = Array.from(document.querySelectorAll(sel)).find((el2) => (el2.getAttribute(dataQ) || el2.getAttribute(dataInitQ)) === target);
25397
+ if (!item) return "gone";
25398
+ const text = answerText(item.querySelector(answerSel));
25399
+ if (/an error has occurred/i.test(text.slice(0, 80))) return "error";
25400
+ if (text.length > 30) return "ok";
25401
+ return false;
25402
+ },
25403
+ { ...itemArgs, q },
25404
+ { timeout: 6500, polling: 200 }
25405
+ );
25406
+ return await outcome.jsonValue() === "ok" ? "ok" : "failed";
25407
+ } catch {
25408
+ return "failed";
25409
+ }
25410
+ };
25308
25411
  let round = 0;
25309
- while (seenQs.size < options.maxQuestions) {
25412
+ let growthWaits = 0;
25413
+ while (true) {
25310
25414
  if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
25311
25415
  this.throwIfAborted(signal);
25312
- await this.throwIfCaptcha(page, "Google PAA expansion");
25313
- const beforeQs = await readAllQs();
25314
- if (beforeQs.length >= options.maxQuestions) break;
25315
- const unexpandedSel = `${PAASelectors.item}:not(.${PAASelectors.expandedClass}) ${PAASelectors.clickTarget}`;
25316
- const unexpandedCount = await page.locator(unexpandedSel).count();
25317
- if (unexpandedCount === 0) break;
25318
- this.reporter.onDepth(++round);
25319
- for (let ci = 0; ci < unexpandedCount; ci++) {
25320
- if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
25321
- this.throwIfAborted(signal);
25322
- try {
25323
- const btn = page.locator(unexpandedSel).first();
25324
- await btn.scrollIntoViewIfNeeded();
25325
- await btn.hover({ force: true });
25326
- await page.waitForTimeout(100);
25327
- await btn.click({ force: true });
25328
- await page.waitForTimeout(500);
25329
- } catch {
25416
+ const states = await readItemStates();
25417
+ for (const s of states) {
25418
+ if (!seenQs.has(s.q)) {
25419
+ seenQs.add(s.q);
25420
+ orderedQs.push(s.q);
25330
25421
  }
25331
25422
  }
25332
- await page.waitForFunction(
25333
- ({ sel, min }) => document.querySelectorAll(sel).length > min,
25334
- { sel: PAASelectors.item, min: beforeQs.length },
25335
- { timeout: 5e3 }
25336
- ).catch(() => {
25337
- });
25338
- await this.throwIfCaptcha(page, "Google PAA expansion");
25339
- const afterQs = await readAllQs();
25340
- if (afterQs.length === beforeQs.length) break;
25341
- for (const q of afterQs) {
25342
- if (!seenQs.has(q)) {
25343
- seenQs.add(q);
25344
- orderedQs.push(q);
25345
- }
25423
+ const kept = new Set(orderedQs.slice(0, options.maxQuestions));
25424
+ const clickable = states.filter((s) => kept.has(s.q) && !clickedOnceEver.has(s.q) && !abandonedNeverReclick.has(s.q));
25425
+ const target = clickable.find((s) => !s.done) ?? (seenQs.size < options.maxQuestions ? clickable[0] : void 0);
25426
+ if (!target) {
25427
+ if (seenQs.size >= options.maxQuestions) break;
25428
+ if (++growthWaits > 1) break;
25429
+ const grew = await page.waitForFunction(
25430
+ ({ sel, min }) => document.querySelectorAll(sel).length > min,
25431
+ { sel: PAASelectors.item, min: states.length },
25432
+ { timeout: 3e3 }
25433
+ ).catch(() => null);
25434
+ if (!grew) break;
25435
+ continue;
25346
25436
  }
25437
+ growthWaits = 0;
25438
+ this.reporter.onDepth(++round);
25439
+ await this.throwIfCaptcha(page, "Google PAA expansion");
25440
+ clickedOnceEver.add(target.q);
25441
+ if (await expandOneItemSerially(target.q) === "failed") abandonedNeverReclick.add(target.q);
25347
25442
  }
25348
25443
  const itemMap = new Map((await this.extractVisibleItems(page)).map((i) => [i.question, i]));
25349
- await this.fillIncompleteItems(page, orderedQs, itemMap, options);
25444
+ await this.fillIncompleteItems(page, orderedQs, itemMap, options, abandonedNeverReclick);
25350
25445
  for (const q of orderedQs) {
25351
25446
  if (results.length >= options.maxQuestions) break;
25352
25447
  const key = this.normalizeQuestion(q);
@@ -25364,9 +25459,11 @@ var init_PAAExtractor = __esm({
25364
25459
  this.completeness.paaWithoutSource = results.filter((r) => !r.source_title && !r.source_site && !r.source_cite).length;
25365
25460
  return results;
25366
25461
  }
25367
- async fillIncompleteItems(page, orderedQs, itemMap, options) {
25462
+ async fillIncompleteItems(page, orderedQs, itemMap, options, abandonedNeverReclick) {
25368
25463
  const kept = new Set(orderedQs.slice(0, options.maxQuestions));
25369
- const missingBefore = new Set([...kept].filter((q) => !itemMap.get(q)?.answer));
25464
+ const missingBefore = new Set(
25465
+ [...kept].filter((q) => !itemMap.get(q)?.answer && !abandonedNeverReclick?.has(q))
25466
+ );
25370
25467
  if (missingBefore.size === 0) return;
25371
25468
  for (let round = 0; round < 2; round++) {
25372
25469
  if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
@@ -25758,40 +25855,41 @@ var init_PAAExtractor = __esm({
25758
25855
  };
25759
25856
  }
25760
25857
  async captureAIOverviewShareUrl(page, options) {
25761
- for (let attempt = 0; attempt < 3; attempt++) {
25762
- if (options?.softDeadlineMs && Date.now() >= options.softDeadlineMs) return null;
25763
- const clicked = await page.evaluate(() => {
25764
- const heads = Array.from(document.querySelectorAll('h1,h2,h3,[role="heading"],div')).filter((h) => (h.textContent || "").trim() === "AI Overview");
25765
- for (const head of heads) {
25766
- let sec = head;
25767
- for (let up = 0; up < 8 && sec; up++) {
25768
- const btn = sec.querySelector('[aria-label="Share"]');
25769
- if (btn) {
25770
- btn.scrollIntoView({ block: "center" });
25771
- btn.click();
25772
- return true;
25773
- }
25774
- sec = sec.parentElement;
25858
+ if (options?.softDeadlineMs && Date.now() >= options.softDeadlineMs) return null;
25859
+ const clicked = await page.evaluate(() => {
25860
+ const heads = Array.from(document.querySelectorAll('h1,h2,h3,[role="heading"],div')).filter((h) => (h.textContent || "").trim() === "AI Overview");
25861
+ for (const head of heads) {
25862
+ let sec = head;
25863
+ for (let up = 0; up < 8 && sec; up++) {
25864
+ const btn = sec.querySelector('[aria-label="Share"]');
25865
+ if (btn) {
25866
+ btn.scrollIntoView({ block: "center" });
25867
+ btn.click();
25868
+ return true;
25775
25869
  }
25870
+ sec = sec.parentElement;
25776
25871
  }
25777
- return false;
25778
- }).catch(() => false);
25779
- if (!clicked) return null;
25780
- await page.waitForTimeout(4500);
25781
- const url = await page.evaluate(() => {
25872
+ }
25873
+ return false;
25874
+ }).catch(() => false);
25875
+ if (!clicked) return null;
25876
+ let url = null;
25877
+ try {
25878
+ const found = await page.waitForFunction(() => {
25782
25879
  const w = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
25783
25880
  while (w.nextNode()) {
25784
25881
  const m = (w.currentNode.textContent || "").match(/https:\/\/share\.google\/aimode\/\S+/);
25785
25882
  if (m) return m[0];
25786
25883
  }
25787
- return null;
25788
- }).catch(() => null);
25789
- await page.keyboard.press("Escape").catch(() => {
25790
- });
25791
- if (url) return url;
25792
- await page.waitForTimeout(600);
25884
+ return false;
25885
+ }, void 0, { timeout: 4500, polling: 250 });
25886
+ url = await found.jsonValue();
25887
+ } catch {
25888
+ url = null;
25793
25889
  }
25794
- return null;
25890
+ await page.keyboard.press("Escape").catch(() => {
25891
+ });
25892
+ return url;
25795
25893
  }
25796
25894
  buildTree(flat, _seed) {
25797
25895
  const roots = [];
@@ -27302,7 +27400,7 @@ var PACKAGE_VERSION;
27302
27400
  var init_version = __esm({
27303
27401
  "src/version.ts"() {
27304
27402
  "use strict";
27305
- PACKAGE_VERSION = "0.6.0";
27403
+ PACKAGE_VERSION = "0.7.0";
27306
27404
  }
27307
27405
  });
27308
27406
 
@@ -28872,7 +28970,7 @@ function localPlanningToolAnnotations(title) {
28872
28970
  }
28873
28971
  function listSavedReports() {
28874
28972
  try {
28875
- const dir = outputBaseDir2();
28973
+ const dir = outputBaseDir();
28876
28974
  return (0, import_node_fs9.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs9.statSync)((0, import_node_path12.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 100);
28877
28975
  } catch {
28878
28976
  return [];
@@ -28899,7 +28997,7 @@ function registerSavedReportResources(server) {
28899
28997
  const requested = Array.isArray(variables.filename) ? variables.filename[0] : variables.filename;
28900
28998
  const filename = (0, import_node_path12.basename)(decodeURIComponent(String(requested ?? "")));
28901
28999
  if (!filename.endsWith(".md")) throw new Error("Only saved .md reports can be read");
28902
- const text = (0, import_node_fs9.readFileSync)((0, import_node_path12.join)(outputBaseDir2(), filename), "utf8");
29000
+ const text = (0, import_node_fs9.readFileSync)((0, import_node_path12.join)(outputBaseDir(), filename), "utf8");
28903
29001
  return { contents: [{ uri: uri.href, mimeType: "text/markdown", text }] };
28904
29002
  }
28905
29003
  );
@@ -29498,7 +29596,7 @@ var init_http_mcp_tool_executor = __esm({
29498
29596
  });
29499
29597
 
29500
29598
  // src/services/fanout/export.ts
29501
- function outputBaseDir3() {
29599
+ function outputBaseDir2() {
29502
29600
  return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path13.join)((0, import_node_os8.homedir)(), "Downloads", "mcp-scraper");
29503
29601
  }
29504
29602
  function safe(value) {
@@ -29509,7 +29607,7 @@ function writeTable(path6, rows, delimiter) {
29509
29607
  }
29510
29608
  function exportFanout(enriched) {
29511
29609
  const stamp = safe(enriched.capturedAt.replace(/[:.]/g, "-"));
29512
- const outputDir = outputBaseDir3();
29610
+ const outputDir = outputBaseDir2();
29513
29611
  const relativeDir = (0, import_node_path13.join)("fanout", `${stamp}-${safe(enriched.platform)}`);
29514
29612
  const dir = (0, import_node_path13.join)(outputDir, relativeDir);
29515
29613
  (0, import_node_fs10.mkdirSync)(dir, { recursive: true });
@@ -30243,10 +30341,10 @@ function ffmpegFilterPath(path6) {
30243
30341
  async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
30244
30342
  if (!options.annotations.length) throw new Error("annotations must include at least one item");
30245
30343
  const size = await videoSize(inputFilePath);
30246
- const tmp = await (0, import_promises10.mkdtemp)((0, import_node_path14.join)((0, import_node_os9.tmpdir)(), "mcp-scraper-ass-"));
30344
+ const tmp = await (0, import_promises9.mkdtemp)((0, import_node_path14.join)((0, import_node_os9.tmpdir)(), "mcp-scraper-ass-"));
30247
30345
  const assPath = (0, import_node_path14.join)(tmp, "annotations.ass");
30248
30346
  try {
30249
- await (0, import_promises10.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
30347
+ await (0, import_promises9.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
30250
30348
  await execFileAsync2("ffmpeg", [
30251
30349
  "-y",
30252
30350
  "-i",
@@ -30263,7 +30361,7 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
30263
30361
  "copy",
30264
30362
  outputFilePath
30265
30363
  ], { maxBuffer: 1024 * 1024 * 20 });
30266
- const out = await (0, import_promises10.stat)(outputFilePath);
30364
+ const out = await (0, import_promises9.stat)(outputFilePath);
30267
30365
  return {
30268
30366
  filePath: outputFilePath,
30269
30367
  bytes: out.size,
@@ -30272,15 +30370,15 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
30272
30370
  annotationCount: options.annotations.length
30273
30371
  };
30274
30372
  } finally {
30275
- await (0, import_promises10.rm)(tmp, { recursive: true, force: true });
30373
+ await (0, import_promises9.rm)(tmp, { recursive: true, force: true });
30276
30374
  }
30277
30375
  }
30278
- var import_node_child_process4, import_promises10, import_node_os9, import_node_path14, import_node_util2, execFileAsync2;
30376
+ var import_node_child_process4, import_promises9, import_node_os9, import_node_path14, import_node_util2, execFileAsync2;
30279
30377
  var init_replay_annotator = __esm({
30280
30378
  "src/mcp/replay-annotator.ts"() {
30281
30379
  "use strict";
30282
30380
  import_node_child_process4 = require("child_process");
30283
- import_promises10 = require("fs/promises");
30381
+ import_promises9 = require("fs/promises");
30284
30382
  import_node_os9 = require("os");
30285
30383
  import_node_path14 = require("path");
30286
30384
  import_node_util2 = require("util");
@@ -30325,7 +30423,7 @@ function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_
30325
30423
  nextRecommendedTool
30326
30424
  });
30327
30425
  }
30328
- function outputBaseDir4() {
30426
+ function outputBaseDir3() {
30329
30427
  return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path15.join)((0, import_node_os10.homedir)(), "Downloads", "mcp-scraper");
30330
30428
  }
30331
30429
  function safeFilePart2(value) {
@@ -30335,7 +30433,7 @@ function replayFilePath(sessionId, replayId, filename) {
30335
30433
  const requested = filename?.trim();
30336
30434
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
30337
30435
  const name = requested ? safeFilePart2(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart2(sessionId)}-${safeFilePart2(replayId)}`;
30338
- return (0, import_node_path15.join)(outputBaseDir4(), "browser-replays", `${name}.mp4`);
30436
+ return (0, import_node_path15.join)(outputBaseDir3(), "browser-replays", `${name}.mp4`);
30339
30437
  }
30340
30438
  function slugPart(value) {
30341
30439
  const trimmed = value?.trim().toLowerCase();
@@ -30414,7 +30512,7 @@ function registerBrowserAgentMcpTools(server, opts) {
30414
30512
  }
30415
30513
  const bytes = Buffer.from(await res.arrayBuffer());
30416
30514
  const filePath = replayFilePath(sessionId, replayId, filename);
30417
- (0, import_node_fs11.mkdirSync)((0, import_node_path15.join)(outputBaseDir4(), "browser-replays"), { recursive: true });
30515
+ (0, import_node_fs11.mkdirSync)((0, import_node_path15.join)(outputBaseDir3(), "browser-replays"), { recursive: true });
30418
30516
  (0, import_node_fs11.writeFileSync)(filePath, bytes);
30419
30517
  return {
30420
30518
  ok: true,
@@ -30964,7 +31062,7 @@ function registerBrowserAgentMcpTools(server, opts) {
30964
31062
  try {
30965
31063
  const sourcePath = String(downloaded.data.file_path);
30966
31064
  const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
30967
- (0, import_node_fs11.mkdirSync)((0, import_node_path15.join)(outputBaseDir4(), "browser-replays"), { recursive: true });
31065
+ (0, import_node_fs11.mkdirSync)((0, import_node_path15.join)(outputBaseDir3(), "browser-replays"), { recursive: true });
30968
31066
  const result = await annotateReplayVideo(sourcePath, outputPath, {
30969
31067
  annotations: input.annotations,
30970
31068
  sourceWidth: input.source_width,
@@ -38245,16 +38343,16 @@ async function cleanupLocal(cutoff) {
38245
38343
  let deleted = 0;
38246
38344
  let entries;
38247
38345
  try {
38248
- entries = await (0, import_promises11.readdir)(dir);
38346
+ entries = await (0, import_promises10.readdir)(dir);
38249
38347
  } catch {
38250
38348
  return { deleted: 0, store: "local" };
38251
38349
  }
38252
38350
  for (const name of entries) {
38253
38351
  const path6 = (0, import_node_path16.join)(dir, name);
38254
38352
  try {
38255
- const s = await (0, import_promises11.stat)(path6);
38353
+ const s = await (0, import_promises10.stat)(path6);
38256
38354
  if (s.isFile() && s.mtimeMs < cutoff) {
38257
- await (0, import_promises11.unlink)(path6);
38355
+ await (0, import_promises10.unlink)(path6);
38258
38356
  deleted++;
38259
38357
  }
38260
38358
  } catch {
@@ -38271,11 +38369,11 @@ async function cleanupExpiredScrapeBlobs(maxAgeMs = SCRAPE_BLOB_TTL_MS) {
38271
38369
  return { deleted: 0, store: "none" };
38272
38370
  }
38273
38371
  }
38274
- var import_promises11, import_node_os11, import_node_path16;
38372
+ var import_promises10, import_node_os11, import_node_path16;
38275
38373
  var init_scrape_blob_cleanup = __esm({
38276
38374
  "src/api/scrape-blob-cleanup.ts"() {
38277
38375
  "use strict";
38278
- import_promises11 = require("fs/promises");
38376
+ import_promises10 = require("fs/promises");
38279
38377
  import_node_os11 = require("os");
38280
38378
  import_node_path16 = require("path");
38281
38379
  init_scrape_vault_sink();
@@ -38594,7 +38692,7 @@ function countPaaQuestions(result) {
38594
38692
  return Array.isArray(value.flat) ? value.flat.length : 0;
38595
38693
  }
38596
38694
  function paaCostForQuestionCount(questionCount) {
38597
- return Math.max(1, questionCount) * MC_COSTS.paa;
38695
+ return MC_COSTS.paa_base + Math.max(1, questionCount) * MC_COSTS.paa;
38598
38696
  }
38599
38697
  async function processJob(job) {
38600
38698
  running++;