mcp-scraper 0.3.17 → 0.3.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.
- package/README.md +26 -58
- package/dist/bin/api-server.cjs +286 -49
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +4 -4
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +5 -5
- package/dist/bin/mcp-scraper-cli.js.map +1 -1
- package/dist/bin/mcp-scraper-install.cjs +12 -12
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +3 -3
- package/dist/bin/mcp-scraper-install.js.map +1 -1
- package/dist/bin/mcp-stdio-server.cjs +2441 -785
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +1377 -9
- package/dist/bin/mcp-stdio-server.js.map +1 -1
- package/dist/bin/paa-harvest.js +2 -3
- package/dist/bin/paa-harvest.js.map +1 -1
- package/dist/{chunk-N33YEY6W.js → chunk-2XCNU22L.js} +210 -10
- package/dist/{chunk-N33YEY6W.js.map → chunk-2XCNU22L.js.map} +1 -1
- package/dist/chunk-C7ZNIXOA.js +7 -0
- package/dist/chunk-C7ZNIXOA.js.map +1 -0
- package/dist/{chunk-ACIUCZ27.js → chunk-DE7WP4UU.js} +11 -11
- package/dist/chunk-DE7WP4UU.js.map +1 -0
- package/dist/{chunk-ZAUMSBV3.js → chunk-HDPWLYK7.js} +6 -6
- package/dist/{chunk-4OPKIDON.js → chunk-IGOEAKC7.js} +4 -6
- package/dist/{chunk-4OPKIDON.js.map → chunk-IGOEAKC7.js.map} +1 -1
- package/dist/{chunk-M2S27J6Z.js → chunk-ZAP7MYVO.js} +26 -1
- package/dist/chunk-ZAP7MYVO.js.map +1 -0
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/{server-TKWY47JA.js → server-3M3XCVOT.js} +323 -158
- package/dist/server-3M3XCVOT.js.map +1 -0
- package/dist/{worker-2WVKKCC7.js → worker-J6I5XP2F.js} +9 -10
- package/dist/{worker-2WVKKCC7.js.map → worker-J6I5XP2F.js.map} +1 -1
- package/docs/mcp-tool-manifest.generated.json +28 -27
- package/package.json +3 -3
- package/dist/bin/browser-agent-stdio-server.cjs +0 -1523
- package/dist/bin/browser-agent-stdio-server.cjs.map +0 -1
- package/dist/bin/browser-agent-stdio-server.d.cts +0 -1
- package/dist/bin/browser-agent-stdio-server.d.ts +0 -1
- package/dist/bin/browser-agent-stdio-server.js +0 -43
- package/dist/bin/browser-agent-stdio-server.js.map +0 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs +0 -5718
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +0 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.d.cts +0 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.d.ts +0 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +0 -70
- package/dist/bin/mcp-scraper-combined-stdio-server.js.map +0 -1
- package/dist/chunk-3YGKXXUG.js +0 -133
- package/dist/chunk-3YGKXXUG.js.map +0 -1
- package/dist/chunk-ACIUCZ27.js.map +0 -1
- package/dist/chunk-M2S27J6Z.js.map +0 -1
- package/dist/chunk-PVWWY5NV.js +0 -7
- package/dist/chunk-PVWWY5NV.js.map +0 -1
- package/dist/chunk-UFD7N36R.js +0 -1332
- package/dist/chunk-UFD7N36R.js.map +0 -1
- package/dist/chunk-WN7PBKMV.js +0 -28
- package/dist/chunk-WN7PBKMV.js.map +0 -1
- package/dist/server-TKWY47JA.js.map +0 -1
- /package/dist/{chunk-ZAUMSBV3.js.map → chunk-HDPWLYK7.js.map} +0 -0
package/dist/bin/api-server.cjs
CHANGED
|
@@ -117,18 +117,26 @@ var init_errors = __esm({
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
// src/api/outbound-sanitize.ts
|
|
120
|
+
function redactVendorUrls(value) {
|
|
121
|
+
return value.replace(VENDOR_URL_RE, "[browser-service]");
|
|
122
|
+
}
|
|
120
123
|
function sanitizeOutboundDiagnostics(value, parentKey = "") {
|
|
121
124
|
if (typeof value === "string") {
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
let out = redactVendorUrls(value);
|
|
126
|
+
if (SANITIZED_VALUE_KEYS.test(parentKey) && /kernel/i.test(out)) {
|
|
127
|
+
out = sanitizeVendorName(out);
|
|
124
128
|
}
|
|
125
|
-
return
|
|
129
|
+
return out;
|
|
126
130
|
}
|
|
127
131
|
if (Array.isArray(value)) return value.map((v) => sanitizeOutboundDiagnostics(v, parentKey));
|
|
128
132
|
if (value !== null && typeof value === "object") {
|
|
129
133
|
const out = {};
|
|
130
134
|
for (const [key, val] of Object.entries(value)) {
|
|
131
135
|
const renamed = KEY_RENAMES[key] ?? key;
|
|
136
|
+
if (VENDOR_URL_KEYS.has(key)) {
|
|
137
|
+
out[renamed] = null;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
132
140
|
out[renamed] = sanitizeOutboundDiagnostics(val, key);
|
|
133
141
|
}
|
|
134
142
|
return out;
|
|
@@ -149,7 +157,7 @@ function sanitizeHarvestResult(result) {
|
|
|
149
157
|
}
|
|
150
158
|
};
|
|
151
159
|
}
|
|
152
|
-
var KEY_RENAMES, SANITIZED_VALUE_KEYS;
|
|
160
|
+
var KEY_RENAMES, SANITIZED_VALUE_KEYS, VENDOR_URL_KEYS, VENDOR_URL_RE;
|
|
153
161
|
var init_outbound_sanitize = __esm({
|
|
154
162
|
"src/api/outbound-sanitize.ts"() {
|
|
155
163
|
"use strict";
|
|
@@ -167,6 +175,16 @@ var init_outbound_sanitize = __esm({
|
|
|
167
175
|
kernelProxyId: "proxyId"
|
|
168
176
|
};
|
|
169
177
|
SANITIZED_VALUE_KEYS = /error|message/i;
|
|
178
|
+
VENDOR_URL_KEYS = /* @__PURE__ */ new Set([
|
|
179
|
+
"hosted_url",
|
|
180
|
+
"hostedUrl",
|
|
181
|
+
"live_view_url",
|
|
182
|
+
"liveViewUrl",
|
|
183
|
+
"cdp_ws_url",
|
|
184
|
+
"cdpWsUrl",
|
|
185
|
+
"browser_live_view_url"
|
|
186
|
+
]);
|
|
187
|
+
VENDOR_URL_RE = /\b(?:wss?|https?):\/\/[^\s"'<>]*\bonkernel\.com[^\s"'<>]*/gi;
|
|
170
188
|
}
|
|
171
189
|
});
|
|
172
190
|
|
|
@@ -9812,11 +9830,6 @@ var init_site_extract_repository = __esm({
|
|
|
9812
9830
|
});
|
|
9813
9831
|
|
|
9814
9832
|
// src/inngest/functions/site-extract.ts
|
|
9815
|
-
function chunk(items, size) {
|
|
9816
|
-
const out = [];
|
|
9817
|
-
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
9818
|
-
return out;
|
|
9819
|
-
}
|
|
9820
9833
|
function buildArtifacts(job, pages, branding, imageAudit) {
|
|
9821
9834
|
const { edges, metrics } = buildLinkGraph(pages, job.startUrl);
|
|
9822
9835
|
const issues = computeIssues(pages, metrics);
|
|
@@ -9891,18 +9904,62 @@ var init_site_extract = __esm({
|
|
|
9891
9904
|
const maxPages = Number(job.options.maxPages ?? 1e4);
|
|
9892
9905
|
const concurrency = Number(job.options.concurrency ?? 1);
|
|
9893
9906
|
const urlsPerBrowser = Number(job.options.urlsPerBrowser ?? job.options.rotateProxyEvery ?? 10);
|
|
9894
|
-
const
|
|
9907
|
+
const seed = await step.run("discover", async () => {
|
|
9895
9908
|
const discovered = await discoverUrls(job.startUrl, maxPages, key);
|
|
9896
|
-
|
|
9897
|
-
|
|
9909
|
+
const out = discovered.length ? discovered : [job.startUrl];
|
|
9910
|
+
await setExtractJobTotal(jobId, out.length);
|
|
9911
|
+
return out;
|
|
9898
9912
|
});
|
|
9899
|
-
const
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9913
|
+
const siteHost = (() => {
|
|
9914
|
+
try {
|
|
9915
|
+
return new URL(job.startUrl).hostname.replace(/^www\./, "");
|
|
9916
|
+
} catch {
|
|
9917
|
+
return "";
|
|
9918
|
+
}
|
|
9919
|
+
})();
|
|
9920
|
+
const norm = (u) => {
|
|
9921
|
+
try {
|
|
9922
|
+
const x = new URL(u);
|
|
9923
|
+
x.hash = "";
|
|
9924
|
+
return (x.origin + x.pathname).replace(/\/+$/, "") + x.search;
|
|
9925
|
+
} catch {
|
|
9926
|
+
return u;
|
|
9927
|
+
}
|
|
9928
|
+
};
|
|
9929
|
+
const sameSite = (u) => {
|
|
9930
|
+
try {
|
|
9931
|
+
return new URL(u).hostname.replace(/^www\./, "") === siteHost;
|
|
9932
|
+
} catch {
|
|
9933
|
+
return false;
|
|
9934
|
+
}
|
|
9935
|
+
};
|
|
9936
|
+
const queue = [...seed];
|
|
9937
|
+
const seen = new Set(seed.map(norm));
|
|
9938
|
+
let crawled = 0;
|
|
9939
|
+
const MAX_BATCHES = Math.ceil(maxPages / BATCH) + 30;
|
|
9940
|
+
for (let i = 0; i < MAX_BATCHES && crawled < maxPages && crawled < queue.length; i++) {
|
|
9941
|
+
const batch = queue.slice(crawled, Math.min(crawled + BATCH, maxPages));
|
|
9942
|
+
if (!batch.length) break;
|
|
9943
|
+
const newLinks = await step.run(`crawl-batch-${i}`, async () => {
|
|
9944
|
+
const pages = await extractPagesRotating(batch, { kernelApiKey: key, concurrency, urlsPerBrowser });
|
|
9903
9945
|
await saveExtractPages(jobId, pages);
|
|
9904
|
-
|
|
9946
|
+
const found = [];
|
|
9947
|
+
for (const p of pages) for (const l of p.outlinks ?? []) {
|
|
9948
|
+
if (l.internal && l.href && sameSite(l.href)) found.push(l.href);
|
|
9949
|
+
}
|
|
9950
|
+
return [...new Set(found)];
|
|
9905
9951
|
});
|
|
9952
|
+
crawled += batch.length;
|
|
9953
|
+
let added = false;
|
|
9954
|
+
for (const href of newLinks) {
|
|
9955
|
+
const n = norm(href);
|
|
9956
|
+
if (!seen.has(n) && seen.size < maxPages) {
|
|
9957
|
+
seen.add(n);
|
|
9958
|
+
queue.push(href);
|
|
9959
|
+
added = true;
|
|
9960
|
+
}
|
|
9961
|
+
}
|
|
9962
|
+
if (added) await setExtractJobTotal(jobId, Math.min(seen.size, maxPages));
|
|
9906
9963
|
}
|
|
9907
9964
|
const branding = Array.isArray(job.options.formats) && job.options.formats.includes("branding") ? await step.run("branding", () => extractBranding(job.startUrl, key).catch(() => null)) : null;
|
|
9908
9965
|
const imageAudit = Array.isArray(job.options.formats) && job.options.formats.includes("images") ? await step.run("image-audit", async () => {
|
|
@@ -12247,10 +12304,10 @@ function buildTranscriptMarkdown(result) {
|
|
|
12247
12304
|
if (result.chunks?.length) {
|
|
12248
12305
|
lines.push("## Timestamped Segments");
|
|
12249
12306
|
lines.push("");
|
|
12250
|
-
for (const
|
|
12251
|
-
const start = fmtTs(
|
|
12252
|
-
const end = fmtTs(
|
|
12253
|
-
lines.push(`**[${start} \u2192 ${end}]** ${
|
|
12307
|
+
for (const chunk of result.chunks) {
|
|
12308
|
+
const start = fmtTs(chunk.timestamp[0]);
|
|
12309
|
+
const end = fmtTs(chunk.timestamp[1]);
|
|
12310
|
+
lines.push(`**[${start} \u2192 ${end}]** ${chunk.text.trim()}`);
|
|
12254
12311
|
lines.push("");
|
|
12255
12312
|
}
|
|
12256
12313
|
}
|
|
@@ -12888,20 +12945,20 @@ var init_FacebookAdExtractor = __esm({
|
|
|
12888
12945
|
if (splitRe.lastIndex === m.index) splitRe.lastIndex++;
|
|
12889
12946
|
}
|
|
12890
12947
|
if (last < bodyText.length) adChunks.push(bodyText.slice(last));
|
|
12891
|
-
const ads = adChunks.filter((c) => /Library ID[:\s]+\d{10,20}/i.test(c)).slice(0, maxAds).map((
|
|
12892
|
-
const lidM =
|
|
12948
|
+
const ads = adChunks.filter((c) => /Library ID[:\s]+\d{10,20}/i.test(c)).slice(0, maxAds).map((chunk) => {
|
|
12949
|
+
const lidM = chunk.match(/Library ID[:\s]+([0-9]{10,20})/i);
|
|
12893
12950
|
const libraryId = lidM ? lidM[1] : null;
|
|
12894
|
-
const statusM =
|
|
12951
|
+
const statusM = chunk.match(/^(Active|Inactive)/i);
|
|
12895
12952
|
const status = statusM ? statusM[1] : null;
|
|
12896
|
-
const startM =
|
|
12953
|
+
const startM = chunk.match(/Started running on\s+([A-Za-z]+\.?\s+\d{1,2},?\s*\d{4})/i);
|
|
12897
12954
|
const started = startM ? startM[1].replace(/\s+/g, " ").trim() : null;
|
|
12898
12955
|
const visual = adVisuals[libraryId ?? ""] ?? { imageSrc: null, videoSrc: null, videoPoster: null };
|
|
12899
|
-
const hasVideoText = /0:00\s*\/\s*0:00|\d+:\d+\s*\/\s*\d+:\d+/.test(
|
|
12956
|
+
const hasVideoText = /0:00\s*\/\s*0:00|\d+:\d+\s*\/\s*\d+:\d+/.test(chunk);
|
|
12900
12957
|
const creativeType = visual.videoSrc || hasVideoText ? "video" : visual.imageSrc ? "image" : "unknown";
|
|
12901
12958
|
let domain = null;
|
|
12902
12959
|
const domainRe = /\b([A-Z0-9]{2,}(?:\.[A-Z]{2,})+)\b/g;
|
|
12903
12960
|
let dm;
|
|
12904
|
-
while ((dm = domainRe.exec(
|
|
12961
|
+
while ((dm = domainRe.exec(chunk)) !== null) {
|
|
12905
12962
|
const c = dm[1];
|
|
12906
12963
|
if (c.length >= 6 && c !== "LIBRARY") {
|
|
12907
12964
|
domain = c;
|
|
@@ -12910,9 +12967,9 @@ var init_FacebookAdExtractor = __esm({
|
|
|
12910
12967
|
}
|
|
12911
12968
|
let primaryText = null;
|
|
12912
12969
|
let headline = null;
|
|
12913
|
-
const sponsoredIdx =
|
|
12970
|
+
const sponsoredIdx = chunk.search(/\bSponsored\b/i);
|
|
12914
12971
|
if (sponsoredIdx >= 0) {
|
|
12915
|
-
let afterSponsored =
|
|
12972
|
+
let afterSponsored = chunk.slice(sponsoredIdx + "Sponsored".length).trim();
|
|
12916
12973
|
afterSponsored = afterSponsored.replace(/\s*\d*:?\d+\s*\/\s*\d*:?\d+.*$/, "");
|
|
12917
12974
|
if (domain) {
|
|
12918
12975
|
const di = afterSponsored.indexOf(domain);
|
|
@@ -12923,18 +12980,18 @@ var init_FacebookAdExtractor = __esm({
|
|
|
12923
12980
|
}
|
|
12924
12981
|
const ctaRe2 = new RegExp("\\b(" + CTA_LABELS.map((l) => l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|") + ")\\b", "i");
|
|
12925
12982
|
if (domain) {
|
|
12926
|
-
const di =
|
|
12983
|
+
const di = chunk.indexOf(domain);
|
|
12927
12984
|
if (di >= 0) {
|
|
12928
|
-
let afterDomain =
|
|
12985
|
+
let afterDomain = chunk.slice(di + domain.length).trim();
|
|
12929
12986
|
const ctaMatch = afterDomain.search(ctaRe2);
|
|
12930
12987
|
if (ctaMatch >= 0) afterDomain = afterDomain.slice(0, ctaMatch).trim();
|
|
12931
12988
|
if (afterDomain.length > 3 && afterDomain.length < 120) headline = afterDomain;
|
|
12932
12989
|
}
|
|
12933
12990
|
}
|
|
12934
12991
|
if (!headline) {
|
|
12935
|
-
const ctaIdx =
|
|
12992
|
+
const ctaIdx = chunk.search(ctaRe2);
|
|
12936
12993
|
if (ctaIdx > 0) {
|
|
12937
|
-
const beforeCta =
|
|
12994
|
+
const beforeCta = chunk.slice(0, ctaIdx).trimEnd();
|
|
12938
12995
|
const sentenceEnd = Math.max(beforeCta.lastIndexOf(". "), beforeCta.lastIndexOf("! "), beforeCta.lastIndexOf("? "));
|
|
12939
12996
|
if (sentenceEnd >= 0) {
|
|
12940
12997
|
const candidate = beforeCta.slice(sentenceEnd + 2).trim();
|
|
@@ -12946,12 +13003,12 @@ var init_FacebookAdExtractor = __esm({
|
|
|
12946
13003
|
}
|
|
12947
13004
|
let cta = null;
|
|
12948
13005
|
for (const lbl of CTA_LABELS) {
|
|
12949
|
-
if (
|
|
13006
|
+
if (chunk.toLowerCase().includes(lbl.toLowerCase())) {
|
|
12950
13007
|
cta = lbl;
|
|
12951
13008
|
break;
|
|
12952
13009
|
}
|
|
12953
13010
|
}
|
|
12954
|
-
const multiM =
|
|
13011
|
+
const multiM = chunk.match(/(\d+)\s+ad[s]?\s+use[s]?\s+this/i);
|
|
12955
13012
|
const clusterCount = multiM ? parseInt(multiM[1]) : null;
|
|
12956
13013
|
return {
|
|
12957
13014
|
libraryId,
|
|
@@ -12974,8 +13031,8 @@ var init_FacebookAdExtractor = __esm({
|
|
|
12974
13031
|
const unknownCreativeCount = ads.filter((a) => a.creativeType === "unknown").length;
|
|
12975
13032
|
const advertiserPageId = new URL(page.url()).searchParams.get("view_all_page_id") ?? null;
|
|
12976
13033
|
const nameFreq = /* @__PURE__ */ new Map();
|
|
12977
|
-
for (const
|
|
12978
|
-
const detailsParts =
|
|
13034
|
+
for (const chunk of adChunks) {
|
|
13035
|
+
const detailsParts = chunk.split("See ad details ");
|
|
12979
13036
|
if (detailsParts.length < 2) continue;
|
|
12980
13037
|
const sponsoredIdx = detailsParts[1].indexOf(" Sponsored");
|
|
12981
13038
|
if (sponsoredIdx < 0) continue;
|
|
@@ -14394,11 +14451,11 @@ var init_facebook_ad_routes = __esm({
|
|
|
14394
14451
|
}
|
|
14395
14452
|
if (last < bodyText.length) adChunks.push(bodyText.slice(last));
|
|
14396
14453
|
const advertiserMap = /* @__PURE__ */ new Map();
|
|
14397
|
-
for (const
|
|
14398
|
-
const lidM =
|
|
14454
|
+
for (const chunk of adChunks) {
|
|
14455
|
+
const lidM = chunk.match(/Library ID[:\s]+([0-9]{10,20})/i);
|
|
14399
14456
|
if (!lidM) continue;
|
|
14400
14457
|
const libraryId = lidM[1];
|
|
14401
|
-
const detailsParts =
|
|
14458
|
+
const detailsParts = chunk.split("See ad details ");
|
|
14402
14459
|
if (detailsParts.length < 2) continue;
|
|
14403
14460
|
const afterDetails = detailsParts[1];
|
|
14404
14461
|
const sponsoredIdx = afterDetails.indexOf(" Sponsored");
|
|
@@ -14922,8 +14979,8 @@ function runFfmpegMux(videoPath, audioPath, outPath) {
|
|
|
14922
14979
|
stdio: ["ignore", "ignore", "pipe"]
|
|
14923
14980
|
});
|
|
14924
14981
|
let stderr = "";
|
|
14925
|
-
child.stderr.on("data", (
|
|
14926
|
-
stderr += String(
|
|
14982
|
+
child.stderr.on("data", (chunk) => {
|
|
14983
|
+
stderr += String(chunk);
|
|
14927
14984
|
});
|
|
14928
14985
|
child.on("error", (err) => resolve({ ok: false, error: err.message }));
|
|
14929
14986
|
child.on("close", (code) => resolve({ ok: code === 0, error: code === 0 ? null : stderr.trim() || `ffmpeg exited ${code}` }));
|
|
@@ -23698,7 +23755,7 @@ var PACKAGE_VERSION;
|
|
|
23698
23755
|
var init_version = __esm({
|
|
23699
23756
|
"src/version.ts"() {
|
|
23700
23757
|
"use strict";
|
|
23701
|
-
PACKAGE_VERSION = "0.3.
|
|
23758
|
+
PACKAGE_VERSION = "0.3.18";
|
|
23702
23759
|
}
|
|
23703
23760
|
});
|
|
23704
23761
|
|
|
@@ -23722,7 +23779,11 @@ ROUTING
|
|
|
23722
23779
|
- Workflows (multi-step): local directory build -> directory_workflow; rank blueprint -> rank_tracker_workflow;
|
|
23723
23780
|
AI-answer citation fan-out (AEO) -> query_fanout_workflow; deep research -> deep_research_workflow.
|
|
23724
23781
|
- Anything without a dedicated tool (e.g. Reddit, arbitrary logged-in sites) -> the browser_* agent
|
|
23725
|
-
(browser_open, then navigate/read)
|
|
23782
|
+
(browser_open, then navigate/read).
|
|
23783
|
+
- Logged-in sites (ChatGPT, Claude, Reddit, any account): save the login first -> browser_profile_connect
|
|
23784
|
+
(returns an mcpscraper.dev sign-in link for the user; one profile holds MANY logins, call it again with
|
|
23785
|
+
the same profile + a new domain to add accounts) -> poll browser_profile_list until AUTHENTICATED ->
|
|
23786
|
+
browser_open with that profile. browser_profile_list also shows what a profile is connected to.
|
|
23726
23787
|
|
|
23727
23788
|
NOTES
|
|
23728
23789
|
- Bulk / full-site crawls: call extract_site with rotateProxies:true for blocked or rate-limited sites.
|
|
@@ -25518,8 +25579,64 @@ async function migrateBrowserAgent() {
|
|
|
25518
25579
|
)
|
|
25519
25580
|
`);
|
|
25520
25581
|
await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_replays_session ON browser_agent_replays(session_id)`);
|
|
25582
|
+
await db.execute(`
|
|
25583
|
+
CREATE TABLE IF NOT EXISTS browser_auth_connections (
|
|
25584
|
+
connection_id TEXT PRIMARY KEY,
|
|
25585
|
+
hosted_url TEXT,
|
|
25586
|
+
domain TEXT,
|
|
25587
|
+
profile TEXT,
|
|
25588
|
+
account_email TEXT,
|
|
25589
|
+
note TEXT,
|
|
25590
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
25591
|
+
updated_at TEXT
|
|
25592
|
+
)
|
|
25593
|
+
`);
|
|
25594
|
+
try {
|
|
25595
|
+
await db.execute(`ALTER TABLE browser_auth_connections ADD COLUMN account_email TEXT`);
|
|
25596
|
+
} catch {
|
|
25597
|
+
}
|
|
25598
|
+
try {
|
|
25599
|
+
await db.execute(`ALTER TABLE browser_auth_connections ADD COLUMN note TEXT`);
|
|
25600
|
+
} catch {
|
|
25601
|
+
}
|
|
25602
|
+
try {
|
|
25603
|
+
await db.execute(`ALTER TABLE browser_auth_connections ADD COLUMN updated_at TEXT`);
|
|
25604
|
+
} catch {
|
|
25605
|
+
}
|
|
25606
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS browser_auth_connections_profile ON browser_auth_connections(profile)`);
|
|
25521
25607
|
_ready = true;
|
|
25522
25608
|
}
|
|
25609
|
+
async function saveAuthConnection(input) {
|
|
25610
|
+
if (!input.connectionId) return;
|
|
25611
|
+
const db = getDb();
|
|
25612
|
+
await db.execute({
|
|
25613
|
+
sql: `INSERT INTO browser_auth_connections (connection_id, hosted_url, domain, profile, account_email, note, updated_at)
|
|
25614
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
|
25615
|
+
ON CONFLICT(connection_id) DO UPDATE SET
|
|
25616
|
+
hosted_url = COALESCE(excluded.hosted_url, browser_auth_connections.hosted_url),
|
|
25617
|
+
domain = COALESCE(excluded.domain, browser_auth_connections.domain),
|
|
25618
|
+
profile = COALESCE(excluded.profile, browser_auth_connections.profile),
|
|
25619
|
+
account_email = COALESCE(excluded.account_email, browser_auth_connections.account_email),
|
|
25620
|
+
note = COALESCE(excluded.note, browser_auth_connections.note),
|
|
25621
|
+
updated_at = datetime('now')`,
|
|
25622
|
+
args: [input.connectionId, input.hostedUrl, input.domain, input.profile, input.accountEmail ?? null, input.note ?? null]
|
|
25623
|
+
});
|
|
25624
|
+
}
|
|
25625
|
+
async function getAuthConnectionRow(connectionId) {
|
|
25626
|
+
if (!connectionId) return null;
|
|
25627
|
+
const db = getDb();
|
|
25628
|
+
const res = await db.execute({ sql: `SELECT * FROM browser_auth_connections WHERE connection_id = ?`, args: [connectionId] });
|
|
25629
|
+
return res.rows[0] ?? null;
|
|
25630
|
+
}
|
|
25631
|
+
async function listAuthConnectionsByProfile(profile) {
|
|
25632
|
+
if (!profile) return [];
|
|
25633
|
+
const db = getDb();
|
|
25634
|
+
const res = await db.execute({
|
|
25635
|
+
sql: `SELECT * FROM browser_auth_connections WHERE profile = ? ORDER BY COALESCE(updated_at, created_at) DESC`,
|
|
25636
|
+
args: [profile]
|
|
25637
|
+
});
|
|
25638
|
+
return res.rows;
|
|
25639
|
+
}
|
|
25523
25640
|
async function createSessionRow(input) {
|
|
25524
25641
|
const db = getDb();
|
|
25525
25642
|
const id = `bas_${(0, import_node_crypto6.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
|
|
@@ -26547,6 +26664,17 @@ async function findProfileConnection(k, profileName, domain) {
|
|
|
26547
26664
|
}
|
|
26548
26665
|
return null;
|
|
26549
26666
|
}
|
|
26667
|
+
async function listProfileConnections(profileName) {
|
|
26668
|
+
if (!profileName) return [];
|
|
26669
|
+
const k = client();
|
|
26670
|
+
const out = [];
|
|
26671
|
+
for await (const connection of k.auth.connections.list({ profile_name: profileName })) {
|
|
26672
|
+
const data = connection;
|
|
26673
|
+
if (data.profile_name && data.profile_name !== profileName) continue;
|
|
26674
|
+
out.push(profileOnboardResult(connection));
|
|
26675
|
+
}
|
|
26676
|
+
return out;
|
|
26677
|
+
}
|
|
26550
26678
|
async function findOrCreateProfileConnection(k, opts) {
|
|
26551
26679
|
try {
|
|
26552
26680
|
return await k.auth.connections.create({
|
|
@@ -26949,7 +27077,7 @@ function publicSession(row) {
|
|
|
26949
27077
|
session_id: row.id,
|
|
26950
27078
|
status: row.status,
|
|
26951
27079
|
label: row.label,
|
|
26952
|
-
|
|
27080
|
+
watch_url: `/console/${row.id}`,
|
|
26953
27081
|
created_at: row.created_at,
|
|
26954
27082
|
last_action_at: row.last_action_at,
|
|
26955
27083
|
closed_at: row.closed_at,
|
|
@@ -27005,10 +27133,23 @@ function buildBrowserAgentRoutes() {
|
|
|
27005
27133
|
const profileName = typeof body.profile === "string" ? body.profile.trim() : "";
|
|
27006
27134
|
const domain = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : "chatgpt.com";
|
|
27007
27135
|
const loginUrl = typeof body.login_url === "string" ? body.login_url.trim() : void 0;
|
|
27136
|
+
const accountEmail = typeof body.account_email === "string" ? body.account_email.trim() || null : null;
|
|
27137
|
+
const note = typeof body.note === "string" ? body.note.trim() || null : null;
|
|
27008
27138
|
if (!profileName) return c.json({ error: "profile is required" }, 400);
|
|
27009
27139
|
if (!domain) return c.json({ error: "domain is required" }, 400);
|
|
27010
27140
|
try {
|
|
27011
27141
|
const result = await startProfileOnboarding({ profileName, domain, loginUrl });
|
|
27142
|
+
try {
|
|
27143
|
+
await saveAuthConnection({
|
|
27144
|
+
connectionId: result.connectionId,
|
|
27145
|
+
hostedUrl: result.hostedUrl,
|
|
27146
|
+
domain: result.domain,
|
|
27147
|
+
profile: result.profileName,
|
|
27148
|
+
accountEmail,
|
|
27149
|
+
note
|
|
27150
|
+
});
|
|
27151
|
+
} catch {
|
|
27152
|
+
}
|
|
27012
27153
|
return c.json({
|
|
27013
27154
|
connection_id: result.connectionId,
|
|
27014
27155
|
profile: result.profileName,
|
|
@@ -27017,8 +27158,6 @@ function buildBrowserAgentRoutes() {
|
|
|
27017
27158
|
flow_status: result.flowStatus,
|
|
27018
27159
|
flow_step: result.flowStep,
|
|
27019
27160
|
flow_expires_at: result.flowExpiresAt,
|
|
27020
|
-
hosted_url: result.hostedUrl,
|
|
27021
|
-
live_view_url: result.liveViewUrl,
|
|
27022
27161
|
browser_session_id: result.browserSessionId,
|
|
27023
27162
|
post_login_url: result.postLoginUrl
|
|
27024
27163
|
});
|
|
@@ -27033,6 +27172,17 @@ function buildBrowserAgentRoutes() {
|
|
|
27033
27172
|
const domain = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : void 0;
|
|
27034
27173
|
try {
|
|
27035
27174
|
const result = await getProfileOnboardingStatus({ connectionId, profileName, domain });
|
|
27175
|
+
if (result.hostedUrl) {
|
|
27176
|
+
try {
|
|
27177
|
+
await saveAuthConnection({
|
|
27178
|
+
connectionId: result.connectionId,
|
|
27179
|
+
hostedUrl: result.hostedUrl,
|
|
27180
|
+
domain: result.domain,
|
|
27181
|
+
profile: result.profileName
|
|
27182
|
+
});
|
|
27183
|
+
} catch {
|
|
27184
|
+
}
|
|
27185
|
+
}
|
|
27036
27186
|
return c.json({
|
|
27037
27187
|
connection_id: result.connectionId,
|
|
27038
27188
|
profile: result.profileName,
|
|
@@ -27041,8 +27191,6 @@ function buildBrowserAgentRoutes() {
|
|
|
27041
27191
|
flow_status: result.flowStatus,
|
|
27042
27192
|
flow_step: result.flowStep,
|
|
27043
27193
|
flow_expires_at: result.flowExpiresAt,
|
|
27044
|
-
hosted_url: result.hostedUrl,
|
|
27045
|
-
live_view_url: result.liveViewUrl,
|
|
27046
27194
|
browser_session_id: result.browserSessionId,
|
|
27047
27195
|
post_login_url: result.postLoginUrl
|
|
27048
27196
|
});
|
|
@@ -27050,6 +27198,37 @@ function buildBrowserAgentRoutes() {
|
|
|
27050
27198
|
return c.json(failure(err), 404);
|
|
27051
27199
|
}
|
|
27052
27200
|
});
|
|
27201
|
+
app2.post("/profiles/list", async (c) => {
|
|
27202
|
+
const body = await c.req.json().catch(() => ({}));
|
|
27203
|
+
const profileName = typeof body.profile === "string" ? body.profile.trim() : "";
|
|
27204
|
+
if (!profileName) return c.json({ error: "profile is required" }, 400);
|
|
27205
|
+
const domainFilter = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : void 0;
|
|
27206
|
+
const connectionIdFilter = typeof body.connection_id === "string" ? body.connection_id.trim() : void 0;
|
|
27207
|
+
try {
|
|
27208
|
+
const [live, stored] = await Promise.all([
|
|
27209
|
+
listProfileConnections(profileName),
|
|
27210
|
+
listAuthConnectionsByProfile(profileName).catch(() => [])
|
|
27211
|
+
]);
|
|
27212
|
+
const notesByDomain = new Map(stored.map((row) => [row.domain ?? "", row]));
|
|
27213
|
+
const notesByConnection = new Map(stored.map((row) => [row.connection_id, row]));
|
|
27214
|
+
let connections = live.map((conn) => {
|
|
27215
|
+
const record = notesByConnection.get(conn.connectionId) ?? notesByDomain.get(conn.domain);
|
|
27216
|
+
return {
|
|
27217
|
+
connection_id: conn.connectionId,
|
|
27218
|
+
domain: conn.domain,
|
|
27219
|
+
status: conn.status,
|
|
27220
|
+
account_email: record?.account_email ?? null,
|
|
27221
|
+
note: record?.note ?? null,
|
|
27222
|
+
last_connected_at: record?.updated_at ?? record?.created_at ?? null
|
|
27223
|
+
};
|
|
27224
|
+
});
|
|
27225
|
+
if (connectionIdFilter) connections = connections.filter((c2) => c2.connection_id === connectionIdFilter);
|
|
27226
|
+
if (domainFilter) connections = connections.filter((c2) => c2.domain === domainFilter);
|
|
27227
|
+
return c.json({ profile: profileName, connections });
|
|
27228
|
+
} catch (err) {
|
|
27229
|
+
return c.json(failure(err), 502);
|
|
27230
|
+
}
|
|
27231
|
+
});
|
|
27053
27232
|
app2.post("/sessions", async (c) => {
|
|
27054
27233
|
const user = c.get("user");
|
|
27055
27234
|
if (Number(user.balance_mc ?? 0) < BROWSER_OPEN_MIN_BALANCE_MC) {
|
|
@@ -27422,6 +27601,50 @@ var init_browser_agent_routes = __esm({
|
|
|
27422
27601
|
});
|
|
27423
27602
|
|
|
27424
27603
|
// src/api/browser-agent-console.ts
|
|
27604
|
+
function renderAuthConsoleHtml(connectionId, hostedUrl) {
|
|
27605
|
+
const safeId = String(connectionId).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
27606
|
+
const goUrl = `/console/auth/${encodeURIComponent(connectionId)}/go`;
|
|
27607
|
+
const body = hostedUrl ? `<iframe class="stage" allow="clipboard-read; clipboard-write" src="${goUrl}"></iframe>
|
|
27608
|
+
<div class="fallback">
|
|
27609
|
+
<span>Sign-in window not showing?</span>
|
|
27610
|
+
<a class="btn" href="${goUrl}" target="_blank" rel="noopener">Open sign-in in a new tab</a>
|
|
27611
|
+
</div>` : `<div class="empty">
|
|
27612
|
+
<div>This sign-in link is no longer active.</div>
|
|
27613
|
+
<div class="muted">Ask the assistant to reconnect the profile, then open the new link.</div>
|
|
27614
|
+
</div>`;
|
|
27615
|
+
return `<!DOCTYPE html>
|
|
27616
|
+
<html lang="en">
|
|
27617
|
+
<head>
|
|
27618
|
+
<meta charset="UTF-8" />
|
|
27619
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
27620
|
+
<title>MCP Scraper \u2014 Secure Sign-in</title>
|
|
27621
|
+
<style>
|
|
27622
|
+
:root { color-scheme: dark; }
|
|
27623
|
+
:where(*) { box-sizing: border-box; }
|
|
27624
|
+
body { margin: 0; height: 100vh; display: flex; flex-direction: column; font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif; background: #0b0e14; color: #d7dce5; }
|
|
27625
|
+
header { display: flex; align-items: center; gap: 10px; padding: 12px 18px; border-bottom: 1px solid #1c2230; background: #0f131c; }
|
|
27626
|
+
header h1 { font-size: 14px; margin: 0; font-weight: 600; color: #fff; }
|
|
27627
|
+
header .muted { color: #6b7689; font-size: 12px; }
|
|
27628
|
+
.spacer { flex: 1; }
|
|
27629
|
+
.stage { flex: 1; width: 100%; border: 0; display: block; background: #05070b; }
|
|
27630
|
+
.fallback { display: flex; align-items: center; gap: 12px; padding: 10px 18px; border-top: 1px solid #1c2230; background: #0f131c; font-size: 13px; color: #8893a7; }
|
|
27631
|
+
.btn { background: #2b6cff; color: #fff; border-radius: 7px; padding: 7px 12px; text-decoration: none; font-weight: 500; }
|
|
27632
|
+
.empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: #8893a7; }
|
|
27633
|
+
.muted { color: #6b7689; font-size: 12px; }
|
|
27634
|
+
</style>
|
|
27635
|
+
${ATTRIBUTION_PIXEL_SCRIPT}
|
|
27636
|
+
</head>
|
|
27637
|
+
<body>
|
|
27638
|
+
<header>
|
|
27639
|
+
<h1>MCP Scraper</h1>
|
|
27640
|
+
<span class="muted">Secure sign-in \xB7 ${safeId}</span>
|
|
27641
|
+
<span class="spacer"></span>
|
|
27642
|
+
<span class="muted">Complete the login below \u2014 your session is saved to your profile.</span>
|
|
27643
|
+
</header>
|
|
27644
|
+
${body}
|
|
27645
|
+
</body>
|
|
27646
|
+
</html>`;
|
|
27647
|
+
}
|
|
27425
27648
|
function renderConsoleHtml(initialSessionId) {
|
|
27426
27649
|
const initial = JSON.stringify(initialSessionId ?? "");
|
|
27427
27650
|
return `<!DOCTYPE html>
|
|
@@ -28207,6 +28430,7 @@ var init_server = __esm({
|
|
|
28207
28430
|
init_mcp_routes();
|
|
28208
28431
|
init_browser_agent_routes();
|
|
28209
28432
|
init_browser_agent_console();
|
|
28433
|
+
init_browser_agent_db();
|
|
28210
28434
|
init_stripe_routes();
|
|
28211
28435
|
init_site_audit_worker();
|
|
28212
28436
|
import_factory6 = require("hono/factory");
|
|
@@ -29035,6 +29259,19 @@ var init_server = __esm({
|
|
|
29035
29259
|
app.route("/mcp", mcpApp);
|
|
29036
29260
|
app.route("/agent", buildBrowserAgentRoutes());
|
|
29037
29261
|
app.get("/console", (c) => c.html(renderConsoleHtml()));
|
|
29262
|
+
app.get("/console/auth/:id", async (c) => {
|
|
29263
|
+
const id = c.req.param("id");
|
|
29264
|
+
await migrateBrowserAgent();
|
|
29265
|
+
const row = await getAuthConnectionRow(id);
|
|
29266
|
+
return c.html(renderAuthConsoleHtml(id, row?.hosted_url ?? null));
|
|
29267
|
+
});
|
|
29268
|
+
app.get("/console/auth/:id/go", async (c) => {
|
|
29269
|
+
const id = c.req.param("id");
|
|
29270
|
+
await migrateBrowserAgent();
|
|
29271
|
+
const row = await getAuthConnectionRow(id);
|
|
29272
|
+
if (!row?.hosted_url) return c.text("This sign-in link is no longer active.", 404);
|
|
29273
|
+
return c.redirect(row.hosted_url, 302);
|
|
29274
|
+
});
|
|
29038
29275
|
app.get("/console/:id", (c) => c.html(renderConsoleHtml(c.req.param("id"))));
|
|
29039
29276
|
app.route("/stripe", stripeApp);
|
|
29040
29277
|
if (!process.env.INNGEST_EVENT_KEY) {
|