mcp-scraper 0.26.2 → 0.27.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 (41) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/api-server.cjs +309 -270
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +2 -2
  5. package/dist/bin/mcp-scraper-cli.cjs +3 -2
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +3 -3
  8. package/dist/bin/mcp-scraper-install.cjs +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +1 -1
  11. package/dist/bin/mcp-stdio-server.cjs +61 -27
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +64 -5
  14. package/dist/bin/mcp-stdio-server.js.map +1 -1
  15. package/dist/bin/paa-harvest.cjs +2 -1
  16. package/dist/bin/paa-harvest.cjs.map +1 -1
  17. package/dist/bin/paa-harvest.js +2 -2
  18. package/dist/{chunk-FUWZWKGO.js → chunk-KAMKTSJQ.js} +2 -2
  19. package/dist/chunk-O6KFDDRM.js +7 -0
  20. package/dist/chunk-O6KFDDRM.js.map +1 -0
  21. package/dist/{chunk-QO2TRJ3L.js → chunk-RPR5LDLS.js} +24 -48
  22. package/dist/chunk-RPR5LDLS.js.map +1 -0
  23. package/dist/{chunk-E5J4HJBO.js → chunk-YH5DRFWQ.js} +4 -2
  24. package/dist/{chunk-XGIPATLV.js → chunk-ZQSHKWV4.js} +3 -2
  25. package/dist/chunk-ZQSHKWV4.js.map +1 -0
  26. package/dist/index.cjs +2 -1
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.js +2 -2
  29. package/dist/{server-K6ODZOZA.js → server-YXVSZ3T7.js} +323 -262
  30. package/dist/server-YXVSZ3T7.js.map +1 -0
  31. package/dist/{worker-E3KHQH2F.js → worker-RYP5EOV5.js} +3 -3
  32. package/docs/mcp-tool-manifest.generated.json +37 -8
  33. package/package.json +1 -1
  34. package/dist/chunk-QO2TRJ3L.js.map +0 -1
  35. package/dist/chunk-V37MZON3.js +0 -7
  36. package/dist/chunk-V37MZON3.js.map +0 -1
  37. package/dist/chunk-XGIPATLV.js.map +0 -1
  38. package/dist/server-K6ODZOZA.js.map +0 -1
  39. /package/dist/{chunk-FUWZWKGO.js.map → chunk-KAMKTSJQ.js.map} +0 -0
  40. /package/dist/{chunk-E5J4HJBO.js.map → chunk-YH5DRFWQ.js.map} +0 -0
  41. /package/dist/{worker-E3KHQH2F.js.map → worker-RYP5EOV5.js.map} +0 -0
@@ -12,12 +12,13 @@ import {
12
12
  MapsSelectors,
13
13
  buildYouTubeChannelVideosUrl,
14
14
  deleteKernelProxyId,
15
+ encodeUule,
15
16
  harvest,
17
+ normalizeLocation,
16
18
  resolveKernelProxyId
17
- } from "./chunk-E5J4HJBO.js";
19
+ } from "./chunk-YH5DRFWQ.js";
18
20
  import {
19
21
  HttpMcpToolExecutor,
20
- MemoryMcpToolExecutor,
21
22
  assessTranscriptSignal,
22
23
  buildLinkGraph,
23
24
  buildPaaExtractorMcpServer,
@@ -35,7 +36,7 @@ import {
35
36
  sanitizeAttempts,
36
37
  sanitizeHarvestResult,
37
38
  transcribeMediaUrl
38
- } from "./chunk-QO2TRJ3L.js";
39
+ } from "./chunk-RPR5LDLS.js";
39
40
  import {
40
41
  auditImages,
41
42
  buildLinkReport,
@@ -64,7 +65,7 @@ import {
64
65
  workflowOutputBaseDir,
65
66
  workflowStepCount,
66
67
  workflowSupportsSteps
67
- } from "./chunk-FUWZWKGO.js";
68
+ } from "./chunk-KAMKTSJQ.js";
68
69
  import "./chunk-K443GQY5.js";
69
70
  import {
70
71
  DEFAULT_MAPS_PROXY_MODE,
@@ -75,8 +76,8 @@ import {
75
76
  RawMapsHoursRowSchema,
76
77
  RawMapsOverviewSchema,
77
78
  RawMapsReviewStatsSchema
78
- } from "./chunk-XGIPATLV.js";
79
- import "./chunk-V37MZON3.js";
79
+ } from "./chunk-ZQSHKWV4.js";
80
+ import "./chunk-O6KFDDRM.js";
80
81
  import {
81
82
  completeExtractJob,
82
83
  countSuccessfulPages,
@@ -13078,6 +13079,262 @@ videoApp.post("/status", createApiKeyAuth(), async (c) => {
13078
13079
  // src/api/maps-routes.ts
13079
13080
  import { Hono as Hono11 } from "hono";
13080
13081
 
13082
+ // src/extractor/MapsSearchExtractor.ts
13083
+ var LOCAL_RESULTS_PAGE_LIMIT = 8;
13084
+ var LOCAL_RESULTS_WAIT_MS = 1e3;
13085
+ function buildGoogleLocalResultsUrl(input) {
13086
+ const query = [input.query, input.location].filter(Boolean).join(" ");
13087
+ const params = new URLSearchParams({ q: query, udm: "1", gl: input.gl, hl: input.hl, pws: "0" });
13088
+ if (input.location) params.set("uule", encodeUule(normalizeLocation(input.location)));
13089
+ return `https://www.google.com/search?${params.toString()}`;
13090
+ }
13091
+ var MapsSearchExtractor = class {
13092
+ constructor(driver) {
13093
+ this.driver = driver;
13094
+ }
13095
+ driver;
13096
+ async extract(options) {
13097
+ const startMs = Date.now();
13098
+ const searchQuery = [options.query, options.location].filter(Boolean).join(" ");
13099
+ const searchUrl = buildGoogleLocalResultsUrl(options);
13100
+ const config = {
13101
+ headless: options.headless,
13102
+ kernelApiKey: options.kernelApiKey,
13103
+ kernelProxyId: options.kernelProxyId,
13104
+ kernelProxyResolution: options.kernelProxyResolution,
13105
+ proxyMode: options.proxyMode,
13106
+ viewport: { width: 1280, height: 900 },
13107
+ locale: `${options.hl}-${options.gl.toUpperCase()}`,
13108
+ debug: options.debug
13109
+ };
13110
+ try {
13111
+ await this.driver.launch(config);
13112
+ const page = this.driver.getPage();
13113
+ await page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
13114
+ await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
13115
+ if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
13116
+ const results = await this.collectResults(page, options);
13117
+ return {
13118
+ query: options.query,
13119
+ location: options.location ?? null,
13120
+ searchQuery,
13121
+ searchUrl,
13122
+ extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
13123
+ requestedMaxResults: options.maxResults,
13124
+ resultCount: results.length,
13125
+ results,
13126
+ durationMs: Date.now() - startMs
13127
+ };
13128
+ } finally {
13129
+ await this.driver.close();
13130
+ }
13131
+ }
13132
+ async detectBlock(page) {
13133
+ return page.evaluate(() => {
13134
+ const text2 = document.body.innerText.slice(0, 2e3);
13135
+ return /unusual traffic|captcha|recaptcha|about this page/i.test(text2) || /\/sorry\//.test(location.href);
13136
+ });
13137
+ }
13138
+ async collectResults(page, options) {
13139
+ const seen = /* @__PURE__ */ new Set();
13140
+ const results = [];
13141
+ let pageCount = 0;
13142
+ while (results.length < options.maxResults && pageCount < LOCAL_RESULTS_PAGE_LIMIT) {
13143
+ if (await this.detectBlock(page)) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
13144
+ const cards = await this.extractVisibleCards(page, options.location);
13145
+ for (const card of cards) {
13146
+ if (results.length >= options.maxResults || seen.has(card.cardKey)) continue;
13147
+ seen.add(card.cardKey);
13148
+ const details = await this.openCardAndExtractDialog(page, card, options.includeServices);
13149
+ results.push({
13150
+ position: results.length + 1,
13151
+ name: card.name,
13152
+ placeUrl: details?.placeUrl ?? card.placeUrl,
13153
+ cid: details?.cid ?? card.cid,
13154
+ cidDecimal: details?.cidDecimal ?? card.cidDecimal,
13155
+ rating: details?.rating ?? card.rating,
13156
+ reviewCount: details?.reviewCount ?? card.reviewCount,
13157
+ category: details?.category ?? card.category,
13158
+ address: details?.address ?? card.address,
13159
+ phone: details?.phone ?? card.phone,
13160
+ hoursStatus: details?.hoursStatus ?? card.hoursStatus,
13161
+ websiteUrl: details?.websiteUrl ?? card.websiteUrl,
13162
+ directionsUrl: card.directionsUrl,
13163
+ metadata: card.metadata,
13164
+ services: details?.services ?? [],
13165
+ areasServed: details?.areasServed ?? [],
13166
+ profileDetailsStatus: details ? "collected" : "unavailable"
13167
+ });
13168
+ }
13169
+ if (results.length >= options.maxResults) break;
13170
+ const advanced = await this.advanceToMoreResults(page);
13171
+ if (!advanced) break;
13172
+ pageCount += 1;
13173
+ }
13174
+ return results;
13175
+ }
13176
+ async extractVisibleCards(page, location2) {
13177
+ const fallbackLocation = location2 ?? "";
13178
+ return page.evaluate((locationHint) => {
13179
+ const normalize = (value) => {
13180
+ const text2 = value?.replace(/\s+/g, " ").trim() ?? "";
13181
+ return text2 || null;
13182
+ };
13183
+ const phonePattern = /(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/;
13184
+ const addressPattern = /\b[A-Z]{2}\s+\d{5}(?:-\d{4})?\b|\b(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Boulevard|Dr|Drive|Ln|Lane|Way|Pkwy|Parkway)\b/i;
13185
+ const cards = Array.from(document.querySelectorAll("button")).filter((button) => {
13186
+ const name = normalize(button.querySelector("h3")?.textContent);
13187
+ return Boolean(name) && !button.closest('[role="dialog"], dialog') && !/\bsponsored\b/i.test(button.innerText);
13188
+ });
13189
+ const seen = /* @__PURE__ */ new Set();
13190
+ const out = [];
13191
+ for (const [cardIndex, card] of cards.entries()) {
13192
+ const name = normalize(card.querySelector("h3")?.textContent);
13193
+ if (!name) continue;
13194
+ const text2 = normalize(card.innerText) ?? name;
13195
+ const key = `${name.toLowerCase()}|${text2.toLowerCase()}`;
13196
+ if (seen.has(key)) continue;
13197
+ seen.add(key);
13198
+ const lines = text2.split(/\n+/).map((line) => normalize(line)).filter((line) => Boolean(line));
13199
+ const aria = normalize(card.getAttribute("aria-label"));
13200
+ const rating = (aria ?? text2).match(/(?:rated\s+)?(\d(?:\.\d)?)\s*(?:out of 5)?/i)?.[1] ?? null;
13201
+ const reviewCount = (aria ?? text2).match(/(?:\(|·\s*)([\d,]+)\s+(?:user\s+)?reviews?\b/i)?.[1]?.replace(/,/g, "") ?? null;
13202
+ const phone = lines.find((line) => phonePattern.test(line)) ?? null;
13203
+ const address = lines.find((line) => addressPattern.test(line)) ?? null;
13204
+ const hoursStatus = lines.find((line) => /\b(?:open|closed|opens|closes)\b/i.test(line)) ?? null;
13205
+ const category = lines.find(
13206
+ (line) => line !== name && line !== phone && line !== address && line !== hoursStatus && !/^(?:rated|\d[\d.,]*\s*(?:out of 5|reviews?)?)\b/i.test(line) && !/sponsored|website|directions|call|save|share|more/i.test(line)
13207
+ ) ?? null;
13208
+ const mapsSearchUrl = `https://www.google.com/maps/search/${encodeURIComponent([name, locationHint].filter(Boolean).join(" "))}`;
13209
+ out.push({
13210
+ cardIndex,
13211
+ cardKey: key,
13212
+ name,
13213
+ placeUrl: mapsSearchUrl,
13214
+ cid: null,
13215
+ cidDecimal: null,
13216
+ rating,
13217
+ reviewCount,
13218
+ category,
13219
+ address,
13220
+ phone,
13221
+ hoursStatus,
13222
+ websiteUrl: null,
13223
+ directionsUrl: `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", "))}`,
13224
+ metadata: lines.slice(0, 20)
13225
+ });
13226
+ }
13227
+ return out;
13228
+ }, fallbackLocation);
13229
+ }
13230
+ async openCardAndExtractDialog(page, card, includeServices) {
13231
+ const clicked = await page.evaluate((input) => {
13232
+ const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim().toLowerCase();
13233
+ const cards = Array.from(document.querySelectorAll("button")).filter(
13234
+ (button) => Boolean(button.querySelector("h3")) && !button.closest('[role="dialog"], dialog') && !/\bsponsored\b/i.test(button.innerText)
13235
+ );
13236
+ const exact = cards.find((button) => `${normalize(button.querySelector("h3")?.textContent)}|${normalize(button.innerText)}` === input.cardKey);
13237
+ const target = exact ?? cards[input.cardIndex];
13238
+ if (!target) return false;
13239
+ target.click();
13240
+ return true;
13241
+ }, { cardIndex: card.cardIndex, cardKey: card.cardKey });
13242
+ if (!clicked) return null;
13243
+ const dialog = page.locator('dialog, [role="dialog"]').filter({ has: page.locator('h1, h2, [role="heading"]') }).last();
13244
+ await dialog.waitFor({ state: "visible", timeout: 6e3 }).catch(() => {
13245
+ });
13246
+ if (await dialog.count() === 0 || !await dialog.isVisible().catch(() => false)) return null;
13247
+ const details = await dialog.evaluate((root, includeServicesValue) => {
13248
+ const text2 = (value) => value?.replace(/\s+/g, " ").trim() ?? "";
13249
+ const phonePattern = /(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/;
13250
+ const buttons = Array.from(root.querySelectorAll("button"));
13251
+ const buttonTexts = buttons.map((button) => text2(button.innerText)).filter(Boolean);
13252
+ const servicesText = includeServicesValue ? buttonTexts.find((value) => /^services\s*:/i.test(value)) : null;
13253
+ const services = servicesText ? [...new Set(servicesText.replace(/^services\s*:\s*/i, "").split(/\s*[,·]\s*/).map((value) => value.trim()).filter(Boolean))] : [];
13254
+ const areaText = includeServicesValue ? buttonTexts.find((value) => /^serves\b/i.test(value)) : null;
13255
+ const areasServed = areaText ? [areaText.replace(/^serves\s*/i, "").trim()].filter(Boolean) : [];
13256
+ const externalLinks = Array.from(root.querySelectorAll("a[href]"));
13257
+ const website = externalLinks.find((link) => /^website$/i.test(text2(link.innerText)) || /^website$/i.test(link.getAttribute("aria-label") ?? ""));
13258
+ const mapLink = externalLinks.find((link) => /google\.[^/]+\/maps|\/maps\//i.test(link.href));
13259
+ const addressLink = externalLinks.find((link) => /\b[A-Z]{2}\s+\d{5}(?:-\d{4})?\b|\b(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Boulevard|Dr|Drive|Ln|Lane|Way|Pkwy|Parkway)\b/i.test(text2(link.innerText)));
13260
+ const allText = text2(root.innerText);
13261
+ const rating = allText.match(/\b(?:rated\s+)?(\d(?:\.\d)?)\s+out of 5\b/i)?.[1] ?? allText.match(/\b(\d(?:\.\d)?)\s+rated\b/i)?.[1] ?? null;
13262
+ const reviewCount = allText.match(/([\d,]+)\s+(?:user\s+)?reviews?\b/i)?.[1]?.replace(/,/g, "") ?? null;
13263
+ const phoneButton = buttonTexts.find((value) => /^call\b/i.test(value) || phonePattern.test(value)) ?? "";
13264
+ const phone = phoneButton.match(phonePattern)?.[0] ?? null;
13265
+ const hoursStatus = buttonTexts.find((value) => /\b(?:open|closed|opens|closes)\b/i.test(value)) ?? null;
13266
+ const placeUrl = mapLink?.href ?? addressLink?.href ?? location.href;
13267
+ const fid = placeUrl.match(/!1s(0x[0-9a-f]+):(0x[0-9a-f]+)/i);
13268
+ const cid = fid ? `${fid[1]}:${fid[2]}` : null;
13269
+ let cidDecimal = null;
13270
+ if (fid) {
13271
+ try {
13272
+ cidDecimal = BigInt(fid[2]).toString();
13273
+ } catch {
13274
+ }
13275
+ }
13276
+ if (!cidDecimal) {
13277
+ const cidFromQuery = new URL(placeUrl).searchParams.get("cid");
13278
+ cidDecimal = cidFromQuery && /^\d+$/.test(cidFromQuery) ? cidFromQuery : null;
13279
+ }
13280
+ return {
13281
+ placeUrl,
13282
+ cid,
13283
+ cidDecimal,
13284
+ rating,
13285
+ reviewCount,
13286
+ category: null,
13287
+ address: text2(addressLink?.innerText) || null,
13288
+ phone,
13289
+ websiteUrl: website?.href ?? null,
13290
+ hoursStatus,
13291
+ services,
13292
+ areasServed
13293
+ };
13294
+ }, includeServices);
13295
+ await this.closeDialog(page);
13296
+ return details;
13297
+ }
13298
+ async closeDialog(page) {
13299
+ const closed = await page.evaluate(() => {
13300
+ const dialogs = Array.from(document.querySelectorAll('dialog, [role="dialog"]'));
13301
+ const dialog = dialogs.find((candidate) => candidate.offsetParent !== null);
13302
+ const close = dialog && Array.from(dialog.querySelectorAll("button")).find(
13303
+ (button) => /^close$/i.test(button.getAttribute("aria-label") ?? "") || /^close$/i.test(button.innerText.trim())
13304
+ );
13305
+ close?.click();
13306
+ return Boolean(close);
13307
+ });
13308
+ if (closed) await page.waitForTimeout(250);
13309
+ }
13310
+ async advanceToMoreResults(page) {
13311
+ const before = await page.evaluate(() => document.body.innerText.slice(-2e3));
13312
+ const advanced = await page.evaluate(() => {
13313
+ const visible = (element) => element.offsetParent !== null;
13314
+ const mobile = Array.from(document.querySelectorAll("button, a")).find(
13315
+ (element) => visible(element) && /^(more search results|more results)$/i.test(element.innerText.trim())
13316
+ );
13317
+ if (mobile) {
13318
+ mobile.click();
13319
+ return true;
13320
+ }
13321
+ const next = Array.from(document.querySelectorAll("a[href]")).find(
13322
+ (link) => visible(link) && (/^next$/i.test(link.innerText.trim()) || /[?&]start=\d+/.test(link.href))
13323
+ );
13324
+ if (next) {
13325
+ next.click();
13326
+ return true;
13327
+ }
13328
+ return false;
13329
+ });
13330
+ if (!advanced) return false;
13331
+ await page.waitForTimeout(LOCAL_RESULTS_WAIT_MS);
13332
+ await page.waitForFunction((previous) => document.body.innerText.slice(-2e3) !== previous, before, { timeout: 5e3 }).catch(() => {
13333
+ });
13334
+ return true;
13335
+ }
13336
+ };
13337
+
13081
13338
  // src/extractor/MapsNavigator.ts
13082
13339
  var MapsNavigator = class {
13083
13340
  constructor(page) {
@@ -13136,32 +13393,26 @@ var MapsNavigator = class {
13136
13393
  await this.page.waitForSelector('[role="main"] h2', { timeout: 5e3 }).catch(() => {
13137
13394
  });
13138
13395
  }
13139
- async navigateToSerpBusinessPanel(businessName, location2) {
13140
- const query = `${businessName} ${location2}`;
13141
- const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}&udm=1`;
13396
+ async navigateToSerpBusinessPanel(businessName, location2, gl, hl) {
13397
+ const searchUrl = buildGoogleLocalResultsUrl({ query: businessName, location: location2, gl, hl });
13142
13398
  await this.page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch(() => {
13143
13399
  });
13144
- await this.page.waitForTimeout(2e3);
13400
+ await this.page.waitForTimeout(1e3);
13145
13401
  const clicked = await this.page.evaluate((name) => {
13146
- const trimmedName = name.trim();
13147
- const words = trimmedName.split(/\s+/);
13148
- const prefix = words.slice(0, Math.min(2, words.length)).join(" ");
13149
- const headings = Array.from(document.querySelectorAll('h3, div[role="heading"]'));
13150
- const heading = headings.find((el2) => el2.innerText?.trim() === trimmedName) ?? headings.find((el2) => el2.innerText?.trim().startsWith(prefix));
13151
- const clickable = heading?.closest('a, div[role="link"], div[jsaction]');
13152
- if (clickable) {
13153
- clickable.click();
13154
- return true;
13155
- }
13156
- if (heading) {
13157
- heading.click();
13402
+ const normalized = name.replace(/\s+/g, " ").trim().toLowerCase();
13403
+ const card = Array.from(document.querySelectorAll("button")).find(
13404
+ (button) => button.querySelector("h3")?.textContent?.replace(/\s+/g, " ").trim().toLowerCase() === normalized
13405
+ );
13406
+ if (card) {
13407
+ card.click();
13158
13408
  return true;
13159
13409
  }
13160
13410
  return false;
13161
13411
  }, businessName);
13162
13412
  if (!clicked) return false;
13163
- await this.page.waitForTimeout(2e3);
13164
- return true;
13413
+ await this.page.waitForSelector('dialog, [role="dialog"]', { timeout: 5e3 }).catch(() => {
13414
+ });
13415
+ return this.page.locator('dialog, [role="dialog"]').count().then((count) => count > 0);
13165
13416
  }
13166
13417
  buildReviewsUrl(placeUrl) {
13167
13418
  const fid = placeUrl.match(/!1s(0x[0-9a-f]+:0x[0-9a-f]+)/i);
@@ -13337,7 +13588,7 @@ var MapsExtractor = class {
13337
13588
  let servicesStatus = "not_requested";
13338
13589
  if (options.includeServices) {
13339
13590
  try {
13340
- const opened = await navigator.navigateToSerpBusinessPanel(options.businessName, options.location);
13591
+ const opened = await navigator.navigateToSerpBusinessPanel(options.businessName, options.location, options.gl, options.hl);
13341
13592
  if (opened) {
13342
13593
  const result = await this.extractServicesAndAreasServed(page);
13343
13594
  services = result.services;
@@ -13558,242 +13809,15 @@ var MapsExtractor = class {
13558
13809
  return result.data;
13559
13810
  }
13560
13811
  async extractServicesAndAreasServed(page) {
13561
- await page.waitForTimeout(1500);
13562
- let services = [];
13563
- if (await this.clickRowStartingWithRetry(page, "Services:")) {
13564
- await page.waitForSelector('[role="dialog"]', { timeout: 5e3 }).catch(() => {
13565
- });
13566
- await page.waitForTimeout(800);
13567
- services = await this.extractOpenDialogItems(page, "Services");
13568
- await this.closeOpenDialog(page, "Services");
13569
- await page.waitForTimeout(500);
13570
- }
13571
- let areasServed = [];
13572
- if (await this.clickRowStartingWithRetry(page, "Serves ")) {
13573
- await page.waitForSelector('[role="dialog"]', { timeout: 5e3 }).catch(() => {
13574
- });
13575
- await page.waitForTimeout(800);
13576
- areasServed = await this.extractOpenDialogItems(page, "Areas Served");
13577
- await this.closeOpenDialog(page, "Areas Served");
13578
- }
13579
- return { services, areasServed };
13580
- }
13581
- async clickRowStartingWithRetry(page, prefix) {
13582
- if (await this.clickRowStartingWith(page, prefix)) return true;
13583
- await page.waitForTimeout(2e3);
13584
- return this.clickRowStartingWith(page, prefix);
13585
- }
13586
- async clickRowStartingWith(page, prefix) {
13587
- return page.evaluate((p) => {
13588
- const el2 = Array.from(document.querySelectorAll('button, a, div[role="button"]')).find(
13589
- (e) => (e.innerText ?? "").trim().startsWith(p)
13590
- );
13591
- if (!el2) return false;
13592
- el2.click();
13593
- return true;
13594
- }, prefix);
13595
- }
13596
- async extractOpenDialogItems(page, expectedHeading) {
13597
- return page.evaluate((expected) => {
13598
- const dialogs = Array.from(document.querySelectorAll('[role="dialog"]'));
13599
- const withHeading = dialogs.find((d) => {
13600
- const h = d.querySelector('h1, h2, [role="heading"]');
13601
- const headingText = h?.innerText?.trim() ?? "";
13602
- return headingText.startsWith(expected);
13603
- });
13604
- if (!withHeading) return [];
13605
- const headingEl = withHeading.querySelector('h1, h2, [role="heading"]');
13606
- const headingLines = new Set(
13607
- (headingEl?.innerText ?? "").split("\n").map((s) => s.trim()).filter(Boolean)
13608
- );
13609
- const seen = /* @__PURE__ */ new Set();
13610
- const items = [];
13611
- withHeading.querySelectorAll("*").forEach((el2) => {
13612
- if (el2.children.length > 0) return;
13613
- const text2 = el2.innerText?.trim() ?? el2.textContent?.trim() ?? "";
13614
- if (!text2 || headingLines.has(text2)) return;
13615
- if (/^(back|close|done|thanks)$/i.test(text2)) return;
13616
- if (/^serves\s.*following/i.test(text2)) return;
13617
- if (seen.has(text2)) return;
13618
- seen.add(text2);
13619
- items.push(text2);
13620
- });
13621
- return items;
13622
- }, expectedHeading);
13623
- }
13624
- async closeOpenDialog(page, expectedHeading) {
13625
- await page.evaluate((expected) => {
13626
- const dialog = Array.from(document.querySelectorAll('[role="dialog"]')).find((d) => {
13627
- const h = d.querySelector('h1, h2, [role="heading"]');
13628
- const headingText = h?.innerText?.trim() ?? "";
13629
- return headingText.startsWith(expected);
13630
- });
13631
- const closeBtn = dialog?.querySelector('[aria-label*="Close" i], button[aria-label*="close" i]');
13632
- closeBtn?.click();
13633
- }, expectedHeading);
13634
- }
13635
- };
13636
-
13637
- // src/extractor/MapsSearchExtractor.ts
13638
- var MAPS_SEARCH_SCROLL_BUDGET_MS = 6e4;
13639
- var MAPS_SEARCH_SCROLL_STEP_MS = 1200;
13640
- var MAPS_SEARCH_MAX_NO_GROWTH_ROUNDS = 4;
13641
- var MapsSearchExtractor = class {
13642
- constructor(driver) {
13643
- this.driver = driver;
13644
- }
13645
- driver;
13646
- async extract(options) {
13647
- const startMs = Date.now();
13648
- const searchQuery = [options.query, options.location].filter(Boolean).join(" ");
13649
- const searchUrl = `https://www.google.com/maps/search/${encodeURIComponent(searchQuery)}?hl=${encodeURIComponent(options.hl)}`;
13650
- const config = {
13651
- headless: options.headless,
13652
- kernelApiKey: options.kernelApiKey,
13653
- kernelProxyId: options.kernelProxyId,
13654
- kernelProxyResolution: options.kernelProxyResolution,
13655
- proxyMode: options.proxyMode,
13656
- viewport: { width: 1280, height: 900 },
13657
- locale: `${options.hl}-${options.gl.toUpperCase()}`,
13658
- debug: options.debug
13659
- };
13660
- try {
13661
- await this.driver.launch(config);
13662
- const page = this.driver.getPage();
13663
- await page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
13664
- await page.waitForTimeout(3e3);
13665
- const blocked = await this.detectBlock(page);
13666
- if (blocked) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
13667
- const results = await this.collectResults(page, options.maxResults);
13668
- return {
13669
- query: options.query,
13670
- location: options.location ?? null,
13671
- searchQuery,
13672
- searchUrl,
13673
- extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
13674
- requestedMaxResults: options.maxResults,
13675
- resultCount: results.length,
13676
- results,
13677
- durationMs: Date.now() - startMs
13678
- };
13679
- } finally {
13680
- await this.driver.close();
13681
- }
13682
- }
13683
- async detectBlock(page) {
13684
13812
  return page.evaluate(() => {
13685
- const text2 = document.body.innerText.slice(0, 2e3);
13686
- return /unusual traffic|captcha|recaptcha|about this page/i.test(text2) || /\/sorry\//.test(location.href);
13687
- });
13688
- }
13689
- async collectResults(page, maxResults) {
13690
- const seen = /* @__PURE__ */ new Map();
13691
- const started = Date.now();
13692
- let noGrowthRounds = 0;
13693
- while (Date.now() - started < MAPS_SEARCH_SCROLL_BUDGET_MS) {
13694
- const before = seen.size;
13695
- const batch = await this.extractVisibleResults(page);
13696
- for (const result of batch) {
13697
- const key = this.resultKey(result);
13698
- if (!seen.has(key)) seen.set(key, { ...result, position: seen.size + 1 });
13699
- if (seen.size >= maxResults) break;
13700
- }
13701
- if (seen.size >= maxResults) break;
13702
- if (seen.size === before) noGrowthRounds += 1;
13703
- else noGrowthRounds = 0;
13704
- if (noGrowthRounds >= MAPS_SEARCH_MAX_NO_GROWTH_ROUNDS) break;
13705
- await page.evaluate(() => {
13706
- const feed = document.querySelector('[role="feed"]');
13707
- if (feed) {
13708
- feed.scrollTop = feed.scrollHeight;
13709
- } else {
13710
- window.scrollTo(0, document.body.scrollHeight);
13711
- }
13712
- });
13713
- await page.waitForTimeout(MAPS_SEARCH_SCROLL_STEP_MS);
13714
- }
13715
- return [...seen.values()].slice(0, maxResults);
13716
- }
13717
- resultKey(result) {
13718
- return result.cidDecimal ?? result.placeUrl.replace(/[?&].*$/, "") ?? result.name;
13719
- }
13720
- async extractVisibleResults(page) {
13721
- return page.evaluate(() => {
13722
- function normalizeText(value) {
13723
- const text2 = value?.replace(/\s+/g, " ").trim() ?? "";
13724
- return text2 || null;
13725
- }
13726
- function cidFromUrl(url) {
13727
- const fid = url.match(/!1s(0x[0-9a-f]+):(0x[0-9a-f]+)/i);
13728
- if (!fid) return { cid: null, cidDecimal: null };
13729
- let cidDecimal = null;
13730
- try {
13731
- cidDecimal = BigInt(fid[2]).toString();
13732
- } catch {
13733
- }
13734
- return { cid: `${fid[1]}:${fid[2]}`, cidDecimal };
13735
- }
13736
- function textParts(card) {
13737
- if (!card) return [];
13738
- const parts = [];
13739
- card.querySelectorAll("div, span").forEach((el2) => {
13740
- const text2 = Array.from(el2.childNodes).filter((node) => node.nodeType === 3).map((node) => node.textContent?.trim() ?? "").filter((text3) => text3.length > 1 && text3.length < 140).join(" ");
13741
- if (text2 && !parts.includes(text2)) parts.push(text2);
13742
- });
13743
- return parts;
13744
- }
13745
- function firstMatching(parts, pattern) {
13746
- const value = parts.find((part) => pattern.test(part));
13747
- return value ?? null;
13748
- }
13749
- function normalizedSet(values) {
13750
- return new Set(values.filter(Boolean).map((value) => value.toLowerCase()));
13751
- }
13752
- const out = [];
13753
- const seen = /* @__PURE__ */ new Set();
13754
- const anchors = Array.from(document.querySelectorAll('a[href*="/maps/place/"]'));
13755
- for (const anchor of anchors) {
13756
- const placeUrl = anchor.href;
13757
- const stableUrl = placeUrl.replace(/[?&].*$/, "");
13758
- if (seen.has(stableUrl)) continue;
13759
- seen.add(stableUrl);
13760
- const card = anchor.closest('.Nv2PK, [role="article"], .bfdHYd') ?? anchor.parentElement;
13761
- const parts = textParts(card);
13762
- const aria = normalizeText(anchor.getAttribute("aria-label"));
13763
- const heading = normalizeText(card?.querySelector('.qBF1Pd, .fontHeadlineSmall, [role="heading"]')?.textContent);
13764
- const name = aria ?? heading ?? parts[0] ?? stableUrl;
13765
- const links = Array.from(card?.querySelectorAll("a[href]") ?? []);
13766
- const websiteUrl = links.find((link) => link.href.startsWith("http") && !link.href.includes("google."))?.href ?? null;
13767
- const rating = firstMatching(parts, /^\d(?:\.\d)?$/);
13768
- const reviewCountRaw = firstMatching(parts, /^\(?[\d,]+\)?(?:\s+reviews?)?$/i);
13769
- const phone = firstMatching(parts, /(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/);
13770
- const hoursStatus = parts.find((part) => /^(open|closed|closes|opens)\b|^·\s*(opens|closes)\b/i.test(part)) ?? null;
13771
- const address = parts.find((part) => /\b[A-Z]{2}\s+\d{5}\b|\b(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Drive|Dr)\b/i.test(part)) ?? null;
13772
- const directionsUrl = links.find((link) => /google\.[^/]+\/maps\/dir|\/dir\//i.test(link.href))?.href ?? `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", ") || name)}`;
13773
- const excluded = normalizedSet([name, rating, reviewCountRaw, phone, hoursStatus, address, "Website", "Directions"]);
13774
- const category = parts.find((part) => {
13775
- const normalized = part.toLowerCase();
13776
- return !excluded.has(normalized) && !/^\d(?:\.\d)?$|^\(?[\d,]+\)?(?:\s+reviews?)?$/i.test(part) && !/(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/.test(part) && !/\b[A-Z]{2}\s+\d{5}\b|\b(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Drive|Dr)\b/i.test(part) && !/^(open|closed|closes|opens)\b|^·\s*(opens|closes)\b|directions|website|book online|sponsored|visit site|financing/i.test(part);
13777
- }) ?? null;
13778
- const { cid, cidDecimal } = cidFromUrl(placeUrl);
13779
- out.push({
13780
- position: out.length + 1,
13781
- name,
13782
- placeUrl,
13783
- cid,
13784
- cidDecimal,
13785
- rating,
13786
- reviewCount: reviewCountRaw ? reviewCountRaw.replace(/[()]/g, "") : null,
13787
- category,
13788
- address,
13789
- phone,
13790
- hoursStatus,
13791
- websiteUrl,
13792
- directionsUrl,
13793
- metadata: parts.slice(0, 20)
13794
- });
13795
- }
13796
- return out;
13813
+ const visible = Array.from(document.querySelectorAll('dialog, [role="dialog"]')).find((dialog) => dialog.offsetParent !== null);
13814
+ if (!visible) return { services: [], areasServed: [] };
13815
+ const values = Array.from(visible.querySelectorAll("button")).map((button) => button.innerText.replace(/\s+/g, " ").trim()).filter(Boolean);
13816
+ const servicesText = values.find((value) => /^services\s*:/i.test(value));
13817
+ const services = servicesText ? [...new Set(servicesText.replace(/^services\s*:\s*/i, "").split(/\s*[,·]\s*/).map((value) => value.trim()).filter(Boolean))] : [];
13818
+ const served = values.find((value) => /^serves\b/i.test(value));
13819
+ const areasServed = served ? [served.replace(/^serves\s*/i, "").trim()].filter(Boolean) : [];
13820
+ return { services, areasServed };
13797
13821
  });
13798
13822
  }
13799
13823
  };
@@ -14710,6 +14734,7 @@ async function searchCity(options, market) {
14710
14734
  gl: options.gl,
14711
14735
  hl: options.hl,
14712
14736
  maxResults: options.maxResultsPerCity,
14737
+ includeServices: false,
14713
14738
  headless: options.headless,
14714
14739
  kernelApiKey: options.kernelApiKey,
14715
14740
  proxyMode: options.proxyMode,
@@ -16515,6 +16540,42 @@ serpIntelligenceApp.post("/page-snapshots", async (c) => {
16515
16540
  import { Hono as Hono17 } from "hono";
16516
16541
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
16517
16542
 
16543
+ // src/mcp/memory-mcp-tool-executor.ts
16544
+ var MemoryMcpToolExecutor = class {
16545
+ baseUrl;
16546
+ apiKey;
16547
+ constructor(baseUrl, apiKey) {
16548
+ this.baseUrl = baseUrl.replace(/\/$/, "");
16549
+ this.apiKey = apiKey;
16550
+ }
16551
+ async callMemoryTool(toolName, args) {
16552
+ try {
16553
+ const res = await fetch(`${this.baseUrl}/memory/mcp-call`, {
16554
+ method: "POST",
16555
+ headers: {
16556
+ "content-type": "application/json",
16557
+ "x-api-key": this.apiKey
16558
+ },
16559
+ body: JSON.stringify({ toolName, args })
16560
+ });
16561
+ const data = await res.json().catch(() => null);
16562
+ if (!res.ok) {
16563
+ const message = data?.error ?? `memory ${toolName} failed (HTTP ${res.status})`;
16564
+ return { content: [{ type: "text", text: message }], isError: true };
16565
+ }
16566
+ const result = data ?? { ok: false, error: `memory ${toolName} returned no result` };
16567
+ return {
16568
+ content: [{ type: "text", text: JSON.stringify(result) }],
16569
+ structuredContent: result,
16570
+ isError: false
16571
+ };
16572
+ } catch (err) {
16573
+ const message = err instanceof Error ? err.message : `memory ${toolName} call failed`;
16574
+ return { content: [{ type: "text", text: message }], isError: true };
16575
+ }
16576
+ }
16577
+ };
16578
+
16518
16579
  // src/mcp/memory-inprocess-executor.ts
16519
16580
  var CUTOVER = /* @__PURE__ */ new Map();
16520
16581
  function registerCutover(upstreamName, handler) {
@@ -26943,7 +27004,7 @@ app.get("/cron/tick", async (c) => {
26943
27004
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
26944
27005
  return c.json({ error: "Unauthorized" }, 401);
26945
27006
  }
26946
- const { drainQueue } = await import("./worker-E3KHQH2F.js");
27007
+ const { drainQueue } = await import("./worker-RYP5EOV5.js");
26947
27008
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
26948
27009
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
26949
27010
  const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup] = await Promise.all([
@@ -27145,4 +27206,4 @@ app.get("/blog/:slug/", (c) => {
27145
27206
  export {
27146
27207
  app
27147
27208
  };
27148
- //# sourceMappingURL=server-K6ODZOZA.js.map
27209
+ //# sourceMappingURL=server-YXVSZ3T7.js.map