mcp-scraper 0.26.3 → 0.28.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.
- package/README.md +4 -4
- package/dist/bin/api-server.cjs +525 -283
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +3 -2
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +3 -3
- package/dist/bin/mcp-scraper-install.cjs +2 -2
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +62 -10
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +4 -4
- package/dist/bin/paa-harvest.cjs +2 -1
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +2 -2
- package/dist/{chunk-TBRX7VOP.js → chunk-DUS4ENOY.js} +2 -2
- package/dist/{chunk-TBRX7VOP.js.map → chunk-DUS4ENOY.js.map} +1 -1
- package/dist/{chunk-FUWZWKGO.js → chunk-KAMKTSJQ.js} +2 -2
- package/dist/{chunk-73YM3DEB.js → chunk-LTT7QX3C.js} +61 -10
- package/dist/chunk-LTT7QX3C.js.map +1 -0
- package/dist/chunk-QG77M3YJ.js +7 -0
- package/dist/chunk-QG77M3YJ.js.map +1 -0
- package/dist/{chunk-E5J4HJBO.js → chunk-YH5DRFWQ.js} +4 -2
- package/dist/{chunk-XGIPATLV.js → chunk-ZQSHKWV4.js} +3 -2
- package/dist/chunk-ZQSHKWV4.js.map +1 -0
- package/dist/index.cjs +2 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{server-GN34MGAE.js → server-QQB4BNAB.js} +449 -267
- package/dist/server-QQB4BNAB.js.map +1 -0
- package/dist/{worker-E3KHQH2F.js → worker-RYP5EOV5.js} +3 -3
- package/docs/mcp-tool-manifest.generated.json +128 -9
- package/package.json +2 -2
- package/dist/chunk-27VXTOD2.js +0 -7
- package/dist/chunk-27VXTOD2.js.map +0 -1
- package/dist/chunk-73YM3DEB.js.map +0 -1
- package/dist/chunk-XGIPATLV.js.map +0 -1
- package/dist/server-GN34MGAE.js.map +0 -1
- /package/dist/{chunk-FUWZWKGO.js.map → chunk-KAMKTSJQ.js.map} +0 -0
- /package/dist/{chunk-E5J4HJBO.js.map → chunk-YH5DRFWQ.js.map} +0 -0
- /package/dist/{worker-E3KHQH2F.js.map → worker-RYP5EOV5.js.map} +0 -0
|
@@ -12,9 +12,11 @@ import {
|
|
|
12
12
|
MapsSelectors,
|
|
13
13
|
buildYouTubeChannelVideosUrl,
|
|
14
14
|
deleteKernelProxyId,
|
|
15
|
+
encodeUule,
|
|
15
16
|
harvest,
|
|
17
|
+
normalizeLocation,
|
|
16
18
|
resolveKernelProxyId
|
|
17
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-YH5DRFWQ.js";
|
|
18
20
|
import {
|
|
19
21
|
HttpMcpToolExecutor,
|
|
20
22
|
assessTranscriptSignal,
|
|
@@ -34,7 +36,7 @@ import {
|
|
|
34
36
|
sanitizeAttempts,
|
|
35
37
|
sanitizeHarvestResult,
|
|
36
38
|
transcribeMediaUrl
|
|
37
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-LTT7QX3C.js";
|
|
38
40
|
import {
|
|
39
41
|
auditImages,
|
|
40
42
|
buildLinkReport,
|
|
@@ -63,7 +65,7 @@ import {
|
|
|
63
65
|
workflowOutputBaseDir,
|
|
64
66
|
workflowStepCount,
|
|
65
67
|
workflowSupportsSteps
|
|
66
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-KAMKTSJQ.js";
|
|
67
69
|
import "./chunk-K443GQY5.js";
|
|
68
70
|
import {
|
|
69
71
|
DEFAULT_MAPS_PROXY_MODE,
|
|
@@ -74,8 +76,8 @@ import {
|
|
|
74
76
|
RawMapsHoursRowSchema,
|
|
75
77
|
RawMapsOverviewSchema,
|
|
76
78
|
RawMapsReviewStatsSchema
|
|
77
|
-
} from "./chunk-
|
|
78
|
-
import "./chunk-
|
|
79
|
+
} from "./chunk-ZQSHKWV4.js";
|
|
80
|
+
import "./chunk-QG77M3YJ.js";
|
|
79
81
|
import {
|
|
80
82
|
completeExtractJob,
|
|
81
83
|
countSuccessfulPages,
|
|
@@ -5061,7 +5063,7 @@ async function extractSite(opts) {
|
|
|
5061
5063
|
}
|
|
5062
5064
|
|
|
5063
5065
|
// src/api/server.ts
|
|
5064
|
-
import { Hono as
|
|
5066
|
+
import { Hono as Hono25 } from "hono";
|
|
5065
5067
|
import { serve as serveInngest } from "inngest/hono";
|
|
5066
5068
|
|
|
5067
5069
|
// src/inngest/client.ts
|
|
@@ -13077,6 +13079,262 @@ videoApp.post("/status", createApiKeyAuth(), async (c) => {
|
|
|
13077
13079
|
// src/api/maps-routes.ts
|
|
13078
13080
|
import { Hono as Hono11 } from "hono";
|
|
13079
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
|
+
|
|
13080
13338
|
// src/extractor/MapsNavigator.ts
|
|
13081
13339
|
var MapsNavigator = class {
|
|
13082
13340
|
constructor(page) {
|
|
@@ -13135,32 +13393,26 @@ var MapsNavigator = class {
|
|
|
13135
13393
|
await this.page.waitForSelector('[role="main"] h2', { timeout: 5e3 }).catch(() => {
|
|
13136
13394
|
});
|
|
13137
13395
|
}
|
|
13138
|
-
async navigateToSerpBusinessPanel(businessName, location2) {
|
|
13139
|
-
const
|
|
13140
|
-
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 });
|
|
13141
13398
|
await this.page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch(() => {
|
|
13142
13399
|
});
|
|
13143
|
-
await this.page.waitForTimeout(
|
|
13400
|
+
await this.page.waitForTimeout(1e3);
|
|
13144
13401
|
const clicked = await this.page.evaluate((name) => {
|
|
13145
|
-
const
|
|
13146
|
-
const
|
|
13147
|
-
|
|
13148
|
-
|
|
13149
|
-
|
|
13150
|
-
|
|
13151
|
-
if (clickable) {
|
|
13152
|
-
clickable.click();
|
|
13153
|
-
return true;
|
|
13154
|
-
}
|
|
13155
|
-
if (heading) {
|
|
13156
|
-
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();
|
|
13157
13408
|
return true;
|
|
13158
13409
|
}
|
|
13159
13410
|
return false;
|
|
13160
13411
|
}, businessName);
|
|
13161
13412
|
if (!clicked) return false;
|
|
13162
|
-
await this.page.
|
|
13163
|
-
|
|
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);
|
|
13164
13416
|
}
|
|
13165
13417
|
buildReviewsUrl(placeUrl) {
|
|
13166
13418
|
const fid = placeUrl.match(/!1s(0x[0-9a-f]+:0x[0-9a-f]+)/i);
|
|
@@ -13336,7 +13588,7 @@ var MapsExtractor = class {
|
|
|
13336
13588
|
let servicesStatus = "not_requested";
|
|
13337
13589
|
if (options.includeServices) {
|
|
13338
13590
|
try {
|
|
13339
|
-
const opened = await navigator.navigateToSerpBusinessPanel(options.businessName, options.location);
|
|
13591
|
+
const opened = await navigator.navigateToSerpBusinessPanel(options.businessName, options.location, options.gl, options.hl);
|
|
13340
13592
|
if (opened) {
|
|
13341
13593
|
const result = await this.extractServicesAndAreasServed(page);
|
|
13342
13594
|
services = result.services;
|
|
@@ -13557,242 +13809,15 @@ var MapsExtractor = class {
|
|
|
13557
13809
|
return result.data;
|
|
13558
13810
|
}
|
|
13559
13811
|
async extractServicesAndAreasServed(page) {
|
|
13560
|
-
await page.waitForTimeout(1500);
|
|
13561
|
-
let services = [];
|
|
13562
|
-
if (await this.clickRowStartingWithRetry(page, "Services:")) {
|
|
13563
|
-
await page.waitForSelector('[role="dialog"]', { timeout: 5e3 }).catch(() => {
|
|
13564
|
-
});
|
|
13565
|
-
await page.waitForTimeout(800);
|
|
13566
|
-
services = await this.extractOpenDialogItems(page, "Services");
|
|
13567
|
-
await this.closeOpenDialog(page, "Services");
|
|
13568
|
-
await page.waitForTimeout(500);
|
|
13569
|
-
}
|
|
13570
|
-
let areasServed = [];
|
|
13571
|
-
if (await this.clickRowStartingWithRetry(page, "Serves ")) {
|
|
13572
|
-
await page.waitForSelector('[role="dialog"]', { timeout: 5e3 }).catch(() => {
|
|
13573
|
-
});
|
|
13574
|
-
await page.waitForTimeout(800);
|
|
13575
|
-
areasServed = await this.extractOpenDialogItems(page, "Areas Served");
|
|
13576
|
-
await this.closeOpenDialog(page, "Areas Served");
|
|
13577
|
-
}
|
|
13578
|
-
return { services, areasServed };
|
|
13579
|
-
}
|
|
13580
|
-
async clickRowStartingWithRetry(page, prefix) {
|
|
13581
|
-
if (await this.clickRowStartingWith(page, prefix)) return true;
|
|
13582
|
-
await page.waitForTimeout(2e3);
|
|
13583
|
-
return this.clickRowStartingWith(page, prefix);
|
|
13584
|
-
}
|
|
13585
|
-
async clickRowStartingWith(page, prefix) {
|
|
13586
|
-
return page.evaluate((p) => {
|
|
13587
|
-
const el2 = Array.from(document.querySelectorAll('button, a, div[role="button"]')).find(
|
|
13588
|
-
(e) => (e.innerText ?? "").trim().startsWith(p)
|
|
13589
|
-
);
|
|
13590
|
-
if (!el2) return false;
|
|
13591
|
-
el2.click();
|
|
13592
|
-
return true;
|
|
13593
|
-
}, prefix);
|
|
13594
|
-
}
|
|
13595
|
-
async extractOpenDialogItems(page, expectedHeading) {
|
|
13596
|
-
return page.evaluate((expected) => {
|
|
13597
|
-
const dialogs = Array.from(document.querySelectorAll('[role="dialog"]'));
|
|
13598
|
-
const withHeading = dialogs.find((d) => {
|
|
13599
|
-
const h = d.querySelector('h1, h2, [role="heading"]');
|
|
13600
|
-
const headingText = h?.innerText?.trim() ?? "";
|
|
13601
|
-
return headingText.startsWith(expected);
|
|
13602
|
-
});
|
|
13603
|
-
if (!withHeading) return [];
|
|
13604
|
-
const headingEl = withHeading.querySelector('h1, h2, [role="heading"]');
|
|
13605
|
-
const headingLines = new Set(
|
|
13606
|
-
(headingEl?.innerText ?? "").split("\n").map((s) => s.trim()).filter(Boolean)
|
|
13607
|
-
);
|
|
13608
|
-
const seen = /* @__PURE__ */ new Set();
|
|
13609
|
-
const items = [];
|
|
13610
|
-
withHeading.querySelectorAll("*").forEach((el2) => {
|
|
13611
|
-
if (el2.children.length > 0) return;
|
|
13612
|
-
const text2 = el2.innerText?.trim() ?? el2.textContent?.trim() ?? "";
|
|
13613
|
-
if (!text2 || headingLines.has(text2)) return;
|
|
13614
|
-
if (/^(back|close|done|thanks)$/i.test(text2)) return;
|
|
13615
|
-
if (/^serves\s.*following/i.test(text2)) return;
|
|
13616
|
-
if (seen.has(text2)) return;
|
|
13617
|
-
seen.add(text2);
|
|
13618
|
-
items.push(text2);
|
|
13619
|
-
});
|
|
13620
|
-
return items;
|
|
13621
|
-
}, expectedHeading);
|
|
13622
|
-
}
|
|
13623
|
-
async closeOpenDialog(page, expectedHeading) {
|
|
13624
|
-
await page.evaluate((expected) => {
|
|
13625
|
-
const dialog = Array.from(document.querySelectorAll('[role="dialog"]')).find((d) => {
|
|
13626
|
-
const h = d.querySelector('h1, h2, [role="heading"]');
|
|
13627
|
-
const headingText = h?.innerText?.trim() ?? "";
|
|
13628
|
-
return headingText.startsWith(expected);
|
|
13629
|
-
});
|
|
13630
|
-
const closeBtn = dialog?.querySelector('[aria-label*="Close" i], button[aria-label*="close" i]');
|
|
13631
|
-
closeBtn?.click();
|
|
13632
|
-
}, expectedHeading);
|
|
13633
|
-
}
|
|
13634
|
-
};
|
|
13635
|
-
|
|
13636
|
-
// src/extractor/MapsSearchExtractor.ts
|
|
13637
|
-
var MAPS_SEARCH_SCROLL_BUDGET_MS = 6e4;
|
|
13638
|
-
var MAPS_SEARCH_SCROLL_STEP_MS = 1200;
|
|
13639
|
-
var MAPS_SEARCH_MAX_NO_GROWTH_ROUNDS = 4;
|
|
13640
|
-
var MapsSearchExtractor = class {
|
|
13641
|
-
constructor(driver) {
|
|
13642
|
-
this.driver = driver;
|
|
13643
|
-
}
|
|
13644
|
-
driver;
|
|
13645
|
-
async extract(options) {
|
|
13646
|
-
const startMs = Date.now();
|
|
13647
|
-
const searchQuery = [options.query, options.location].filter(Boolean).join(" ");
|
|
13648
|
-
const searchUrl = `https://www.google.com/maps/search/${encodeURIComponent(searchQuery)}?hl=${encodeURIComponent(options.hl)}`;
|
|
13649
|
-
const config = {
|
|
13650
|
-
headless: options.headless,
|
|
13651
|
-
kernelApiKey: options.kernelApiKey,
|
|
13652
|
-
kernelProxyId: options.kernelProxyId,
|
|
13653
|
-
kernelProxyResolution: options.kernelProxyResolution,
|
|
13654
|
-
proxyMode: options.proxyMode,
|
|
13655
|
-
viewport: { width: 1280, height: 900 },
|
|
13656
|
-
locale: `${options.hl}-${options.gl.toUpperCase()}`,
|
|
13657
|
-
debug: options.debug
|
|
13658
|
-
};
|
|
13659
|
-
try {
|
|
13660
|
-
await this.driver.launch(config);
|
|
13661
|
-
const page = this.driver.getPage();
|
|
13662
|
-
await page.goto(searchUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
13663
|
-
await page.waitForTimeout(3e3);
|
|
13664
|
-
const blocked = await this.detectBlock(page);
|
|
13665
|
-
if (blocked) throw new CaptchaError(RECAPTCHA_INSTRUCTIONS);
|
|
13666
|
-
const results = await this.collectResults(page, options.maxResults);
|
|
13667
|
-
return {
|
|
13668
|
-
query: options.query,
|
|
13669
|
-
location: options.location ?? null,
|
|
13670
|
-
searchQuery,
|
|
13671
|
-
searchUrl,
|
|
13672
|
-
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13673
|
-
requestedMaxResults: options.maxResults,
|
|
13674
|
-
resultCount: results.length,
|
|
13675
|
-
results,
|
|
13676
|
-
durationMs: Date.now() - startMs
|
|
13677
|
-
};
|
|
13678
|
-
} finally {
|
|
13679
|
-
await this.driver.close();
|
|
13680
|
-
}
|
|
13681
|
-
}
|
|
13682
|
-
async detectBlock(page) {
|
|
13683
13812
|
return page.evaluate(() => {
|
|
13684
|
-
const
|
|
13685
|
-
return
|
|
13686
|
-
|
|
13687
|
-
|
|
13688
|
-
|
|
13689
|
-
|
|
13690
|
-
|
|
13691
|
-
|
|
13692
|
-
while (Date.now() - started < MAPS_SEARCH_SCROLL_BUDGET_MS) {
|
|
13693
|
-
const before = seen.size;
|
|
13694
|
-
const batch = await this.extractVisibleResults(page);
|
|
13695
|
-
for (const result of batch) {
|
|
13696
|
-
const key = this.resultKey(result);
|
|
13697
|
-
if (!seen.has(key)) seen.set(key, { ...result, position: seen.size + 1 });
|
|
13698
|
-
if (seen.size >= maxResults) break;
|
|
13699
|
-
}
|
|
13700
|
-
if (seen.size >= maxResults) break;
|
|
13701
|
-
if (seen.size === before) noGrowthRounds += 1;
|
|
13702
|
-
else noGrowthRounds = 0;
|
|
13703
|
-
if (noGrowthRounds >= MAPS_SEARCH_MAX_NO_GROWTH_ROUNDS) break;
|
|
13704
|
-
await page.evaluate(() => {
|
|
13705
|
-
const feed = document.querySelector('[role="feed"]');
|
|
13706
|
-
if (feed) {
|
|
13707
|
-
feed.scrollTop = feed.scrollHeight;
|
|
13708
|
-
} else {
|
|
13709
|
-
window.scrollTo(0, document.body.scrollHeight);
|
|
13710
|
-
}
|
|
13711
|
-
});
|
|
13712
|
-
await page.waitForTimeout(MAPS_SEARCH_SCROLL_STEP_MS);
|
|
13713
|
-
}
|
|
13714
|
-
return [...seen.values()].slice(0, maxResults);
|
|
13715
|
-
}
|
|
13716
|
-
resultKey(result) {
|
|
13717
|
-
return result.cidDecimal ?? result.placeUrl.replace(/[?&].*$/, "") ?? result.name;
|
|
13718
|
-
}
|
|
13719
|
-
async extractVisibleResults(page) {
|
|
13720
|
-
return page.evaluate(() => {
|
|
13721
|
-
function normalizeText(value) {
|
|
13722
|
-
const text2 = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
13723
|
-
return text2 || null;
|
|
13724
|
-
}
|
|
13725
|
-
function cidFromUrl(url) {
|
|
13726
|
-
const fid = url.match(/!1s(0x[0-9a-f]+):(0x[0-9a-f]+)/i);
|
|
13727
|
-
if (!fid) return { cid: null, cidDecimal: null };
|
|
13728
|
-
let cidDecimal = null;
|
|
13729
|
-
try {
|
|
13730
|
-
cidDecimal = BigInt(fid[2]).toString();
|
|
13731
|
-
} catch {
|
|
13732
|
-
}
|
|
13733
|
-
return { cid: `${fid[1]}:${fid[2]}`, cidDecimal };
|
|
13734
|
-
}
|
|
13735
|
-
function textParts(card) {
|
|
13736
|
-
if (!card) return [];
|
|
13737
|
-
const parts = [];
|
|
13738
|
-
card.querySelectorAll("div, span").forEach((el2) => {
|
|
13739
|
-
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(" ");
|
|
13740
|
-
if (text2 && !parts.includes(text2)) parts.push(text2);
|
|
13741
|
-
});
|
|
13742
|
-
return parts;
|
|
13743
|
-
}
|
|
13744
|
-
function firstMatching(parts, pattern) {
|
|
13745
|
-
const value = parts.find((part) => pattern.test(part));
|
|
13746
|
-
return value ?? null;
|
|
13747
|
-
}
|
|
13748
|
-
function normalizedSet(values) {
|
|
13749
|
-
return new Set(values.filter(Boolean).map((value) => value.toLowerCase()));
|
|
13750
|
-
}
|
|
13751
|
-
const out = [];
|
|
13752
|
-
const seen = /* @__PURE__ */ new Set();
|
|
13753
|
-
const anchors = Array.from(document.querySelectorAll('a[href*="/maps/place/"]'));
|
|
13754
|
-
for (const anchor of anchors) {
|
|
13755
|
-
const placeUrl = anchor.href;
|
|
13756
|
-
const stableUrl = placeUrl.replace(/[?&].*$/, "");
|
|
13757
|
-
if (seen.has(stableUrl)) continue;
|
|
13758
|
-
seen.add(stableUrl);
|
|
13759
|
-
const card = anchor.closest('.Nv2PK, [role="article"], .bfdHYd') ?? anchor.parentElement;
|
|
13760
|
-
const parts = textParts(card);
|
|
13761
|
-
const aria = normalizeText(anchor.getAttribute("aria-label"));
|
|
13762
|
-
const heading = normalizeText(card?.querySelector('.qBF1Pd, .fontHeadlineSmall, [role="heading"]')?.textContent);
|
|
13763
|
-
const name = aria ?? heading ?? parts[0] ?? stableUrl;
|
|
13764
|
-
const links = Array.from(card?.querySelectorAll("a[href]") ?? []);
|
|
13765
|
-
const websiteUrl = links.find((link) => link.href.startsWith("http") && !link.href.includes("google."))?.href ?? null;
|
|
13766
|
-
const rating = firstMatching(parts, /^\d(?:\.\d)?$/);
|
|
13767
|
-
const reviewCountRaw = firstMatching(parts, /^\(?[\d,]+\)?(?:\s+reviews?)?$/i);
|
|
13768
|
-
const phone = firstMatching(parts, /(?:\+?1[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}/);
|
|
13769
|
-
const hoursStatus = parts.find((part) => /^(open|closed|closes|opens)\b|^·\s*(opens|closes)\b/i.test(part)) ?? null;
|
|
13770
|
-
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;
|
|
13771
|
-
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)}`;
|
|
13772
|
-
const excluded = normalizedSet([name, rating, reviewCountRaw, phone, hoursStatus, address, "Website", "Directions"]);
|
|
13773
|
-
const category = parts.find((part) => {
|
|
13774
|
-
const normalized = part.toLowerCase();
|
|
13775
|
-
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);
|
|
13776
|
-
}) ?? null;
|
|
13777
|
-
const { cid, cidDecimal } = cidFromUrl(placeUrl);
|
|
13778
|
-
out.push({
|
|
13779
|
-
position: out.length + 1,
|
|
13780
|
-
name,
|
|
13781
|
-
placeUrl,
|
|
13782
|
-
cid,
|
|
13783
|
-
cidDecimal,
|
|
13784
|
-
rating,
|
|
13785
|
-
reviewCount: reviewCountRaw ? reviewCountRaw.replace(/[()]/g, "") : null,
|
|
13786
|
-
category,
|
|
13787
|
-
address,
|
|
13788
|
-
phone,
|
|
13789
|
-
hoursStatus,
|
|
13790
|
-
websiteUrl,
|
|
13791
|
-
directionsUrl,
|
|
13792
|
-
metadata: parts.slice(0, 20)
|
|
13793
|
-
});
|
|
13794
|
-
}
|
|
13795
|
-
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 };
|
|
13796
13821
|
});
|
|
13797
13822
|
}
|
|
13798
13823
|
};
|
|
@@ -14709,6 +14734,7 @@ async function searchCity(options, market) {
|
|
|
14709
14734
|
gl: options.gl,
|
|
14710
14735
|
hl: options.hl,
|
|
14711
14736
|
maxResults: options.maxResultsPerCity,
|
|
14737
|
+
includeServices: false,
|
|
14712
14738
|
headless: options.headless,
|
|
14713
14739
|
kernelApiKey: options.kernelApiKey,
|
|
14714
14740
|
proxyMode: options.proxyMode,
|
|
@@ -16963,6 +16989,8 @@ import { setScopeTool } from "mcpscraper-memory-tools/tools/access/set-scope";
|
|
|
16963
16989
|
import { inviteAccountTool } from "mcpscraper-memory-tools/tools/access/invite-account";
|
|
16964
16990
|
import { getChatLinkTool } from "mcpscraper-memory-tools/tools/access/get-chat-link";
|
|
16965
16991
|
import { revokeChatLinkTool } from "mcpscraper-memory-tools/tools/access/revoke-chat-link";
|
|
16992
|
+
import { getVaultAppLinkTool } from "mcpscraper-memory-tools/tools/access/get-vault-app-link";
|
|
16993
|
+
import { revokeVaultAppLinkTool } from "mcpscraper-memory-tools/tools/access/revoke-vault-app-link";
|
|
16966
16994
|
import { provisionDefaultsTool as provisionDefaultsTool2 } from "mcpscraper-memory-tools/tools/vaults/provision-defaults";
|
|
16967
16995
|
import { createSecureVaultTool } from "mcpscraper-memory-tools/tools/vaults/create-secure-vault";
|
|
16968
16996
|
import { createScheduledActionTool } from "mcpscraper-memory-tools/tools/schedule/create-scheduled-action";
|
|
@@ -17004,6 +17032,14 @@ function registerBillingCutovers() {
|
|
|
17004
17032
|
const result = await revokeChatLinkTool.execute({ ...input, apiKey }, {});
|
|
17005
17033
|
return { content: [{ type: "text", text: JSON.stringify(result ?? {}) }], structuredContent: result ?? {}, isError: false };
|
|
17006
17034
|
});
|
|
17035
|
+
registerCutover("getVaultAppLinkTool", async (input, apiKey) => {
|
|
17036
|
+
const result = await getVaultAppLinkTool.execute({ ...input, apiKey }, {});
|
|
17037
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? {}) }], structuredContent: result ?? {}, isError: false };
|
|
17038
|
+
});
|
|
17039
|
+
registerCutover("revokeVaultAppLinkTool", async (input, apiKey) => {
|
|
17040
|
+
const result = await revokeVaultAppLinkTool.execute({ ...input, apiKey }, {});
|
|
17041
|
+
return { content: [{ type: "text", text: JSON.stringify(result ?? {}) }], structuredContent: result ?? {}, isError: false };
|
|
17042
|
+
});
|
|
17007
17043
|
registerCutover("provisionDefaultsTool", async (input, apiKey) => {
|
|
17008
17044
|
const result = await provisionDefaultsTool2.execute({ ...input, apiKey }, {});
|
|
17009
17045
|
return { content: [{ type: "text", text: JSON.stringify(result ?? {}) }], structuredContent: result ?? {}, isError: false };
|
|
@@ -20660,9 +20696,154 @@ loadChannels();
|
|
|
20660
20696
|
</html>`;
|
|
20661
20697
|
}
|
|
20662
20698
|
|
|
20663
|
-
// src/api/
|
|
20699
|
+
// src/api/vault-routes.ts
|
|
20664
20700
|
import { Hono as Hono22 } from "hono";
|
|
20665
|
-
var
|
|
20701
|
+
var VAULTS = ["People", "Projects", "Tasks", "Communications"];
|
|
20702
|
+
var vaultApp = new Hono22();
|
|
20703
|
+
function validToken(token) {
|
|
20704
|
+
return !!token && token.startsWith("mk_") && token.length > 20 && token.length < 256;
|
|
20705
|
+
}
|
|
20706
|
+
function isVault(value) {
|
|
20707
|
+
return typeof value === "string" && VAULTS.includes(value);
|
|
20708
|
+
}
|
|
20709
|
+
function stringValue(value, max = 1e4) {
|
|
20710
|
+
if (typeof value !== "string") return null;
|
|
20711
|
+
const trimmed = value.trim();
|
|
20712
|
+
return trimmed && trimmed.length <= max ? trimmed : null;
|
|
20713
|
+
}
|
|
20714
|
+
function propsValue(value) {
|
|
20715
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
20716
|
+
const entries = Object.entries(value);
|
|
20717
|
+
if (entries.length > 80) return null;
|
|
20718
|
+
return Object.fromEntries(entries.filter(([key]) => key.length <= 96));
|
|
20719
|
+
}
|
|
20720
|
+
function cors(c) {
|
|
20721
|
+
const origin = c.req.header("origin");
|
|
20722
|
+
const configured = (process.env.VAULT_APP_ALLOWED_ORIGINS ?? "https://vault.mcpscraper.dev").split(",").map((value) => value.trim()).filter(Boolean);
|
|
20723
|
+
const local = !!origin && /^http:\/\/localhost(?::\d+)?$/.test(origin);
|
|
20724
|
+
if (origin && (configured.includes(origin) || local)) c.header("Access-Control-Allow-Origin", origin);
|
|
20725
|
+
c.header("Vary", "Origin");
|
|
20726
|
+
c.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
20727
|
+
c.header("Access-Control-Allow-Headers", "content-type");
|
|
20728
|
+
}
|
|
20729
|
+
vaultApp.use("/api/*", async (c, next) => {
|
|
20730
|
+
cors(c);
|
|
20731
|
+
if (c.req.method === "OPTIONS") return c.body(null, 204);
|
|
20732
|
+
await next();
|
|
20733
|
+
});
|
|
20734
|
+
function workspaceRecord(vault, note) {
|
|
20735
|
+
const props = note.props ?? {};
|
|
20736
|
+
return {
|
|
20737
|
+
id: `${vault}:${note.path}`,
|
|
20738
|
+
vault,
|
|
20739
|
+
path: note.path,
|
|
20740
|
+
title: note.title,
|
|
20741
|
+
content: note.content,
|
|
20742
|
+
type: typeof props.type === "string" ? props.type : defaultType(vault),
|
|
20743
|
+
status: typeof props.status === "string" ? props.status : defaultStatus(vault),
|
|
20744
|
+
props,
|
|
20745
|
+
revision: note.revision,
|
|
20746
|
+
updatedAt: note.updatedAt,
|
|
20747
|
+
capturedAt: note.capturedAt
|
|
20748
|
+
};
|
|
20749
|
+
}
|
|
20750
|
+
function defaultType(vault) {
|
|
20751
|
+
if (vault === "People") return "person";
|
|
20752
|
+
if (vault === "Projects") return "project";
|
|
20753
|
+
if (vault === "Tasks") return "task";
|
|
20754
|
+
return "activity";
|
|
20755
|
+
}
|
|
20756
|
+
function defaultStatus(vault) {
|
|
20757
|
+
if (vault === "People") return "active";
|
|
20758
|
+
if (vault === "Projects") return "active";
|
|
20759
|
+
if (vault === "Tasks") return "to_do";
|
|
20760
|
+
return "triaged";
|
|
20761
|
+
}
|
|
20762
|
+
async function readVault(vault, token) {
|
|
20763
|
+
const list = await memoryCall("listTool", { vault }, token);
|
|
20764
|
+
if (!list.ok) throw new Error(list.error ?? `unable to read ${vault}`);
|
|
20765
|
+
const noteRefs = (list.notes ?? []).slice(0, 200);
|
|
20766
|
+
const notes = await Promise.all(
|
|
20767
|
+
noteRefs.map(async ({ path: path5 }) => memoryCall("getTool", { vault, path: path5 }, token))
|
|
20768
|
+
);
|
|
20769
|
+
return notes.filter((result) => result.ok && !!result.note).map((result) => workspaceRecord(vault, result.note));
|
|
20770
|
+
}
|
|
20771
|
+
vaultApp.get("/api/workspace", async (c) => {
|
|
20772
|
+
const token = c.req.query("token");
|
|
20773
|
+
if (!validToken(token)) return c.json({ ok: false, error: "missing or malformed Vault App link" }, 401);
|
|
20774
|
+
try {
|
|
20775
|
+
const requested = c.req.query("vault");
|
|
20776
|
+
const vaults = isVault(requested) ? [requested] : VAULTS;
|
|
20777
|
+
const results = await Promise.all(vaults.map((vault) => readVault(vault, token)));
|
|
20778
|
+
const records = results.flat().sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
20779
|
+
return c.json({ ok: true, records, limited: results.some((result) => result.length >= 200) });
|
|
20780
|
+
} catch (error) {
|
|
20781
|
+
return c.json({ ok: false, error: error instanceof Error ? error.message : "unable to load vault workspace" }, 502);
|
|
20782
|
+
}
|
|
20783
|
+
});
|
|
20784
|
+
vaultApp.post("/api/records", async (c) => {
|
|
20785
|
+
const body = await c.req.json().catch(() => ({}));
|
|
20786
|
+
const token = typeof body.token === "string" ? body.token : void 0;
|
|
20787
|
+
if (!validToken(token)) return c.json({ ok: false, error: "missing or malformed Vault App link" }, 401);
|
|
20788
|
+
const vault = body.vault;
|
|
20789
|
+
const path5 = stringValue(body.path, 320);
|
|
20790
|
+
const title = stringValue(body.title, 320);
|
|
20791
|
+
const content = typeof body.content === "string" && body.content.length <= 1e5 ? body.content : "";
|
|
20792
|
+
const props = propsValue(body.props) ?? {};
|
|
20793
|
+
if (!isVault(vault) || !path5 || !title) return c.json({ ok: false, error: "vault, path, and title are required" }, 400);
|
|
20794
|
+
const baseRevision = typeof body.baseRevision === "number" && Number.isInteger(body.baseRevision) ? body.baseRevision : void 0;
|
|
20795
|
+
const result = await memoryCall("putTool", { vault, path: path5, title, content: content || title, props, ...baseRevision === void 0 ? {} : { baseRevision } }, token);
|
|
20796
|
+
return c.json(result, result.ok ? 200 : result.conflict ? 409 : 502);
|
|
20797
|
+
});
|
|
20798
|
+
vaultApp.post("/api/records/transition", async (c) => {
|
|
20799
|
+
const body = await c.req.json().catch(() => ({}));
|
|
20800
|
+
const token = typeof body.token === "string" ? body.token : void 0;
|
|
20801
|
+
if (!validToken(token)) return c.json({ ok: false, error: "missing or malformed Vault App link" }, 401);
|
|
20802
|
+
const vault = typeof body.vault === "string" ? body.vault : "";
|
|
20803
|
+
const path5 = typeof body.path === "string" ? body.path : "";
|
|
20804
|
+
const status = stringValue(body.status, 80);
|
|
20805
|
+
const baseRevision = typeof body.baseRevision === "number" && Number.isInteger(body.baseRevision) ? body.baseRevision : void 0;
|
|
20806
|
+
if (!isVault(vault) || !path5 || !status) return c.json({ ok: false, error: "valid vault, path, and status are required" }, 400);
|
|
20807
|
+
const current = await memoryCall("getTool", { vault, path: path5 }, token);
|
|
20808
|
+
if (!current.ok || !current.note) return c.json(current, current.ok ? 404 : 502);
|
|
20809
|
+
const result = await memoryCall("putTool", {
|
|
20810
|
+
vault,
|
|
20811
|
+
path: path5,
|
|
20812
|
+
title: current.note.title,
|
|
20813
|
+
content: current.note.content,
|
|
20814
|
+
props: { ...current.note.props ?? {}, status },
|
|
20815
|
+
baseRevision: baseRevision ?? current.note.revision
|
|
20816
|
+
}, token);
|
|
20817
|
+
return c.json(result, result.ok ? 200 : result.conflict ? 409 : 502);
|
|
20818
|
+
});
|
|
20819
|
+
vaultApp.post("/api/activity", async (c) => {
|
|
20820
|
+
const body = await c.req.json().catch(() => ({}));
|
|
20821
|
+
const token = typeof body.token === "string" ? body.token : void 0;
|
|
20822
|
+
if (!validToken(token)) return c.json({ ok: false, error: "missing or malformed Vault App link" }, 401);
|
|
20823
|
+
const title = stringValue(body.title, 320);
|
|
20824
|
+
const content = typeof body.content === "string" && body.content.length <= 2e4 ? body.content : "";
|
|
20825
|
+
const props = propsValue(body.props) ?? {};
|
|
20826
|
+
if (!title) return c.json({ ok: false, error: "title is required" }, 400);
|
|
20827
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
20828
|
+
const result = await memoryCall("putTool", {
|
|
20829
|
+
vault: "Communications",
|
|
20830
|
+
path: `Inbox/activity-${stamp}`,
|
|
20831
|
+
title,
|
|
20832
|
+
content: content || title,
|
|
20833
|
+
props: { status: "triaged", type: "decision", occurred_at: (/* @__PURE__ */ new Date()).toISOString(), ...props }
|
|
20834
|
+
}, token);
|
|
20835
|
+
return c.json(result, result.ok ? 200 : 502);
|
|
20836
|
+
});
|
|
20837
|
+
vaultApp.get("/:token", (c) => {
|
|
20838
|
+
const token = c.req.param("token");
|
|
20839
|
+
if (!validToken(token)) return c.html('<!doctype html><html><body style="font-family:system-ui;padding:40px"><h2>This Vault App link is not valid.</h2><p>Ask the AI to run get-vault-app-link again, or revoke-vault-app-link and issue a replacement.</p></body></html>', 401);
|
|
20840
|
+
const base = (process.env.VAULT_APP_BASE_URL ?? "https://vault.mcpscraper.dev").replace(/\/$/, "");
|
|
20841
|
+
return c.redirect(`${base}/vault/${encodeURIComponent(token)}`, 302);
|
|
20842
|
+
});
|
|
20843
|
+
|
|
20844
|
+
// src/api/schedule-routes.ts
|
|
20845
|
+
import { Hono as Hono23 } from "hono";
|
|
20846
|
+
var scheduleApp = new Hono23();
|
|
20666
20847
|
function tokenOk2(token) {
|
|
20667
20848
|
return !!token && token.startsWith("mk_") && token.length > 10;
|
|
20668
20849
|
}
|
|
@@ -20993,7 +21174,7 @@ loadActions();
|
|
|
20993
21174
|
}
|
|
20994
21175
|
|
|
20995
21176
|
// src/api/resend-inbound-routes.ts
|
|
20996
|
-
import { Hono as
|
|
21177
|
+
import { Hono as Hono24 } from "hono";
|
|
20997
21178
|
import { Resend } from "resend";
|
|
20998
21179
|
var SUPPORT_ADDRESS = "support@mcpscraper.dev";
|
|
20999
21180
|
var DMARC_ADDRESS = "dmarc@mcpscraper.dev";
|
|
@@ -21169,7 +21350,7 @@ async function forwardReceivedEmail(resend, email, destination, label) {
|
|
|
21169
21350
|
if (sent.error) throw new Error(`forward failed: ${sent.error.message}`);
|
|
21170
21351
|
return sent.data.id;
|
|
21171
21352
|
}
|
|
21172
|
-
var resendInboundApp = new
|
|
21353
|
+
var resendInboundApp = new Hono24();
|
|
21173
21354
|
resendInboundApp.post("/webhooks", async (c) => {
|
|
21174
21355
|
const apiKey = process.env.RESEND_API_KEY?.trim();
|
|
21175
21356
|
const webhookSecret = process.env.RESEND_WEBHOOK_SECRET?.trim();
|
|
@@ -24754,7 +24935,7 @@ var requirePaidSchedulingTier = createMiddleware3(async (c, next) => {
|
|
|
24754
24935
|
if (!hasPaidSchedulingAccess(user)) return c.json(paidPlanRequiredPayload(), 403);
|
|
24755
24936
|
return next();
|
|
24756
24937
|
});
|
|
24757
|
-
var app = new
|
|
24938
|
+
var app = new Hono25();
|
|
24758
24939
|
var STRIPE_API_VERSION2 = "2026-02-25.clover";
|
|
24759
24940
|
function requireStripeSecret() {
|
|
24760
24941
|
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
@@ -26978,7 +27159,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
26978
27159
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
26979
27160
|
return c.json({ error: "Unauthorized" }, 401);
|
|
26980
27161
|
}
|
|
26981
|
-
const { drainQueue } = await import("./worker-
|
|
27162
|
+
const { drainQueue } = await import("./worker-RYP5EOV5.js");
|
|
26982
27163
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
26983
27164
|
const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
|
|
26984
27165
|
const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup] = await Promise.all([
|
|
@@ -27049,6 +27230,7 @@ app.route("/serp-intelligence", serpIntelligenceApp);
|
|
|
27049
27230
|
app.route("/mcp", mcpApp);
|
|
27050
27231
|
app.route("/agent", buildBrowserAgentRoutes());
|
|
27051
27232
|
app.route("/chat", chatApp);
|
|
27233
|
+
app.route("/vault", vaultApp);
|
|
27052
27234
|
app.route("/schedule", scheduleApp);
|
|
27053
27235
|
app.route("/resend", resendInboundApp);
|
|
27054
27236
|
app.get("/console", (c) => c.html(renderConsoleHtml()));
|
|
@@ -27180,4 +27362,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
27180
27362
|
export {
|
|
27181
27363
|
app
|
|
27182
27364
|
};
|
|
27183
|
-
//# sourceMappingURL=server-
|
|
27365
|
+
//# sourceMappingURL=server-QQB4BNAB.js.map
|