mcp-scraper 0.3.24 → 0.3.25

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 (65) hide show
  1. package/dist/bin/api-server.cjs +8140 -5941
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +3 -3
  4. package/dist/bin/mcp-scraper-cli.cjs +320 -58
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +4 -3
  7. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +3 -3
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +2 -2
  11. package/dist/bin/mcp-stdio-server.cjs +121 -24
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +5 -5
  14. package/dist/bin/paa-harvest.cjs +284 -10
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +5 -3
  17. package/dist/bin/paa-harvest.js.map +1 -1
  18. package/dist/{chunk-ZAP7MYVO.js → chunk-3PRO376E.js} +2 -3
  19. package/dist/chunk-3PRO376E.js.map +1 -0
  20. package/dist/{chunk-LFATOGDF.js → chunk-BP27CZ5Q.js} +4 -3
  21. package/dist/chunk-BP27CZ5Q.js.map +1 -0
  22. package/dist/{chunk-DE7WP4UU.js → chunk-DOBQN3EY.js} +3 -3
  23. package/dist/chunk-DOBQN3EY.js.map +1 -0
  24. package/dist/{chunk-GZ4W72WF.js → chunk-FE2WC4JR.js} +119 -23
  25. package/dist/chunk-FE2WC4JR.js.map +1 -0
  26. package/dist/{chunk-IGOEAKC7.js → chunk-G7PQ64LM.js} +274 -9
  27. package/dist/chunk-G7PQ64LM.js.map +1 -0
  28. package/dist/chunk-K443GQY5.js +24 -0
  29. package/dist/chunk-K443GQY5.js.map +1 -0
  30. package/dist/{chunk-H2R232HK.js → chunk-NNEIXK5L.js} +307 -62
  31. package/dist/chunk-NNEIXK5L.js.map +1 -0
  32. package/dist/{chunk-3KYRG7O7.js → chunk-TRNSXR46.js} +185 -2
  33. package/dist/chunk-TRNSXR46.js.map +1 -0
  34. package/dist/chunk-WPHAMNT7.js +7 -0
  35. package/dist/chunk-WPHAMNT7.js.map +1 -0
  36. package/dist/{chunk-JTWQGPKF.js → chunk-WYCER2HW.js} +33 -10
  37. package/dist/chunk-WYCER2HW.js.map +1 -0
  38. package/dist/{db-EG5ETPTY.js → db-FP2ABUU4.js} +32 -2
  39. package/dist/index.cjs +284 -10
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +7 -3
  42. package/dist/index.d.ts +7 -3
  43. package/dist/index.js +5 -3
  44. package/dist/index.js.map +1 -1
  45. package/dist/{server-2SRARAO5.js → server-RRCKVBHG.js} +1563 -265
  46. package/dist/server-RRCKVBHG.js.map +1 -0
  47. package/dist/{worker-UM5UM6O5.js → worker-UC6D2756.js} +7 -6
  48. package/dist/{worker-UM5UM6O5.js.map → worker-UC6D2756.js.map} +1 -1
  49. package/docs/kernel-proxy-future-enhancements.md +80 -0
  50. package/docs/spec-browser-routing.md +108 -0
  51. package/docs/spec-mcp-tools.md +146 -0
  52. package/docs/specs/audit-visual-demo.md +309 -0
  53. package/package.json +5 -2
  54. package/dist/chunk-3KYRG7O7.js.map +0 -1
  55. package/dist/chunk-DE7WP4UU.js.map +0 -1
  56. package/dist/chunk-GZ4W72WF.js.map +0 -1
  57. package/dist/chunk-H2R232HK.js.map +0 -1
  58. package/dist/chunk-H7R4PPMG.js +0 -7
  59. package/dist/chunk-H7R4PPMG.js.map +0 -1
  60. package/dist/chunk-IGOEAKC7.js.map +0 -1
  61. package/dist/chunk-JTWQGPKF.js.map +0 -1
  62. package/dist/chunk-LFATOGDF.js.map +0 -1
  63. package/dist/chunk-ZAP7MYVO.js.map +0 -1
  64. package/dist/server-2SRARAO5.js.map +0 -1
  65. /package/dist/{db-EG5ETPTY.js.map → db-FP2ABUU4.js.map} +0 -0
@@ -1,6 +1,14 @@
1
+ import {
2
+ postToMemoryLibrary
3
+ } from "./chunk-K443GQY5.js";
1
4
  import {
2
5
  DEFAULT_MAPS_PROXY_MODE
3
- } from "./chunk-LFATOGDF.js";
6
+ } from "./chunk-BP27CZ5Q.js";
7
+
8
+ // src/lib/slugify.ts
9
+ function slugify(s) {
10
+ return s.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
11
+ }
4
12
 
5
13
  // src/workflows/artifact-writer.ts
6
14
  import { mkdir, readFile, stat, writeFile } from "fs/promises";
@@ -72,11 +80,6 @@ function rowsToCsv(headers, rows) {
72
80
  ].join("\n") + "\n";
73
81
  }
74
82
 
75
- // src/lib/slugify.ts
76
- function slugify(s) {
77
- return s.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
78
- }
79
-
80
83
  // src/workflows/artifact-writer.ts
81
84
  function workflowOutputBaseDir(outputDir) {
82
85
  return outputDir?.trim() || process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join(homedir(), "Downloads", "mcp-scraper");
@@ -117,6 +120,7 @@ var ArtifactWriter = class _ArtifactWriter {
117
120
  async remember(kind, label, path, rows) {
118
121
  const size = await stat(path).then((s) => s.size).catch(() => void 0);
119
122
  this.artifacts.push({ kind, label, path, bytes: size, rows });
123
+ await postToMemoryLibrary({ title: `${this.title} ${label}`, content: `artifact: ${path}`, source: `mcp-scraper-workflow:${this.workflowId}:${this.runId}` });
120
124
  return path;
121
125
  }
122
126
  async writeJson(label, relativePath, data) {
@@ -211,7 +215,7 @@ async function openWorkflowReport(id, outputDir) {
211
215
 
212
216
  // src/workflows/registry.ts
213
217
  import { readFile as readFile2 } from "fs/promises";
214
- import { z as z5 } from "zod";
218
+ import { z as z6 } from "zod";
215
219
 
216
220
  // src/workflows/http-client.ts
217
221
  var WorkflowHttpClient = class {
@@ -662,20 +666,41 @@ ${summary}
662
666
  }
663
667
  };
664
668
 
665
- // src/workflows/workflows/local-competitive-audit.ts
669
+ // src/workflows/workflows/get-leads.ts
666
670
  import { z as z3 } from "zod";
667
- var LocalCompetitiveAuditInputSchema = z3.object({
668
- query: z3.string().min(1),
669
- state: z3.string().min(2).default("TN"),
670
- minPopulation: z3.number().int().min(0).default(1e5),
671
- maxCities: z3.number().int().min(1).max(100).default(25),
672
- maxResultsPerCity: z3.number().int().min(1).max(50).default(20),
673
- hydrateTop: z3.number().int().min(0).max(10).default(5),
674
- maxReviews: z3.number().int().min(0).max(500).default(50),
675
- concurrency: z3.number().int().min(1).max(5).default(5),
676
- proxyMode: z3.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
677
- returnPartial: z3.boolean().default(true)
671
+ var GetLeadsInputSchema = z3.object({
672
+ query: z3.string().min(1).describe('Business category, niche, or keyword to search on Google Maps, e.g. "roofers", "med spas", "dentists". Do not include the city here.'),
673
+ location: z3.string().min(1).describe('City / market to search, e.g. "Houston, TX" or "Austin, Texas".'),
674
+ maxResults: z3.number().int().min(1).max(50).default(25).describe("How many Maps businesses to collect for the market. Maximum 50."),
675
+ enrichWebsites: z3.boolean().default(true).describe("Visit each business website (home + contact pages) to harvest email and social links. Uses the proxy/browser-backed extractor so blocked sites still resolve."),
676
+ hydrateReviewCounts: z3.boolean().default(true).describe("Deep-dive each profile to confirm the review count and booking URL that the Maps search list omits."),
677
+ concurrency: z3.number().int().min(1).max(4).default(3).describe("How many businesses to enrich in parallel. Keep low to respect per-account concurrency limits."),
678
+ proxyMode: z3.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy targeting for the Maps search. location targets the market; none is for local debugging.")
678
679
  });
680
+ var LEADS_CSV_HEADERS = [
681
+ "position",
682
+ "business_name",
683
+ "review_stars",
684
+ "review_count",
685
+ "category",
686
+ "address",
687
+ "phone",
688
+ "website",
689
+ "email",
690
+ "email_domain_match",
691
+ "email_status",
692
+ "socials",
693
+ "booking_url",
694
+ "place_url",
695
+ "cid",
696
+ "source_location"
697
+ ];
698
+ var EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
699
+ var MAILTO_RE = /mailto:([^"'?>\s]+)/gi;
700
+ var SOCIAL_RE = /https?:\/\/(?:www\.)?(?:facebook|instagram|linkedin|twitter|youtube|tiktok)\.com\/[^\s"'<>)]+/gi;
701
+ var EMAIL_BAD = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico", "sentry", "wix.com", "wixpress", "example.", "godaddy", "squarespace", "schema.org", "@2x", "u003e", "core-js"];
702
+ var EMAIL_PREFIX = ["info@", "office@", "contact@", "sales@", "hello@", "admin@", "service@", "support@"];
703
+ var SOCIAL_SKIP = ["/sharer", "/share?", "/intent/", "plugins/", "/dialog/", "platform.", "badge", "developers."];
679
704
  async function mapLimit(items, limit, fn) {
680
705
  const out = new Array(items.length);
681
706
  let next = 0;
@@ -689,6 +714,224 @@ async function mapLimit(items, limit, fn) {
689
714
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
690
715
  return out;
691
716
  }
717
+ function hostRoot(url) {
718
+ try {
719
+ return new URL(url).hostname.replace(/^www\./, "").split(".")[0].toLowerCase();
720
+ } catch {
721
+ return "";
722
+ }
723
+ }
724
+ function cleanEmails(raw) {
725
+ const out = /* @__PURE__ */ new Set();
726
+ for (const value of raw) {
727
+ const email = value.trim().toLowerCase().replace(/\.$/, "");
728
+ if (email.split("@").length !== 2) continue;
729
+ if (email.length > 60) continue;
730
+ if (EMAIL_BAD.some((bad) => email.includes(bad))) continue;
731
+ out.add(email);
732
+ }
733
+ return [...out];
734
+ }
735
+ function pickEmail(emails, host) {
736
+ if (!emails.length) return "";
737
+ const root = hostRoot(host);
738
+ for (const prefix of EMAIL_PREFIX) {
739
+ const hit = emails.find((e) => e.startsWith(prefix) && root && e.split("@")[1].includes(root));
740
+ if (hit) return hit;
741
+ }
742
+ const domainHit = emails.find((e) => root && e.split("@")[1].includes(root));
743
+ if (domainHit) return domainHit;
744
+ for (const prefix of EMAIL_PREFIX) {
745
+ const hit = emails.find((e) => e.startsWith(prefix));
746
+ if (hit) return hit;
747
+ }
748
+ return emails[0];
749
+ }
750
+ function parseContacts(html) {
751
+ const mailtos = [...html.matchAll(MAILTO_RE)].map((m) => decodeURIComponent(m[1]).split("?")[0]);
752
+ const inline = html.match(EMAIL_RE) ?? [];
753
+ const emails = cleanEmails([...mailtos, ...inline]);
754
+ const socials = [...new Set((html.match(SOCIAL_RE) ?? []).map((s) => s.replace(/[)"'<>]+$/, "")).filter((s) => !SOCIAL_SKIP.some((skip) => s.toLowerCase().includes(skip))))].slice(0, 5);
755
+ return { emails, socials };
756
+ }
757
+ function buildAttemptUrls(website) {
758
+ const urls = /* @__PURE__ */ new Set();
759
+ urls.add(website);
760
+ try {
761
+ const origin = new URL(website).origin;
762
+ urls.add(`${origin}/contact`);
763
+ urls.add(`${origin}/contact-us`);
764
+ } catch {
765
+ return [...urls];
766
+ }
767
+ return [...urls].slice(0, 3);
768
+ }
769
+ var getLeadsWorkflowDefinition = {
770
+ id: "get-leads",
771
+ title: "Get Leads",
772
+ description: "Build an outreach-ready local lead list: Google Maps search for a niche in a market, confirm review counts + booking URLs, then visit each business website (home + contact) to harvest email and social links, with proxy/browser-backed extraction so blocked sites still resolve. Saves a leads CSV.",
773
+ inputSchema: GetLeadsInputSchema,
774
+ async run(input, ctx) {
775
+ await ctx.artifacts.writeManifest("running", {}, [], []);
776
+ const warnings = [];
777
+ const errors = [];
778
+ const search = await ctx.client.post("/maps/search", {
779
+ query: input.query,
780
+ location: input.location,
781
+ maxResults: input.maxResults,
782
+ proxyMode: input.proxyMode
783
+ }, 18e4);
784
+ await ctx.artifacts.writeJson("Maps search raw", "raw/maps-search.json", search);
785
+ const seen = /* @__PURE__ */ new Set();
786
+ const deduped = search.results.filter((result) => {
787
+ const key = result.cid ?? result.placeUrl ?? result.name.toLowerCase();
788
+ if (seen.has(key)) return false;
789
+ seen.add(key);
790
+ return true;
791
+ });
792
+ const rows = await mapLimit(deduped, input.concurrency, async (result) => {
793
+ let reviewCount = result.reviewCount ?? "";
794
+ let bookingUrl = "";
795
+ let website = result.websiteUrl ?? "";
796
+ if (input.hydrateReviewCounts) {
797
+ try {
798
+ const place = await ctx.client.post("/maps/place", {
799
+ businessName: result.name,
800
+ location: input.location,
801
+ includeReviews: false
802
+ }, 18e4);
803
+ reviewCount = place.reviewCount ?? reviewCount;
804
+ bookingUrl = place.bookingUrl ?? "";
805
+ website = website || (place.website ?? "");
806
+ } catch (err) {
807
+ warnings.push(`Review hydration failed for ${result.name}: ${err instanceof Error ? err.message : String(err)}`);
808
+ }
809
+ }
810
+ let email = "";
811
+ let socials = [];
812
+ let emailStatus = website ? "no email found" : "no website";
813
+ let domainMatch = "";
814
+ if (input.enrichWebsites && website) {
815
+ const attempts = buildAttemptUrls(website);
816
+ let reached = false;
817
+ const allEmails = [];
818
+ const allSocials = /* @__PURE__ */ new Set();
819
+ for (const attemptUrl of attempts) {
820
+ try {
821
+ const page = await ctx.client.post("/extract-url", { url: attemptUrl }, 12e4);
822
+ reached = true;
823
+ const parsed = parseContacts(page.bodyHtml ?? page.bodyMarkdown ?? "");
824
+ allEmails.push(...parsed.emails);
825
+ parsed.socials.forEach((s) => allSocials.add(s));
826
+ if (parsed.emails.length) break;
827
+ } catch {
828
+ continue;
829
+ }
830
+ }
831
+ socials = [...allSocials].slice(0, 5);
832
+ email = pickEmail([...new Set(allEmails)], website);
833
+ if (email) {
834
+ domainMatch = hostRoot(website) && email.split("@")[1].includes(hostRoot(website)) ? "yes" : "NO-verify";
835
+ emailStatus = domainMatch === "yes" ? "ok" : "ok (off-domain \u2014 verify)";
836
+ } else if (!reached) {
837
+ emailStatus = "site unreachable";
838
+ } else if (socials.length) {
839
+ emailStatus = "form/social only";
840
+ }
841
+ }
842
+ return {
843
+ position: result.position,
844
+ business_name: result.name,
845
+ review_stars: result.rating ?? "",
846
+ review_count: reviewCount,
847
+ category: result.category ?? "",
848
+ address: result.address ?? "",
849
+ phone: result.phone ?? "",
850
+ website,
851
+ email,
852
+ email_domain_match: domainMatch,
853
+ email_status: emailStatus,
854
+ socials: socials.join(" ; "),
855
+ booking_url: bookingUrl,
856
+ place_url: result.placeUrl,
857
+ cid: result.cid ?? "",
858
+ source_location: input.location
859
+ };
860
+ });
861
+ await ctx.artifacts.writeCsv("Leads CSV", "exports/leads.csv", LEADS_CSV_HEADERS, rows);
862
+ await ctx.artifacts.writeJson("Leads evidence", "evidence.json", { input, search: { query: search.searchQuery, resultCount: search.resultCount }, rows });
863
+ const withEmail = rows.filter((r) => r.email).length;
864
+ const domainMatched = rows.filter((r) => r.email_domain_match === "yes").length;
865
+ const withReviewCount = rows.filter((r) => r.review_count).length;
866
+ const withSocials = rows.filter((r) => r.socials).length;
867
+ const withWebsite = rows.filter((r) => r.website).length;
868
+ const summary = `${rows.length} ${input.query} in ${input.location}: ${withEmail} emails (${domainMatched} domain-matched), ${withReviewCount} review counts, ${withSocials} with socials.`;
869
+ await ctx.artifacts.writeText("Summary", "summary.md", `# Get Leads
870
+
871
+ ${summary}
872
+ `);
873
+ await ctx.artifacts.writeText("Agent tasks", "tasks.md", [
874
+ "# Get Leads \u2014 next steps",
875
+ "",
876
+ "- [ ] Verify deliverability of the harvested emails before outreach (MX/SMTP check).",
877
+ '- [ ] For "form/social only" rows, use the website contact form or the captured social profile.',
878
+ '- [ ] Treat "off-domain \u2014 verify" emails with care; they may belong to a web developer or parent brand.',
879
+ "- [ ] Respect CAN-SPAM / GDPR \u2014 scraped contact data is not consent to email."
880
+ ].join("\n"));
881
+ const reportPath = await ctx.artifacts.writeHtml("HTML report", "report.html", renderWorkflowReport({
882
+ title: "Get Leads",
883
+ subtitle: `${input.query} \xB7 ${input.location}`,
884
+ summary,
885
+ warnings,
886
+ tables: [
887
+ {
888
+ title: "Leads",
889
+ columns: ["position", "business_name", "review_stars", "review_count", "phone", "website", "email", "email_status", "socials", "booking_url"],
890
+ rows
891
+ }
892
+ ]
893
+ }));
894
+ const status = errors.length ? "failed" : warnings.length ? "partial" : "succeeded";
895
+ const counts = {
896
+ businesses: rows.length,
897
+ withWebsite,
898
+ emails: withEmail,
899
+ domainMatchedEmails: domainMatched,
900
+ reviewCounts: withReviewCount,
901
+ withSocials
902
+ };
903
+ await ctx.artifacts.writeManifest(status, counts, warnings, errors);
904
+ return { title: "Get Leads", summary, status, counts, warnings, errors, reportPath };
905
+ }
906
+ };
907
+
908
+ // src/workflows/workflows/local-competitive-audit.ts
909
+ import { z as z4 } from "zod";
910
+ var LocalCompetitiveAuditInputSchema = z4.object({
911
+ query: z4.string().min(1),
912
+ state: z4.string().min(2).default("TN"),
913
+ minPopulation: z4.number().int().min(0).default(1e5),
914
+ maxCities: z4.number().int().min(1).max(100).default(25),
915
+ maxResultsPerCity: z4.number().int().min(1).max(50).default(20),
916
+ hydrateTop: z4.number().int().min(0).max(10).default(5),
917
+ maxReviews: z4.number().int().min(0).max(500).default(50),
918
+ concurrency: z4.number().int().min(1).max(5).default(5),
919
+ proxyMode: z4.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
920
+ returnPartial: z4.boolean().default(true)
921
+ });
922
+ async function mapLimit2(items, limit, fn) {
923
+ const out = new Array(items.length);
924
+ let next = 0;
925
+ async function worker() {
926
+ while (next < items.length) {
927
+ const index = next;
928
+ next += 1;
929
+ out[index] = await fn(items[index], index);
930
+ }
931
+ }
932
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
933
+ return out;
934
+ }
692
935
  function numberFrom(value) {
693
936
  if (value === null || value === void 0 || value === "") return null;
694
937
  const parsed = Number(String(value).replace(/[^\d.]/g, ""));
@@ -742,7 +985,7 @@ var localCompetitiveAuditWorkflowDefinition = {
742
985
  seen.add(key);
743
986
  return true;
744
987
  });
745
- const hydrated = await mapLimit(deduped, 3, async ({ city, result }, index) => {
988
+ const hydrated = await mapLimit2(deduped, 3, async ({ city, result }, index) => {
746
989
  try {
747
990
  const detail = await ctx.client.post("/maps/place", {
748
991
  businessName: result.name,
@@ -862,7 +1105,7 @@ ${summary}
862
1105
  };
863
1106
 
864
1107
  // src/workflows/workflows/comparison-briefs.ts
865
- import { z as z4 } from "zod";
1108
+ import { z as z5 } from "zod";
866
1109
 
867
1110
  // src/workflows/workflows/seo-workflow-utils.ts
868
1111
  var STOP_WORDS = /* @__PURE__ */ new Set([
@@ -1005,7 +1248,7 @@ function pageSummaryRow(page, source) {
1005
1248
  schema_types: (page.schemaTypes ?? []).join("; ")
1006
1249
  };
1007
1250
  }
1008
- async function mapLimit2(items, limit, fn) {
1251
+ async function mapLimit3(items, limit, fn) {
1009
1252
  const out = new Array(items.length);
1010
1253
  let next = 0;
1011
1254
  async function worker() {
@@ -1020,49 +1263,49 @@ async function mapLimit2(items, limit, fn) {
1020
1263
  }
1021
1264
 
1022
1265
  // src/workflows/workflows/comparison-briefs.ts
1023
- var ProxyModeSchema = z4.enum(["location", "configured", "none"]);
1024
- var MapComparisonInputSchema = z4.object({
1025
- query: z4.string().min(1),
1026
- location: z4.string().optional(),
1027
- state: z4.string().optional(),
1028
- minPopulation: z4.number().int().min(0).default(1e5),
1029
- maxCities: z4.number().int().min(1).max(100).default(5),
1030
- maxResultsPerCity: z4.number().int().min(1).max(50).default(20),
1031
- hydrateTop: z4.number().int().min(0).max(10).default(5),
1032
- maxReviews: z4.number().int().min(0).max(500).default(25),
1033
- concurrency: z4.number().int().min(1).max(5).default(5),
1266
+ var ProxyModeSchema = z5.enum(["location", "configured", "none"]);
1267
+ var MapComparisonInputSchema = z5.object({
1268
+ query: z5.string().min(1),
1269
+ location: z5.string().optional(),
1270
+ state: z5.string().optional(),
1271
+ minPopulation: z5.number().int().min(0).default(1e5),
1272
+ maxCities: z5.number().int().min(1).max(100).default(5),
1273
+ maxResultsPerCity: z5.number().int().min(1).max(50).default(20),
1274
+ hydrateTop: z5.number().int().min(0).max(10).default(5),
1275
+ maxReviews: z5.number().int().min(0).max(500).default(25),
1276
+ concurrency: z5.number().int().min(1).max(5).default(5),
1034
1277
  proxyMode: ProxyModeSchema.default(DEFAULT_MAPS_PROXY_MODE),
1035
- returnPartial: z4.boolean().default(true)
1278
+ returnPartial: z5.boolean().default(true)
1036
1279
  }).refine((input) => input.location || input.state, {
1037
1280
  message: "Either location or state is required for map-comparison"
1038
1281
  });
1039
- var SerpComparisonInputSchema = z4.object({
1040
- keyword: z4.string().min(1),
1041
- domain: z4.string().optional(),
1042
- url: z4.string().url().optional(),
1043
- location: z4.string().optional(),
1044
- maxResults: z4.number().int().min(1).max(20).default(10),
1045
- maxQuestions: z4.number().int().min(1).max(200).default(40),
1046
- extractTop: z4.number().int().min(0).max(10).default(5),
1047
- includePaa: z4.boolean().default(true),
1048
- includeAiOverview: z4.boolean().default(true),
1049
- returnPartial: z4.boolean().default(true)
1282
+ var SerpComparisonInputSchema = z5.object({
1283
+ keyword: z5.string().min(1),
1284
+ domain: z5.string().optional(),
1285
+ url: z5.string().url().optional(),
1286
+ location: z5.string().optional(),
1287
+ maxResults: z5.number().int().min(1).max(20).default(10),
1288
+ maxQuestions: z5.number().int().min(1).max(200).default(40),
1289
+ extractTop: z5.number().int().min(0).max(10).default(5),
1290
+ includePaa: z5.boolean().default(true),
1291
+ includeAiOverview: z5.boolean().default(true),
1292
+ returnPartial: z5.boolean().default(true)
1050
1293
  });
1051
- var PaaExpansionBriefInputSchema = z4.object({
1052
- keyword: z4.string().min(1),
1053
- location: z4.string().optional(),
1054
- maxQuestions: z4.number().int().min(1).max(300).default(80),
1055
- depth: z4.number().int().min(1).max(6).default(3),
1056
- returnPartial: z4.boolean().default(true)
1294
+ var PaaExpansionBriefInputSchema = z5.object({
1295
+ keyword: z5.string().min(1),
1296
+ location: z5.string().optional(),
1297
+ maxQuestions: z5.number().int().min(1).max(300).default(80),
1298
+ depth: z5.number().int().min(1).max(6).default(3),
1299
+ returnPartial: z5.boolean().default(true)
1057
1300
  });
1058
- var AiOverviewLanguageInputSchema = z4.object({
1059
- keyword: z4.string().min(1),
1060
- domain: z4.string().optional(),
1061
- url: z4.string().url().optional(),
1062
- location: z4.string().optional(),
1063
- maxQuestions: z4.number().int().min(1).max(200).default(40),
1064
- extractTop: z4.number().int().min(0).max(8).default(3),
1065
- returnPartial: z4.boolean().default(true)
1301
+ var AiOverviewLanguageInputSchema = z5.object({
1302
+ keyword: z5.string().min(1),
1303
+ domain: z5.string().optional(),
1304
+ url: z5.string().url().optional(),
1305
+ location: z5.string().optional(),
1306
+ maxQuestions: z5.number().int().min(1).max(200).default(40),
1307
+ extractTop: z5.number().int().min(0).max(8).default(3),
1308
+ returnPartial: z5.boolean().default(true)
1066
1309
  });
1067
1310
  function businessRowsFromMaps(location, query, results) {
1068
1311
  return results.map((result) => ({
@@ -1179,7 +1422,7 @@ function pageGapRows(targetPage, competitorPages) {
1179
1422
  return rows.slice(0, 150);
1180
1423
  }
1181
1424
  async function extractPages(ctx, sources, warnings, labelPrefix) {
1182
- return mapLimit2(sources.filter((source) => source.url), 2, async (source, index) => {
1425
+ return mapLimit3(sources.filter((source) => source.url), 2, async (source, index) => {
1183
1426
  try {
1184
1427
  const page = await ctx.client.post("/extract-url", { url: source.url }, 18e4);
1185
1428
  await ctx.artifacts.writeJson(`${labelPrefix} ${index + 1}`, `raw/extract-url/${labelPrefix.toLowerCase()}-${index + 1}.json`, page);
@@ -1228,7 +1471,7 @@ var mapComparisonWorkflowDefinition = {
1228
1471
  }
1229
1472
  const compareRows = comparisonRows(rows);
1230
1473
  const selected = compareRows.slice(0, input.hydrateTop * Math.max(1, input.location ? 1 : input.maxCities));
1231
- const hydrated = await mapLimit2(selected, 3, async (row, index) => {
1474
+ const hydrated = await mapLimit3(selected, 3, async (row, index) => {
1232
1475
  try {
1233
1476
  const detail = await ctx.client.post("/maps/place", {
1234
1477
  businessName: row.business_name,
@@ -1563,6 +1806,7 @@ var aiOverviewLanguageWorkflowDefinition = {
1563
1806
  // src/workflows/registry.ts
1564
1807
  var DEFINITIONS = [
1565
1808
  directoryWorkflowDefinition,
1809
+ getLeadsWorkflowDefinition,
1566
1810
  agentPacketWorkflowDefinition,
1567
1811
  localCompetitiveAuditWorkflowDefinition,
1568
1812
  mapComparisonWorkflowDefinition,
@@ -1669,6 +1913,7 @@ async function runWorkflowStep(id, rawInput, opts) {
1669
1913
  export {
1670
1914
  csvRecords,
1671
1915
  rowsToCsv,
1916
+ slugify,
1672
1917
  workflowOutputBaseDir,
1673
1918
  listWorkflowReports,
1674
1919
  findWorkflowReport,
@@ -1680,4 +1925,4 @@ export {
1680
1925
  runWorkflow,
1681
1926
  runWorkflowStep
1682
1927
  };
1683
- //# sourceMappingURL=chunk-H2R232HK.js.map
1928
+ //# sourceMappingURL=chunk-NNEIXK5L.js.map