mcp-scraper 0.2.16 → 0.2.18

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 (32) hide show
  1. package/README.md +12 -5
  2. package/dist/bin/api-server.cjs +479 -7
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +1 -1
  5. package/dist/bin/browser-agent-stdio-server.cjs +1 -1
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +2 -2
  8. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  9. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-cli.js +1 -1
  11. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +466 -1
  12. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-scraper-combined-stdio-server.js +3 -3
  14. package/dist/bin/mcp-scraper-install.cjs +9 -7
  15. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  16. package/dist/bin/mcp-scraper-install.js +9 -7
  17. package/dist/bin/mcp-scraper-install.js.map +1 -1
  18. package/dist/bin/mcp-stdio-server.cjs +466 -1
  19. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  20. package/dist/bin/mcp-stdio-server.js +2 -2
  21. package/dist/chunk-M5SAUO4K.js +7 -0
  22. package/dist/chunk-M5SAUO4K.js.map +1 -0
  23. package/dist/{chunk-ATJAINML.js → chunk-P5PLQU3H.js} +2 -2
  24. package/dist/{chunk-FMC3V54I.js → chunk-RJMCASQH.js} +469 -2
  25. package/dist/chunk-RJMCASQH.js.map +1 -0
  26. package/dist/{server-KSEQLZNP.js → server-VVY5K44O.js} +3 -3
  27. package/package.json +1 -1
  28. package/dist/chunk-FMC3V54I.js.map +0 -1
  29. package/dist/chunk-KE4JZDLV.js +0 -7
  30. package/dist/chunk-KE4JZDLV.js.map +0 -1
  31. /package/dist/{chunk-ATJAINML.js.map → chunk-P5PLQU3H.js.map} +0 -0
  32. /package/dist/{server-KSEQLZNP.js.map → server-VVY5K44O.js.map} +0 -0
package/README.md CHANGED
@@ -22,7 +22,7 @@ Run this when you want the designed terminal install experience:
22
22
  npx -y -p mcp-scraper@latest mcp-scraper-install
23
23
  ```
24
24
 
25
- It prints the MCP Scraper banner, loaded tool groups, Desktop Extension download, Claude Code command, and Codex config. The MCP server commands stay silent so stdout remains valid JSON-RPC for MCP clients.
25
+ It prints the MCP Scraper banner, loaded tool groups, Desktop Extension download, Claude Code command, and Codex config. This is the command to use when you want the ASCII terminal card. The MCP server commands, including `mcp-scraper-combined`, stay silent so stdout remains valid JSON-RPC for MCP clients.
26
26
 
27
27
  ### Human CLI
28
28
 
@@ -81,7 +81,7 @@ Build the branded one-click bundle:
81
81
  npm run build:mcpb
82
82
  ```
83
83
 
84
- The generated bundle is written to `build/mcpb/mcp-scraper-<version>.mcpb` and copied to `public/downloads/` for the hosted download. The current public bundle is `https://mcpscraper.dev/downloads/mcp-scraper.mcpb` (`0.2.16`, SHA-256 `a581f0ddb12d3cebd03d90dfb657cc9127a1f7ef995fb0934d5428faf8aec58b`). Install it by opening or dragging it into Claude Desktop. Claude displays the `MCP Scraper` install card, icon, and API-key configuration field from the bundle manifest.
84
+ The generated bundle is written to `build/mcpb/mcp-scraper-<version>.mcpb` and copied to `public/downloads/` for the hosted download. The current public bundle is `https://mcpscraper.dev/downloads/mcp-scraper.mcpb` (`0.2.18`, SHA-256 `3b94a39bf23712005a1a04accdefcf3ee24978995033b92e4488390d47350ea0`). Install it by opening or dragging it into Claude Desktop. Claude displays the `MCP Scraper` install card, icon, and API-key configuration field from the bundle manifest.
85
85
 
86
86
  The MCPB install exposes the same web-intelligence tools as `mcp-scraper` plus all `browser_*` tools from `browser-agent` through one server.
87
87
 
@@ -159,6 +159,11 @@ env = { MCP_SCRAPER_API_KEY = "sk_live_your_key" }
159
159
  - `maps_search` — search Google Maps for multiple business/profile candidates. Use for GMB/GBP prospect lists, competitors, categories, and anything needing more than the Google 3-pack. In default `proxyMode: "location"`, retryable failures rotate to a new residential proxy and new browser session for up to 5 attempts. `maxResults` defaults to 10 and is capped at 50.
160
160
  - `maps_place_intel` — hydrate one known/named Google Maps business with profile details and optional reviews. Use after `maps_search` when a selected candidate needs full details.
161
161
  - `directory_workflow` — build city-by-city directory/prospecting datasets from Census place selection plus Google Maps searches. Use it for requests like "all cities over 100k population in Tennessee, then get 20 roofers from Maps." In default `proxyMode: "location"`, each city search rotates retryable failures to a new residential proxy and new browser session for up to 5 attempts. The saved CSV includes `source_location`, `result_position`, `business_name`, `review_stars`, `review_count`, `category`, `address`, `phone`, `hours_status`, `website_url`, `directions_url`, `place_url`, `cid`, `cid_decimal`, Census population, and ZIP groups.
162
+ - `workflow_list` — list higher-level workflow IDs plus AI-facing recipes for market analysis, ICP research, forum/review acquisition, brand design briefings, CRO audits, positioning briefs, content gaps, and AI search visibility audits.
163
+ - `workflow_suggest` — route a high-level business goal to the right workflow/tool chain before spending credits.
164
+ - `workflow_run` — run hosted workflows such as `agent-packet`, `local-competitive-audit`, `map-comparison`, `serp-comparison`, `paa-expansion-brief`, and `ai-overview-language`; returns run metadata, summary, and artifact IDs.
165
+ - `workflow_status` — reopen a workflow run and list its current status and artifacts.
166
+ - `workflow_artifact_read` — pull generated workflow artifacts such as `evidence.json`, CSVs, Markdown briefs, and reports back into MCP context.
162
167
  - `rank_tracker_blueprint` — generate a database schema, cron/heartbeat plan, ingestion workflow, metrics list, and implementation prompt for building rank trackers. It has modes for Maps rankings via `directory_workflow`/`maps_search`, organic rankings via `search_serp`, AI Overview citation tracking, and PAA source presence tracking. This is a local planning tool and does not spend credits.
163
168
  - `credits_info`
164
169
 
@@ -190,9 +195,9 @@ For Google Maps tools (`maps_search` and `directory_workflow`), keep `proxyMode`
190
195
 
191
196
  The MCPB bundle and `mcp-scraper-combined` expose both sections through one local MCP server. The split `mcp-scraper` entrypoint exposes only the web-intelligence tools, and the split `browser-agent` entrypoint exposes only the browser-agent tools.
192
197
 
193
- Chaining and planning tools (`maps_search`, `directory_workflow`, `rank_tracker_blueprint`, `map_site_urls`, `youtube_harvest`, `facebook_ad_search`, `facebook_page_intel`, `facebook_video_transcribe`) advertise an `outputSchema` and return `structuredContent` with the IDs, URLs, CSV paths, transcripts, or blueprint fields needed by the next step. All tools carry MCP annotations (`readOnlyHint: true`; live-web tools use `openWorldHint: true`, while local planning and credit tools are closed-world/idempotent).
198
+ Chaining and planning tools (`maps_search`, `directory_workflow`, `workflow_list`, `workflow_suggest`, `workflow_run`, `workflow_status`, `workflow_artifact_read`, `rank_tracker_blueprint`, `map_site_urls`, `youtube_harvest`, `facebook_ad_search`, `facebook_page_intel`, `facebook_video_transcribe`) return `structuredContent` with the IDs, URLs, CSV paths, transcripts, artifacts, recipe fields, or blueprint fields needed by the next step. All tools carry MCP annotations (`readOnlyHint: true`; live-web tools use `openWorldHint: true`, while local planning and credit tools are closed-world/idempotent).
194
199
 
195
- The hosted MCP endpoint at `https://mcpscraper.dev/mcp` exposes the 16 web-intelligence tools plus `capture_serp_snapshot` and `capture_serp_page_snapshots` (18 total). Browser-agent tools are local stdio tools backed by the REST API under `https://mcpscraper.dev/agent/*`.
200
+ The hosted MCP endpoint at `https://mcpscraper.dev/mcp` exposes the 21 web-intelligence/workflow tools plus `capture_serp_snapshot` and `capture_serp_page_snapshots` (23 total). Browser-agent tools are local stdio tools backed by the REST API under `https://mcpscraper.dev/agent/*`.
196
201
 
197
202
  ## Resources
198
203
 
@@ -218,6 +223,8 @@ Recommended config for update-friendly installs:
218
223
  npx -y -p mcp-scraper@latest mcp-scraper-combined
219
224
  ```
220
225
 
226
+ This is an MCP stdio server command, not a human-facing terminal UI. If you run it directly, it appears to hang because it is waiting for JSON-RPC input from the MCP client. Use `npx -y -p mcp-scraper@latest mcp-scraper-install` when you want the visible installer and ASCII card.
227
+
221
228
  Split-server config:
222
229
 
223
230
  ```bash
@@ -236,7 +243,7 @@ Users who do not update can keep using the tools their local package already adv
236
243
 
237
244
  ## Branded One-Click Installs
238
245
 
239
- Raw `npx` MCP server installs are command/config based. They do not provide a reliable user-facing install card, logo, or setup screen inside MCP clients. Use `mcp-scraper-install` for terminal onboarding text. Do not print marketing text to stdout from an MCP server; stdout is reserved for JSON-RPC protocol messages.
246
+ Raw `npx` MCP server installs are command/config based. They do not provide a reliable user-facing install card, logo, or setup screen inside MCP clients. Use `mcp-scraper-install` for terminal onboarding text and the ASCII card. Do not print marketing text to stdout from an MCP server; stdout is reserved for JSON-RPC protocol messages.
240
247
 
241
248
  For a branded Claude Desktop install, package MCP Scraper as an MCPB Desktop Extension. The repository now builds one combined MCPB bundle with a generated icon, `manifest.json`, bundled runtime dependencies, and `user_config` fields for API-key setup, API URL, and output folder.
242
249
 
@@ -13814,6 +13814,127 @@ var init_maps_routes = __esm({
13814
13814
  }
13815
13815
  });
13816
13816
 
13817
+ // src/mcp/workflow-catalog.ts
13818
+ function normalize(value) {
13819
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
13820
+ }
13821
+ function suggestWorkflowRecipes(goal, limit = 3) {
13822
+ const normalized = normalize(goal);
13823
+ const scored = WORKFLOW_RECIPES.map((recipe) => {
13824
+ const haystack = normalize([
13825
+ recipe.id,
13826
+ recipe.title,
13827
+ recipe.description,
13828
+ recipe.requiredInputs.join(" "),
13829
+ recipe.optionalInputs.join(" "),
13830
+ recipe.produces.join(" ")
13831
+ ].join(" "));
13832
+ let score = 0;
13833
+ for (const token of normalized.split(/\s+/).filter(Boolean)) {
13834
+ if (haystack.includes(token)) score += 1;
13835
+ }
13836
+ if (haystack.includes(normalized)) score += 5;
13837
+ return { recipe, score };
13838
+ });
13839
+ return scored.sort((a, b) => b.score - a.score || a.recipe.title.localeCompare(b.recipe.title)).slice(0, Math.max(1, limit)).map((item) => item.recipe);
13840
+ }
13841
+ var WORKFLOW_RECIPES;
13842
+ var init_workflow_catalog = __esm({
13843
+ "src/mcp/workflow-catalog.ts"() {
13844
+ "use strict";
13845
+ WORKFLOW_RECIPES = [
13846
+ {
13847
+ id: "market_analysis",
13848
+ title: "Market Analysis",
13849
+ description: "Compare a niche across a city, state, or market set using Google Maps competitors, SERP evidence, review counts, categories, and opportunity signals.",
13850
+ primaryWorkflowId: "local-competitive-audit",
13851
+ recommendedTools: ["workflow_run", "directory_workflow", "maps_search", "maps_place_intel", "search_serp", "harvest_paa"],
13852
+ requiredInputs: ["query", "state or location"],
13853
+ optionalInputs: ["domain", "minPopulation", "maxCities", "maxResultsPerCity", "hydrateTop", "maxReviews"],
13854
+ produces: ["city summary", "competitor CSV", "review insight CSV", "market difficulty/opportunity notes", "HTML report"],
13855
+ runHint: "Use workflow_run with workflowId local-competitive-audit for state-wide market batches, or map-comparison for one city/location comparison."
13856
+ },
13857
+ {
13858
+ id: "icp_research",
13859
+ title: "ICP Research",
13860
+ description: "Build an evidence packet for ideal-customer-profile research from real search demand, PAA language, competing pages, AI Overview citations, and source domains.",
13861
+ primaryWorkflowId: "agent-packet",
13862
+ recommendedTools: ["workflow_run", "harvest_paa", "search_serp", "extract_url", "youtube_harvest", "facebook_ad_search"],
13863
+ requiredInputs: ["keyword or audience problem"],
13864
+ optionalInputs: ["domain", "location", "maxQuestions"],
13865
+ produces: ["evidence JSON", "sources CSV", "competitor CSV", "brief markdown", "agent task list"],
13866
+ runHint: "Use workflow_run with workflowId agent-packet. Treat keyword as the buyer problem, job-to-be-done, niche, or service category."
13867
+ },
13868
+ {
13869
+ id: "forum_review_research",
13870
+ title: "Forum And Review Research",
13871
+ description: "Acquire customer language from Google PAA, forum/Perspectives SERP surfaces, Google Maps reviews, review topics, Facebook ads, and video transcripts.",
13872
+ primaryWorkflowId: "local-competitive-audit",
13873
+ recommendedTools: ["workflow_run", "harvest_paa", "maps_search", "maps_place_intel", "facebook_page_intel", "facebook_video_transcribe", "youtube_transcribe"],
13874
+ requiredInputs: ["query", "state or location"],
13875
+ optionalInputs: ["maxReviews", "maxQuestions", "competitors", "facebookUrl", "youtubeVideoId"],
13876
+ produces: ["review insight CSV", "PAA/source language", "competitor review topics", "transcripts where supplied", "pain/theme summary"],
13877
+ runHint: "Use local-competitive-audit for review acquisition, then harvest_paa for forum/Perspectives language and transcript tools for supplied videos."
13878
+ },
13879
+ {
13880
+ id: "brand_design_brief",
13881
+ title: "Brand Design Brief",
13882
+ description: "Create a design briefing grounded in a brand site, competitor pages, SERP language, audience pains, visual assets, colors, fonts, screenshots, and positioning evidence.",
13883
+ primaryWorkflowId: "serp-comparison",
13884
+ recommendedTools: ["workflow_run", "extract_url", "extract_site", "capture_serp_page_snapshots", "search_serp", "harvest_paa", "browser_open"],
13885
+ requiredInputs: ["url or domain", "keyword or market category"],
13886
+ optionalInputs: ["competitorUrls", "location", "extractTop"],
13887
+ produces: ["page comparison CSV", "content gaps", "branding/asset evidence", "SERP positioning evidence", "designer-ready brief"],
13888
+ runHint: "Use workflow_run with workflowId serp-comparison for market/page evidence, then extract_url with extractBranding:true for brand assets and colors."
13889
+ },
13890
+ {
13891
+ id: "cro_audit",
13892
+ title: "CRO Audit",
13893
+ description: "Audit conversion clarity using page extraction, screenshots, browser DOM inspection, competitor SERP/page comparisons, forms/buttons, proof, offer, trust, and friction evidence.",
13894
+ primaryWorkflowId: "serp-comparison",
13895
+ recommendedTools: ["workflow_run", "extract_url", "extract_site", "capture_serp_page_snapshots", "browser_open", "browser_screenshot", "browser_locate"],
13896
+ requiredInputs: ["url"],
13897
+ optionalInputs: ["keyword", "domain", "location", "competitorUrls"],
13898
+ produces: ["page extraction", "SERP/page comparison", "screenshot evidence", "friction checklist", "CRO recommendations"],
13899
+ runHint: "Use workflow_run with workflowId serp-comparison when a keyword/domain is known; use extract_url and browser tools for page-level CRO evidence."
13900
+ },
13901
+ {
13902
+ id: "competitive_positioning_brief",
13903
+ title: "Competitive Positioning Brief",
13904
+ description: "Compare competitors across SERP, PAA, AI Overview citations, page headings, Facebook ads, YouTube angles, Maps proof, and website claims.",
13905
+ primaryWorkflowId: "serp-comparison",
13906
+ recommendedTools: ["workflow_run", "facebook_ad_search", "facebook_page_intel", "youtube_harvest", "youtube_transcribe", "maps_search", "extract_url"],
13907
+ requiredInputs: ["keyword or query", "domain or url"],
13908
+ optionalInputs: ["location", "extractTop", "competitorUrls", "brandNames"],
13909
+ produces: ["SERP comparison", "content gap CSV", "AI Overview citation table", "competitor message map", "positioning brief"],
13910
+ runHint: "Use workflow_run with workflowId serp-comparison, then enrich with Facebook, YouTube, and Maps tools when the user asks for campaign or offer positioning."
13911
+ },
13912
+ {
13913
+ id: "content_gap_brief",
13914
+ title: "Content Gap Brief",
13915
+ description: "Turn live SERP, page extraction, PAA questions, AI Overview citations, and source-domain evidence into a writer-ready content brief.",
13916
+ primaryWorkflowId: "serp-comparison",
13917
+ recommendedTools: ["workflow_run", "paa-expansion-brief", "harvest_paa", "extract_url"],
13918
+ requiredInputs: ["keyword"],
13919
+ optionalInputs: ["domain", "url", "location", "maxQuestions", "extractTop"],
13920
+ produces: ["content gaps", "page comparison CSV", "PAA questions CSV", "section map", "writer brief"],
13921
+ runHint: "Use workflow_run with workflowId serp-comparison for competitor gaps or paa-expansion-brief when the user mainly wants a section map from PAA data."
13922
+ },
13923
+ {
13924
+ id: "ai_search_visibility_audit",
13925
+ title: "AI Search Visibility Audit",
13926
+ description: "Audit whether a brand/domain appears in AI Overview citations, PAA sources, organic results, local pack, and source pages, then produce language guidance.",
13927
+ primaryWorkflowId: "ai-overview-language",
13928
+ recommendedTools: ["workflow_run", "search_serp", "harvest_paa", "extract_url", "capture_serp_snapshot"],
13929
+ requiredInputs: ["keyword", "domain or url"],
13930
+ optionalInputs: ["location", "maxQuestions", "extractTop"],
13931
+ produces: ["AI Overview citations CSV", "claim patterns", "language guidance", "citation hooks", "visibility gaps"],
13932
+ runHint: "Use workflow_run with workflowId ai-overview-language. Use serp-comparison when the user also wants ranking-page gaps."
13933
+ }
13934
+ ];
13935
+ }
13936
+ });
13937
+
13817
13938
  // src/mcp/mcp-response-formatter.ts
13818
13939
  function configureReportSaving(enabled) {
13819
13940
  reportSavingEnabled = enabled;
@@ -13867,6 +13988,14 @@ function oneBlock(content) {
13867
13988
  \u{1F4C4} Saved: \`${filePath}\`` : content;
13868
13989
  return { content: [{ type: "text", text }] };
13869
13990
  }
13991
+ function workflowRecipeTable(recipes) {
13992
+ if (!recipes.length) return "";
13993
+ return [
13994
+ "| Recipe | Best workflow | What it produces |",
13995
+ "|---|---|---|",
13996
+ ...recipes.map((recipe) => `| ${cell(recipe.title)} | ${recipe.primaryWorkflowId ? `\`${recipe.primaryWorkflowId}\`` : "tool chain"} | ${cell(recipe.produces.slice(0, 4).join(", "))} |`)
13997
+ ].join("\n");
13998
+ }
13870
13999
  function formatStructuredError(body, fallback) {
13871
14000
  if (body.error === "insufficient_balance") {
13872
14001
  return `Insufficient credits. Balance: ${body.balance_credits} credits. This call requires ${body.required_credits} credits. Top up at ${body.topup_url}`;
@@ -14439,6 +14568,163 @@ function normalizeMapsAttempts(value) {
14439
14568
  observedRegion: attempt.observedRegion ?? attempt.observed_region ?? null
14440
14569
  }));
14441
14570
  }
14571
+ function workflowArtifactsFrom(run) {
14572
+ return Array.isArray(run?.artifacts) ? run.artifacts : [];
14573
+ }
14574
+ function workflowArtifactRows(artifacts) {
14575
+ if (!artifacts.length) return "";
14576
+ return [
14577
+ "| Artifact | Type | ID | Rows/Bytes |",
14578
+ "|---|---|---|---|",
14579
+ ...artifacts.map((artifact) => {
14580
+ const rows = artifact.rows_count ?? artifact.rows ?? "";
14581
+ const bytes = artifact.bytes ?? "";
14582
+ const size = [rows ? `${rows} rows` : "", bytes ? `${bytes} bytes` : ""].filter(Boolean).join(" / ");
14583
+ return `| ${cell(String(artifact.label ?? artifact.path ?? "artifact"))} | ${cell(String(artifact.kind ?? artifact.content_type ?? "file"))} | \`${String(artifact.id ?? "")}\` | ${cell(size || "\u2014")} |`;
14584
+ })
14585
+ ].join("\n");
14586
+ }
14587
+ function formatWorkflowList(raw, input) {
14588
+ const parsed = parseData(raw);
14589
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
14590
+ const workflows = Array.isArray(parsed.data.workflows) ? parsed.data.workflows : [];
14591
+ const workflowRows = [
14592
+ "| Workflow ID | Title | Use |",
14593
+ "|---|---|---|",
14594
+ ...workflows.map((workflow) => `| \`${String(workflow.id ?? "")}\` | ${cell(String(workflow.title ?? ""))} | ${cell(String(workflow.description ?? ""))} |`)
14595
+ ].join("\n");
14596
+ const recipes = input.includeRecipes === false ? [] : WORKFLOW_RECIPES;
14597
+ const full = [
14598
+ "# MCP Scraper Workflows",
14599
+ "Use `workflow_suggest` when the user describes a high-level job. Use `workflow_run` when the workflow id and input are known. Use `workflow_status` and `workflow_artifact_read` to inspect completed runs and pull evidence back into context.",
14600
+ workflows.length ? `
14601
+ ## Runnable Workflows
14602
+ ${workflowRows}` : "",
14603
+ recipes.length ? `
14604
+ ## High-Level Recipes
14605
+ ${workflowRecipeTable(recipes)}` : "",
14606
+ recipes.length ? "\nThese recipes cover market analysis, ICP research, forum and review acquisition, brand design briefings, CRO audits, competitive positioning, content gaps, and AI search visibility audits." : ""
14607
+ ].filter(Boolean).join("\n");
14608
+ return {
14609
+ ...oneBlock(full),
14610
+ structuredContent: {
14611
+ workflows: workflows.map((workflow) => ({
14612
+ id: String(workflow.id ?? ""),
14613
+ title: String(workflow.title ?? ""),
14614
+ description: String(workflow.description ?? "")
14615
+ })),
14616
+ recipes
14617
+ }
14618
+ };
14619
+ }
14620
+ function formatWorkflowSuggest(input) {
14621
+ const suggestions = suggestWorkflowRecipes(input.goal, input.maxSuggestions ?? 3);
14622
+ const full = [
14623
+ `# Workflow Suggestions`,
14624
+ `**Goal:** ${input.goal}`,
14625
+ "",
14626
+ workflowRecipeTable(suggestions),
14627
+ "",
14628
+ "## How To Execute",
14629
+ "1. Pick the closest recipe.",
14630
+ "2. If it has a `primaryWorkflowId`, call `workflow_run` with that id and the required inputs.",
14631
+ "3. After the run completes, use `workflow_artifact_read` on the most relevant CSV/JSON/Markdown artifacts before writing the final answer."
14632
+ ].join("\n");
14633
+ return {
14634
+ ...oneBlock(full),
14635
+ structuredContent: {
14636
+ goal: input.goal,
14637
+ suggestions
14638
+ }
14639
+ };
14640
+ }
14641
+ function formatWorkflowRun(raw, input) {
14642
+ const parsed = parseData(raw);
14643
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
14644
+ const run = parsed.data.run;
14645
+ const summary = parsed.data.summary;
14646
+ const artifacts = workflowArtifactsFrom(run);
14647
+ const runId = String(run?.id ?? "");
14648
+ const status = String(run?.status ?? summary?.status ?? "unknown");
14649
+ const full = [
14650
+ `# Workflow Run: ${input.workflowId}`,
14651
+ `**Run ID:** \`${runId || "unknown"}\``,
14652
+ `**Status:** ${status}`,
14653
+ summary?.title ? `**Title:** ${summary.title}` : "",
14654
+ summary?.summary ? `
14655
+ ## Summary
14656
+ ${summary.summary}` : "",
14657
+ artifacts.length ? `
14658
+ ## Artifacts
14659
+ ${workflowArtifactRows(artifacts)}` : "",
14660
+ artifacts.length ? "\nUse `workflow_artifact_read` with the run id and artifact id to pull CSV, JSON, Markdown, or report content into context." : ""
14661
+ ].filter(Boolean).join("\n");
14662
+ return {
14663
+ ...oneBlock(full),
14664
+ structuredContent: {
14665
+ workflowId: input.workflowId,
14666
+ input: input.input ?? {},
14667
+ run,
14668
+ summary,
14669
+ artifacts
14670
+ }
14671
+ };
14672
+ }
14673
+ function formatWorkflowStatus(raw, input) {
14674
+ const parsed = parseData(raw);
14675
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
14676
+ const run = parsed.data.run;
14677
+ const artifacts = workflowArtifactsFrom(run);
14678
+ const full = [
14679
+ `# Workflow Status`,
14680
+ `**Run ID:** \`${input.runId}\``,
14681
+ `**Workflow:** ${run?.workflow_id ?? "unknown"}`,
14682
+ `**Status:** ${run?.status ?? "unknown"}`,
14683
+ run?.error_message ? `
14684
+ ## Error
14685
+ ${run.error_message}` : "",
14686
+ artifacts.length ? `
14687
+ ## Artifacts
14688
+ ${workflowArtifactRows(artifacts)}` : ""
14689
+ ].filter(Boolean).join("\n");
14690
+ return {
14691
+ ...oneBlock(full),
14692
+ structuredContent: {
14693
+ run,
14694
+ artifacts
14695
+ }
14696
+ };
14697
+ }
14698
+ function formatWorkflowArtifactRead(raw, input) {
14699
+ const parsed = parseData(raw);
14700
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
14701
+ const text = typeof parsed.data.text === "string" ? parsed.data.text : "";
14702
+ const contentType = String(parsed.data.contentType ?? "text/plain");
14703
+ const bytes = Number(parsed.data.bytes ?? 0);
14704
+ const truncated = parsed.data.truncated === true;
14705
+ const full = [
14706
+ `# Workflow Artifact`,
14707
+ `**Run ID:** \`${input.runId}\``,
14708
+ `**Artifact ID:** \`${input.artifactId}\``,
14709
+ `**Content-Type:** ${contentType}`,
14710
+ `**Bytes:** ${bytes}${truncated ? ` (truncated to ${input.maxBytes ?? 2e5})` : ""}`,
14711
+ "",
14712
+ "```",
14713
+ text,
14714
+ "```"
14715
+ ].join("\n");
14716
+ return {
14717
+ ...oneBlock(full),
14718
+ structuredContent: {
14719
+ runId: input.runId,
14720
+ artifactId: input.artifactId,
14721
+ contentType,
14722
+ bytes,
14723
+ truncated,
14724
+ text
14725
+ }
14726
+ };
14727
+ }
14442
14728
  function formatCreditsInfo(raw, input) {
14443
14729
  const parsed = parseData(raw);
14444
14730
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
@@ -14871,6 +15157,7 @@ var init_mcp_response_formatter = __esm({
14871
15157
  import_node_os3 = require("os");
14872
15158
  import_node_path5 = require("path");
14873
15159
  init_errors();
15160
+ init_workflow_catalog();
14874
15161
  reportSavingEnabled = true;
14875
15162
  }
14876
15163
  });
@@ -20146,12 +20433,12 @@ var PACKAGE_VERSION;
20146
20433
  var init_version = __esm({
20147
20434
  "src/version.ts"() {
20148
20435
  "use strict";
20149
- PACKAGE_VERSION = "0.2.16";
20436
+ PACKAGE_VERSION = "0.2.18";
20150
20437
  }
20151
20438
  });
20152
20439
 
20153
20440
  // src/mcp/mcp-tool-schemas.ts
20154
- var import_zod26, HarvestPaaInputSchema, ExtractUrlInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, MapsPlaceIntelInputSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, ExtractSiteOutputSchema, MapsPlaceIntelOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, FacebookPageIntelOutputSchema, FacebookVideoTranscribeOutputSchema, CreditsInfoInputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema;
20441
+ var import_zod26, HarvestPaaInputSchema, ExtractUrlInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, MapsPlaceIntelInputSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, ExtractSiteOutputSchema, MapsPlaceIntelOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, FacebookPageIntelOutputSchema, FacebookVideoTranscribeOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema;
20155
20442
  var init_mcp_tool_schemas = __esm({
20156
20443
  "src/mcp/mcp-tool-schemas.ts"() {
20157
20444
  "use strict";
@@ -20599,6 +20886,85 @@ var init_mcp_tool_schemas = __esm({
20599
20886
  item: import_zod26.z.string().optional().describe('Optional tool, action, or feature to look up, e.g. "maps reviews", "extract_url", "YouTube transcription", or "concurrency"'),
20600
20887
  includeLedger: import_zod26.z.boolean().default(false).describe("Whether to include recent credit ledger entries")
20601
20888
  };
20889
+ WorkflowIdSchema2 = import_zod26.z.enum([
20890
+ "directory",
20891
+ "agent-packet",
20892
+ "local-competitive-audit",
20893
+ "map-comparison",
20894
+ "serp-comparison",
20895
+ "paa-expansion-brief",
20896
+ "ai-overview-language"
20897
+ ]);
20898
+ WorkflowListInputSchema = {
20899
+ includeRecipes: import_zod26.z.boolean().default(true).describe("Include high-level AI-facing recipes such as market analysis, ICP research, forum/review research, brand design briefings, CRO audits, positioning briefs, content gaps, and AI search visibility audits.")
20900
+ };
20901
+ WorkflowSuggestInputSchema = {
20902
+ goal: import_zod26.z.string().min(1).describe('The user goal or job to route, e.g. "market analysis for roofers in Tennessee", "ICP research for med spas", "CRO audit for this URL", or "brand design briefing".'),
20903
+ query: import_zod26.z.string().optional().describe("Business category, niche, or Maps query when known."),
20904
+ keyword: import_zod26.z.string().optional().describe("Search keyword, audience problem, or content topic when known."),
20905
+ domain: import_zod26.z.string().optional().describe("Target domain or brand domain when known."),
20906
+ url: import_zod26.z.string().url().optional().describe("Target URL when the workflow should inspect a specific page."),
20907
+ location: import_zod26.z.string().optional().describe("City/region/country for localized research, e.g. Denver, CO."),
20908
+ state: import_zod26.z.string().optional().describe("US state abbreviation or name for state-wide market research."),
20909
+ maxSuggestions: import_zod26.z.number().int().min(1).max(8).default(3).describe("Number of matching workflow recipes to return.")
20910
+ };
20911
+ WorkflowRunInputSchema = {
20912
+ workflowId: WorkflowIdSchema2.describe("Workflow to run. Use workflow_list or workflow_suggest first when unsure."),
20913
+ input: import_zod26.z.record(import_zod26.z.unknown()).default({}).describe("Workflow-specific input object. Examples: agent-packet uses {keyword, domain?, location?, maxQuestions?}; local-competitive-audit uses {query, state, minPopulation?, maxCities?, maxResultsPerCity?, hydrateTop?, maxReviews?}; serp-comparison uses {keyword, domain?, url?, location?, extractTop?}."),
20914
+ webhookUrl: import_zod26.z.string().url().optional().describe("Optional HTTPS webhook to receive the completed hosted workflow run event.")
20915
+ };
20916
+ WorkflowStatusInputSchema = {
20917
+ runId: import_zod26.z.string().min(1).describe("Workflow run id returned by workflow_run.")
20918
+ };
20919
+ WorkflowArtifactReadInputSchema = {
20920
+ runId: import_zod26.z.string().min(1).describe("Workflow run id returned by workflow_run or workflow_status."),
20921
+ artifactId: import_zod26.z.string().min(1).describe("Artifact id from the run artifact list."),
20922
+ maxBytes: import_zod26.z.number().int().min(1e3).max(1e6).default(2e5).describe("Maximum bytes of artifact text to return inline. Use lower values for large CSV/JSON artifacts; call again with the downloadUrl if needed outside MCP.")
20923
+ };
20924
+ WorkflowRecipeOutput = import_zod26.z.object({
20925
+ id: import_zod26.z.string(),
20926
+ title: import_zod26.z.string(),
20927
+ description: import_zod26.z.string(),
20928
+ primaryWorkflowId: import_zod26.z.string().nullable(),
20929
+ recommendedTools: import_zod26.z.array(import_zod26.z.string()),
20930
+ requiredInputs: import_zod26.z.array(import_zod26.z.string()),
20931
+ optionalInputs: import_zod26.z.array(import_zod26.z.string()),
20932
+ produces: import_zod26.z.array(import_zod26.z.string()),
20933
+ runHint: import_zod26.z.string()
20934
+ });
20935
+ WorkflowDefinitionOutput = import_zod26.z.object({
20936
+ id: import_zod26.z.string(),
20937
+ title: import_zod26.z.string(),
20938
+ description: import_zod26.z.string()
20939
+ });
20940
+ WorkflowArtifactOutput = import_zod26.z.record(import_zod26.z.unknown());
20941
+ WorkflowListOutputSchema = {
20942
+ workflows: import_zod26.z.array(WorkflowDefinitionOutput),
20943
+ recipes: import_zod26.z.array(WorkflowRecipeOutput)
20944
+ };
20945
+ WorkflowSuggestOutputSchema = {
20946
+ goal: import_zod26.z.string(),
20947
+ suggestions: import_zod26.z.array(WorkflowRecipeOutput)
20948
+ };
20949
+ WorkflowRunOutputSchema = {
20950
+ workflowId: import_zod26.z.string(),
20951
+ input: import_zod26.z.record(import_zod26.z.unknown()),
20952
+ run: import_zod26.z.record(import_zod26.z.unknown()).optional(),
20953
+ summary: import_zod26.z.record(import_zod26.z.unknown()).optional(),
20954
+ artifacts: import_zod26.z.array(WorkflowArtifactOutput)
20955
+ };
20956
+ WorkflowStatusOutputSchema = {
20957
+ run: import_zod26.z.record(import_zod26.z.unknown()).optional(),
20958
+ artifacts: import_zod26.z.array(WorkflowArtifactOutput)
20959
+ };
20960
+ WorkflowArtifactReadOutputSchema = {
20961
+ runId: import_zod26.z.string(),
20962
+ artifactId: import_zod26.z.string(),
20963
+ contentType: import_zod26.z.string(),
20964
+ bytes: import_zod26.z.number().int().min(0),
20965
+ truncated: import_zod26.z.boolean(),
20966
+ text: import_zod26.z.string()
20967
+ };
20602
20968
  SearchSerpInputSchema = {
20603
20969
  query: import_zod26.z.string().min(1).describe('Core search topic only. Separate location when possible. If user says "best dentist in Brooklyn NY serp", use query="best dentist" and location="Brooklyn, NY".'),
20604
20970
  location: import_zod26.z.string().optional().describe("City, region, or country for geo-targeted results, inferred from user request when present."),
@@ -21130,6 +21496,41 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
21130
21496
  outputSchema: DirectoryWorkflowOutputSchema,
21131
21497
  annotations: liveWebToolAnnotations("Directory Workflow: Markets + Maps")
21132
21498
  }, async (input) => formatDirectoryWorkflow(await executor.directoryWorkflow(input), input));
21499
+ server.registerTool("workflow_list", {
21500
+ title: "Workflow Catalog",
21501
+ description: "List MCP Scraper higher-level workflows and AI-facing recipes. Use this when the user asks what MCP Scraper can do beyond single tools, or asks for market analysis, ICP research, forum/review acquisition, full brand design briefings, CRO audits, competitive positioning, content gap briefs, or AI search visibility audits. Returns runnable workflow ids plus recipe guidance for the best tool chain.",
21502
+ inputSchema: WorkflowListInputSchema,
21503
+ outputSchema: WorkflowListOutputSchema,
21504
+ annotations: localPlanningToolAnnotations("Workflow Catalog")
21505
+ }, async (input) => formatWorkflowList(await executor.workflowList(input), input));
21506
+ server.registerTool("workflow_suggest", {
21507
+ title: "Workflow Intent Router",
21508
+ description: 'Route a high-level business or research goal to the right MCP Scraper workflow/tool chain. Use before running work when the user says things like "do market analysis", "research my ICP", "acquire forum and review language", "build a brand design brief", "run a CRO audit", "compare competitors", "make a content gap brief", or "audit AI search visibility". This tool does not spend credits; it tells the AI exactly which workflow/tool chain to run next.',
21509
+ inputSchema: WorkflowSuggestInputSchema,
21510
+ outputSchema: WorkflowSuggestOutputSchema,
21511
+ annotations: localPlanningToolAnnotations("Workflow Intent Router")
21512
+ }, async (input) => formatWorkflowSuggest(input));
21513
+ server.registerTool("workflow_run", {
21514
+ title: "Run Workflow",
21515
+ description: withReportNote("Run a higher-level MCP Scraper workflow and return the run id, summary, and artifacts. Use after workflow_suggest or workflow_list. Runnable workflow ids: directory, agent-packet, local-competitive-audit, map-comparison, serp-comparison, paa-expansion-brief, ai-overview-language. This is the main MCP tool for executing market analysis, ICP evidence packets, local competitive audits, Maps/SERP comparisons, content gap briefs, and AI Overview language guidance."),
21516
+ inputSchema: WorkflowRunInputSchema,
21517
+ outputSchema: WorkflowRunOutputSchema,
21518
+ annotations: liveWebToolAnnotations("Run Workflow")
21519
+ }, async (input) => formatWorkflowRun(await executor.workflowRun(input), input));
21520
+ server.registerTool("workflow_status", {
21521
+ title: "Workflow Status",
21522
+ description: "Fetch a hosted workflow run by id and list its current status and artifacts. Use after workflow_run when the model needs to re-open a run, inspect artifact ids, or recover from a long workflow.",
21523
+ inputSchema: WorkflowStatusInputSchema,
21524
+ outputSchema: WorkflowStatusOutputSchema,
21525
+ annotations: liveWebToolAnnotations("Workflow Status")
21526
+ }, async (input) => formatWorkflowStatus(await executor.workflowStatus(input), input));
21527
+ server.registerTool("workflow_artifact_read", {
21528
+ title: "Read Workflow Artifact",
21529
+ description: "Read a workflow artifact back into MCP context by run id and artifact id. Use this before writing final deliverables so the answer is grounded in generated evidence.json, CSVs, Markdown briefs, reports, and task files instead of memory. Use maxBytes to limit large CSV/JSON artifacts.",
21530
+ inputSchema: WorkflowArtifactReadInputSchema,
21531
+ outputSchema: WorkflowArtifactReadOutputSchema,
21532
+ annotations: liveWebToolAnnotations("Read Workflow Artifact")
21533
+ }, async (input) => formatWorkflowArtifactRead(await executor.workflowArtifactRead(input), input));
21133
21534
  server.registerTool("rank_tracker_blueprint", {
21134
21535
  title: "Rank Tracker Blueprint Builder",
21135
21536
  description: "Generate a build-ready database, cron/heartbeat, ingestion, metrics, and AI implementation prompt for a rank tracker powered by MCP Scraper. Supports Maps rankings through directory_workflow/maps_search, organic rankings through search_serp, AI Overview citation tracking, and People Also Ask source presence tracking. This tool is local planning only; it does not call the web or spend credits.",
@@ -21225,6 +21626,57 @@ var init_http_mcp_tool_executor = __esm({
21225
21626
  return { content: [{ type: "text", text: msg }], isError: true };
21226
21627
  }
21227
21628
  }
21629
+ async getJson(path6, timeoutMs = this.timeoutMs) {
21630
+ try {
21631
+ const res = await fetch(`${this.baseUrl}${path6}`, {
21632
+ method: "GET",
21633
+ headers: {
21634
+ "x-api-key": this.apiKey
21635
+ },
21636
+ signal: AbortSignal.timeout(timeoutMs)
21637
+ });
21638
+ const data = await res.json();
21639
+ if (!res.ok) {
21640
+ return { content: [{ type: "text", text: JSON.stringify(data) }], isError: true };
21641
+ }
21642
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
21643
+ } catch (err) {
21644
+ const msg = err instanceof Error ? err.message : String(err);
21645
+ return { content: [{ type: "text", text: msg }], isError: true };
21646
+ }
21647
+ }
21648
+ async getTextArtifact(path6, maxBytes, timeoutMs = this.timeoutMs) {
21649
+ try {
21650
+ const res = await fetch(`${this.baseUrl}${path6}`, {
21651
+ method: "GET",
21652
+ headers: {
21653
+ "x-api-key": this.apiKey
21654
+ },
21655
+ signal: AbortSignal.timeout(timeoutMs)
21656
+ });
21657
+ if (!res.ok) {
21658
+ const data = await res.json().catch(async () => ({ error: await res.text().catch(() => `HTTP ${res.status}`) }));
21659
+ return { content: [{ type: "text", text: JSON.stringify(data) }], isError: true };
21660
+ }
21661
+ const bytes = Buffer.from(await res.arrayBuffer());
21662
+ const sliced = bytes.subarray(0, Math.min(maxBytes, bytes.length));
21663
+ return {
21664
+ content: [{
21665
+ type: "text",
21666
+ text: JSON.stringify({
21667
+ contentType: res.headers.get("content-type") ?? "application/octet-stream",
21668
+ bytes: bytes.length,
21669
+ truncated: sliced.length < bytes.length,
21670
+ maxBytes,
21671
+ text: sliced.toString("utf8")
21672
+ })
21673
+ }]
21674
+ };
21675
+ } catch (err) {
21676
+ const msg = err instanceof Error ? err.message : String(err);
21677
+ return { content: [{ type: "text", text: msg }], isError: true };
21678
+ }
21679
+ }
21228
21680
  harvestPaa(input) {
21229
21681
  const timeoutMs = this.httpTimeoutOverrideMs ?? harvestTimeoutBudget(input.maxQuestions ?? 30).clientMs;
21230
21682
  return this.call("/harvest/sync", input, timeoutMs);
@@ -21272,6 +21724,26 @@ var init_http_mcp_tool_executor = __esm({
21272
21724
  const timeoutMs = this.httpTimeoutOverrideMs ?? Math.min(9e5, Math.max(18e4, Math.ceil(cityCount / concurrency) * 12e4));
21273
21725
  return this.call("/directory/run", input, timeoutMs);
21274
21726
  }
21727
+ workflowList(_input) {
21728
+ return this.getJson("/workflows/definitions");
21729
+ }
21730
+ workflowRun(input) {
21731
+ const timeoutMs = this.httpTimeoutOverrideMs ?? Number(process.env.MCP_SCRAPER_WORKFLOW_TIMEOUT_MS ?? 9e5);
21732
+ return this.call("/workflows/run", {
21733
+ workflowId: input.workflowId,
21734
+ input: input.input ?? {},
21735
+ webhookUrl: input.webhookUrl
21736
+ }, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 9e5);
21737
+ }
21738
+ workflowStatus(input) {
21739
+ return this.getJson(`/workflows/runs/${encodeURIComponent(input.runId)}`);
21740
+ }
21741
+ workflowArtifactRead(input) {
21742
+ return this.getTextArtifact(
21743
+ `/workflows/runs/${encodeURIComponent(input.runId)}/artifacts/${encodeURIComponent(input.artifactId)}`,
21744
+ input.maxBytes ?? 2e5
21745
+ );
21746
+ }
21275
21747
  creditsInfo(input) {
21276
21748
  return this.call("/billing/credits", input);
21277
21749
  }
@@ -21677,16 +22149,16 @@ async function locatePageTargets(cdpWsUrl, targets) {
21677
22149
  }).catch(() => {
21678
22150
  });
21679
22151
  const data = await page.evaluate((rawTargets) => {
21680
- const normalize = (value) => value.replace(/\s+/g, " ").trim();
22152
+ const normalize2 = (value) => value.replace(/\s+/g, " ").trim();
21681
22153
  const isVisibleRect = (r) => r.width >= 1 && r.height >= 1 && r.bottom >= 0 && r.right >= 0 && r.top <= window.innerHeight && r.left <= window.innerWidth;
21682
22154
  const roleOf = (el2) => el2.getAttribute("role") || el2.tagName.toLowerCase();
21683
22155
  const nameOf = (el2) => {
21684
22156
  const e = el2;
21685
- return normalize(
22157
+ return normalize2(
21686
22158
  e.getAttribute("aria-label") || e.placeholder || e.innerText || e.getAttribute("value") || e.getAttribute("title") || ""
21687
22159
  ).slice(0, 160);
21688
22160
  };
21689
- const textOf = (el2) => normalize(el2.innerText || el2.textContent || "").slice(0, 500);
22161
+ const textOf = (el2) => normalize2(el2.innerText || el2.textContent || "").slice(0, 500);
21690
22162
  const unionRects = (rects) => {
21691
22163
  const visible = rects.filter(isVisibleRect);
21692
22164
  if (!visible.length) return null;
@@ -21725,7 +22197,7 @@ async function locatePageTargets(cdpWsUrl, targets) {
21725
22197
  return nodes.map((el2) => ({ el: el2, rect: el2.getBoundingClientRect() })).filter((item) => isVisibleRect(item.rect));
21726
22198
  };
21727
22199
  const textMatches = (needle, match) => {
21728
- const wanted = normalize(needle);
22200
+ const wanted = normalize2(needle);
21729
22201
  const wantedLower = wanted.toLowerCase();
21730
22202
  if (!wantedLower) return [];
21731
22203
  const matches = [];
@@ -21733,7 +22205,7 @@ async function locatePageTargets(cdpWsUrl, targets) {
21733
22205
  let node = walker.nextNode();
21734
22206
  while (node) {
21735
22207
  const raw = node.textContent || "";
21736
- const normalizedRaw = normalize(raw);
22208
+ const normalizedRaw = normalize2(raw);
21737
22209
  const rawLower = raw.toLowerCase();
21738
22210
  const normalizedLower = normalizedRaw.toLowerCase();
21739
22211
  const ok = match === "exact" ? normalizedLower === wantedLower : normalizedLower.includes(wantedLower);