mcp-scraper 0.3.16 → 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 +418 -53
- 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 +2554 -781
- 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-7XARJHS6.js → chunk-2XCNU22L.js} +332 -13
- package/dist/chunk-2XCNU22L.js.map +1 -0
- 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-LPVBSE2E.js → server-3M3XCVOT.js} +328 -157
- 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 -5601
- 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-7XARJHS6.js.map +0 -1
- package/dist/chunk-ACIUCZ27.js.map +0 -1
- package/dist/chunk-ISZWGIWL.js +0 -1332
- package/dist/chunk-ISZWGIWL.js.map +0 -1
- package/dist/chunk-M2S27J6Z.js.map +0 -1
- package/dist/chunk-SOMBZK3V.js +0 -7
- package/dist/chunk-SOMBZK3V.js.map +0 -1
- package/dist/chunk-WN7PBKMV.js +0 -28
- package/dist/chunk-WN7PBKMV.js.map +0 -1
- package/dist/server-LPVBSE2E.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
|
|
|
@@ -9054,6 +9072,104 @@ var init_seo_link_graph = __esm({
|
|
|
9054
9072
|
}
|
|
9055
9073
|
});
|
|
9056
9074
|
|
|
9075
|
+
// src/api/seo-link-report.ts
|
|
9076
|
+
function registrableDomain(host) {
|
|
9077
|
+
const h = host.replace(/^www\./, "").toLowerCase();
|
|
9078
|
+
const parts = h.split(".");
|
|
9079
|
+
return parts.length <= 2 ? h : parts.slice(-2).join(".");
|
|
9080
|
+
}
|
|
9081
|
+
function buildLinkReport(edges, metrics, siteUrl) {
|
|
9082
|
+
let siteReg = "";
|
|
9083
|
+
try {
|
|
9084
|
+
siteReg = registrableDomain(new URL(siteUrl).hostname);
|
|
9085
|
+
} catch {
|
|
9086
|
+
siteReg = "";
|
|
9087
|
+
}
|
|
9088
|
+
const internalEdges = edges.filter((e) => e.internal);
|
|
9089
|
+
const pages = metrics.length;
|
|
9090
|
+
const orphans = metrics.filter((m) => m.orphan).length;
|
|
9091
|
+
const brokenInternal = internalEdges.filter((e) => e.targetStatus != null && e.targetStatus >= 400).length;
|
|
9092
|
+
const sumInlinks = metrics.reduce((a, m) => a + m.inlinks, 0);
|
|
9093
|
+
const sumOutlinks = metrics.reduce((a, m) => a + m.outlinksInternal + m.outlinksExternal, 0);
|
|
9094
|
+
const distribution = { zero: 0, oneToTwo: 0, threeToTen: 0, elevenPlus: 0 };
|
|
9095
|
+
for (const m of metrics) {
|
|
9096
|
+
if (m.inlinks === 0) distribution.zero++;
|
|
9097
|
+
else if (m.inlinks <= 2) distribution.oneToTwo++;
|
|
9098
|
+
else if (m.inlinks <= 10) distribution.threeToTen++;
|
|
9099
|
+
else distribution.elevenPlus++;
|
|
9100
|
+
}
|
|
9101
|
+
const topByInlinks = [...metrics].sort((a, b) => b.inlinks - a.inlinks).slice(0, 20).map((m) => ({ url: m.url, inlinks: m.inlinks, outlinksInternal: m.outlinksInternal, outlinksExternal: m.outlinksExternal }));
|
|
9102
|
+
const domMap = /* @__PURE__ */ new Map();
|
|
9103
|
+
let externalTotal = 0;
|
|
9104
|
+
for (const e of edges) {
|
|
9105
|
+
let domain;
|
|
9106
|
+
try {
|
|
9107
|
+
domain = registrableDomain(new URL(e.to).hostname);
|
|
9108
|
+
} catch {
|
|
9109
|
+
continue;
|
|
9110
|
+
}
|
|
9111
|
+
if (!domain || domain === siteReg) continue;
|
|
9112
|
+
externalTotal++;
|
|
9113
|
+
const d = domMap.get(domain) ?? { links: 0, nofollow: 0, pages: /* @__PURE__ */ new Set() };
|
|
9114
|
+
d.links++;
|
|
9115
|
+
if (e.nofollow) d.nofollow++;
|
|
9116
|
+
d.pages.add(e.from);
|
|
9117
|
+
domMap.set(domain, d);
|
|
9118
|
+
}
|
|
9119
|
+
const externalDomains = [...domMap.entries()].map(([domain, d]) => ({ domain, links: d.links, nofollow: d.nofollow, pages: d.pages.size })).sort((a, b) => b.links - a.links);
|
|
9120
|
+
const round = (n) => Math.round(n * 10) / 10;
|
|
9121
|
+
return {
|
|
9122
|
+
summary: {
|
|
9123
|
+
internal: {
|
|
9124
|
+
totalLinks: internalEdges.length,
|
|
9125
|
+
pages,
|
|
9126
|
+
orphans,
|
|
9127
|
+
brokenInternal,
|
|
9128
|
+
avgInlinks: pages ? round(sumInlinks / pages) : 0,
|
|
9129
|
+
avgOutlinks: pages ? round(sumOutlinks / pages) : 0,
|
|
9130
|
+
distribution,
|
|
9131
|
+
topByInlinks
|
|
9132
|
+
},
|
|
9133
|
+
external: {
|
|
9134
|
+
totalLinks: externalTotal,
|
|
9135
|
+
uniqueDomains: externalDomains.length,
|
|
9136
|
+
topDomains: externalDomains.slice(0, 20)
|
|
9137
|
+
}
|
|
9138
|
+
},
|
|
9139
|
+
externalDomains
|
|
9140
|
+
};
|
|
9141
|
+
}
|
|
9142
|
+
function renderLinkReport(r) {
|
|
9143
|
+
const s = r.summary;
|
|
9144
|
+
const lines = [
|
|
9145
|
+
`## Link analysis`,
|
|
9146
|
+
`**Internal:** ${s.internal.totalLinks} links \xB7 ${s.internal.pages} pages \xB7 avg ${s.internal.avgInlinks} inlinks/page \xB7 ${s.internal.orphans} orphans \xB7 ${s.internal.brokenInternal} broken`,
|
|
9147
|
+
`**External:** ${s.external.totalLinks} links to ${s.external.uniqueDomains} domains`,
|
|
9148
|
+
``,
|
|
9149
|
+
`### Inlink distribution`,
|
|
9150
|
+
`- 0 inlinks (orphans): ${s.internal.distribution.zero}`,
|
|
9151
|
+
`- 1\u20132: ${s.internal.distribution.oneToTwo}`,
|
|
9152
|
+
`- 3\u201310: ${s.internal.distribution.threeToTen}`,
|
|
9153
|
+
`- 11+: ${s.internal.distribution.elevenPlus}`,
|
|
9154
|
+
``,
|
|
9155
|
+
`### Top 20 internal pages by inlinks`,
|
|
9156
|
+
`| inlinks | out (int/ext) | URL |`,
|
|
9157
|
+
`|---|---|---|`,
|
|
9158
|
+
...s.internal.topByInlinks.map((p) => `| ${p.inlinks} | ${p.outlinksInternal}/${p.outlinksExternal} | ${p.url} |`),
|
|
9159
|
+
``,
|
|
9160
|
+
`### Top 20 external domains by links`,
|
|
9161
|
+
`| links | nofollow | from pages | domain |`,
|
|
9162
|
+
`|---|---|---|---|`,
|
|
9163
|
+
...s.external.topDomains.map((d) => `| ${d.links} | ${d.nofollow} | ${d.pages} | ${d.domain} |`)
|
|
9164
|
+
];
|
|
9165
|
+
return lines.join("\n");
|
|
9166
|
+
}
|
|
9167
|
+
var init_seo_link_report = __esm({
|
|
9168
|
+
"src/api/seo-link-report.ts"() {
|
|
9169
|
+
"use strict";
|
|
9170
|
+
}
|
|
9171
|
+
});
|
|
9172
|
+
|
|
9057
9173
|
// src/api/seo-issues.ts
|
|
9058
9174
|
function dupes(pages, key) {
|
|
9059
9175
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -9714,14 +9830,10 @@ var init_site_extract_repository = __esm({
|
|
|
9714
9830
|
});
|
|
9715
9831
|
|
|
9716
9832
|
// src/inngest/functions/site-extract.ts
|
|
9717
|
-
function chunk(items, size) {
|
|
9718
|
-
const out = [];
|
|
9719
|
-
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
9720
|
-
return out;
|
|
9721
|
-
}
|
|
9722
9833
|
function buildArtifacts(job, pages, branding, imageAudit) {
|
|
9723
9834
|
const { edges, metrics } = buildLinkGraph(pages, job.startUrl);
|
|
9724
9835
|
const issues = computeIssues(pages, metrics);
|
|
9836
|
+
const linkReport = buildLinkReport(edges, [...metrics.values()], job.startUrl);
|
|
9725
9837
|
let reportMd = renderIssueReport(job.startUrl, pages, issues, metrics);
|
|
9726
9838
|
if (imageAudit) reportMd += `
|
|
9727
9839
|
|
|
@@ -9737,7 +9849,10 @@ ${renderImageSection(imageAudit)}`;
|
|
|
9737
9849
|
{ name: "issues.json", body: JSON.stringify(issues, null, 2), type: "application/json" },
|
|
9738
9850
|
{ name: "pages.jsonl", body: jsonl(pageRows), type: "application/x-ndjson" },
|
|
9739
9851
|
{ name: "links.jsonl", body: jsonl(edges), type: "application/x-ndjson" },
|
|
9740
|
-
{ name: "link-metrics.jsonl", body: jsonl([...metrics.values()]), type: "application/x-ndjson" }
|
|
9852
|
+
{ name: "link-metrics.jsonl", body: jsonl([...metrics.values()]), type: "application/x-ndjson" },
|
|
9853
|
+
{ name: "link-report.md", body: renderLinkReport(linkReport), type: "text/markdown" },
|
|
9854
|
+
{ name: "links-summary.json", body: JSON.stringify(linkReport.summary, null, 2), type: "application/json" },
|
|
9855
|
+
{ name: "external-domains.json", body: JSON.stringify(linkReport.externalDomains, null, 2), type: "application/json" }
|
|
9741
9856
|
];
|
|
9742
9857
|
if (imageAudit) {
|
|
9743
9858
|
files.push({ name: "images.jsonl", body: jsonl(imageAudit.rows), type: "application/x-ndjson" });
|
|
@@ -9753,6 +9868,7 @@ var init_site_extract = __esm({
|
|
|
9753
9868
|
init_client();
|
|
9754
9869
|
init_site_extractor();
|
|
9755
9870
|
init_seo_link_graph();
|
|
9871
|
+
init_seo_link_report();
|
|
9756
9872
|
init_seo_issues();
|
|
9757
9873
|
init_image_audit();
|
|
9758
9874
|
init_blob_store();
|
|
@@ -9788,18 +9904,62 @@ var init_site_extract = __esm({
|
|
|
9788
9904
|
const maxPages = Number(job.options.maxPages ?? 1e4);
|
|
9789
9905
|
const concurrency = Number(job.options.concurrency ?? 1);
|
|
9790
9906
|
const urlsPerBrowser = Number(job.options.urlsPerBrowser ?? job.options.rotateProxyEvery ?? 10);
|
|
9791
|
-
const
|
|
9907
|
+
const seed = await step.run("discover", async () => {
|
|
9792
9908
|
const discovered = await discoverUrls(job.startUrl, maxPages, key);
|
|
9793
|
-
|
|
9794
|
-
|
|
9909
|
+
const out = discovered.length ? discovered : [job.startUrl];
|
|
9910
|
+
await setExtractJobTotal(jobId, out.length);
|
|
9911
|
+
return out;
|
|
9795
9912
|
});
|
|
9796
|
-
const
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
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 });
|
|
9800
9945
|
await saveExtractPages(jobId, pages);
|
|
9801
|
-
|
|
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)];
|
|
9802
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));
|
|
9803
9963
|
}
|
|
9804
9964
|
const branding = Array.isArray(job.options.formats) && job.options.formats.includes("branding") ? await step.run("branding", () => extractBranding(job.startUrl, key).catch(() => null)) : null;
|
|
9805
9965
|
const imageAudit = Array.isArray(job.options.formats) && job.options.formats.includes("images") ? await step.run("image-audit", async () => {
|
|
@@ -12144,10 +12304,10 @@ function buildTranscriptMarkdown(result) {
|
|
|
12144
12304
|
if (result.chunks?.length) {
|
|
12145
12305
|
lines.push("## Timestamped Segments");
|
|
12146
12306
|
lines.push("");
|
|
12147
|
-
for (const
|
|
12148
|
-
const start = fmtTs(
|
|
12149
|
-
const end = fmtTs(
|
|
12150
|
-
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()}`);
|
|
12151
12311
|
lines.push("");
|
|
12152
12312
|
}
|
|
12153
12313
|
}
|
|
@@ -12785,20 +12945,20 @@ var init_FacebookAdExtractor = __esm({
|
|
|
12785
12945
|
if (splitRe.lastIndex === m.index) splitRe.lastIndex++;
|
|
12786
12946
|
}
|
|
12787
12947
|
if (last < bodyText.length) adChunks.push(bodyText.slice(last));
|
|
12788
|
-
const ads = adChunks.filter((c) => /Library ID[:\s]+\d{10,20}/i.test(c)).slice(0, maxAds).map((
|
|
12789
|
-
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);
|
|
12790
12950
|
const libraryId = lidM ? lidM[1] : null;
|
|
12791
|
-
const statusM =
|
|
12951
|
+
const statusM = chunk.match(/^(Active|Inactive)/i);
|
|
12792
12952
|
const status = statusM ? statusM[1] : null;
|
|
12793
|
-
const startM =
|
|
12953
|
+
const startM = chunk.match(/Started running on\s+([A-Za-z]+\.?\s+\d{1,2},?\s*\d{4})/i);
|
|
12794
12954
|
const started = startM ? startM[1].replace(/\s+/g, " ").trim() : null;
|
|
12795
12955
|
const visual = adVisuals[libraryId ?? ""] ?? { imageSrc: null, videoSrc: null, videoPoster: null };
|
|
12796
|
-
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);
|
|
12797
12957
|
const creativeType = visual.videoSrc || hasVideoText ? "video" : visual.imageSrc ? "image" : "unknown";
|
|
12798
12958
|
let domain = null;
|
|
12799
12959
|
const domainRe = /\b([A-Z0-9]{2,}(?:\.[A-Z]{2,})+)\b/g;
|
|
12800
12960
|
let dm;
|
|
12801
|
-
while ((dm = domainRe.exec(
|
|
12961
|
+
while ((dm = domainRe.exec(chunk)) !== null) {
|
|
12802
12962
|
const c = dm[1];
|
|
12803
12963
|
if (c.length >= 6 && c !== "LIBRARY") {
|
|
12804
12964
|
domain = c;
|
|
@@ -12807,9 +12967,9 @@ var init_FacebookAdExtractor = __esm({
|
|
|
12807
12967
|
}
|
|
12808
12968
|
let primaryText = null;
|
|
12809
12969
|
let headline = null;
|
|
12810
|
-
const sponsoredIdx =
|
|
12970
|
+
const sponsoredIdx = chunk.search(/\bSponsored\b/i);
|
|
12811
12971
|
if (sponsoredIdx >= 0) {
|
|
12812
|
-
let afterSponsored =
|
|
12972
|
+
let afterSponsored = chunk.slice(sponsoredIdx + "Sponsored".length).trim();
|
|
12813
12973
|
afterSponsored = afterSponsored.replace(/\s*\d*:?\d+\s*\/\s*\d*:?\d+.*$/, "");
|
|
12814
12974
|
if (domain) {
|
|
12815
12975
|
const di = afterSponsored.indexOf(domain);
|
|
@@ -12820,18 +12980,18 @@ var init_FacebookAdExtractor = __esm({
|
|
|
12820
12980
|
}
|
|
12821
12981
|
const ctaRe2 = new RegExp("\\b(" + CTA_LABELS.map((l) => l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|") + ")\\b", "i");
|
|
12822
12982
|
if (domain) {
|
|
12823
|
-
const di =
|
|
12983
|
+
const di = chunk.indexOf(domain);
|
|
12824
12984
|
if (di >= 0) {
|
|
12825
|
-
let afterDomain =
|
|
12985
|
+
let afterDomain = chunk.slice(di + domain.length).trim();
|
|
12826
12986
|
const ctaMatch = afterDomain.search(ctaRe2);
|
|
12827
12987
|
if (ctaMatch >= 0) afterDomain = afterDomain.slice(0, ctaMatch).trim();
|
|
12828
12988
|
if (afterDomain.length > 3 && afterDomain.length < 120) headline = afterDomain;
|
|
12829
12989
|
}
|
|
12830
12990
|
}
|
|
12831
12991
|
if (!headline) {
|
|
12832
|
-
const ctaIdx =
|
|
12992
|
+
const ctaIdx = chunk.search(ctaRe2);
|
|
12833
12993
|
if (ctaIdx > 0) {
|
|
12834
|
-
const beforeCta =
|
|
12994
|
+
const beforeCta = chunk.slice(0, ctaIdx).trimEnd();
|
|
12835
12995
|
const sentenceEnd = Math.max(beforeCta.lastIndexOf(". "), beforeCta.lastIndexOf("! "), beforeCta.lastIndexOf("? "));
|
|
12836
12996
|
if (sentenceEnd >= 0) {
|
|
12837
12997
|
const candidate = beforeCta.slice(sentenceEnd + 2).trim();
|
|
@@ -12843,12 +13003,12 @@ var init_FacebookAdExtractor = __esm({
|
|
|
12843
13003
|
}
|
|
12844
13004
|
let cta = null;
|
|
12845
13005
|
for (const lbl of CTA_LABELS) {
|
|
12846
|
-
if (
|
|
13006
|
+
if (chunk.toLowerCase().includes(lbl.toLowerCase())) {
|
|
12847
13007
|
cta = lbl;
|
|
12848
13008
|
break;
|
|
12849
13009
|
}
|
|
12850
13010
|
}
|
|
12851
|
-
const multiM =
|
|
13011
|
+
const multiM = chunk.match(/(\d+)\s+ad[s]?\s+use[s]?\s+this/i);
|
|
12852
13012
|
const clusterCount = multiM ? parseInt(multiM[1]) : null;
|
|
12853
13013
|
return {
|
|
12854
13014
|
libraryId,
|
|
@@ -12871,8 +13031,8 @@ var init_FacebookAdExtractor = __esm({
|
|
|
12871
13031
|
const unknownCreativeCount = ads.filter((a) => a.creativeType === "unknown").length;
|
|
12872
13032
|
const advertiserPageId = new URL(page.url()).searchParams.get("view_all_page_id") ?? null;
|
|
12873
13033
|
const nameFreq = /* @__PURE__ */ new Map();
|
|
12874
|
-
for (const
|
|
12875
|
-
const detailsParts =
|
|
13034
|
+
for (const chunk of adChunks) {
|
|
13035
|
+
const detailsParts = chunk.split("See ad details ");
|
|
12876
13036
|
if (detailsParts.length < 2) continue;
|
|
12877
13037
|
const sponsoredIdx = detailsParts[1].indexOf(" Sponsored");
|
|
12878
13038
|
if (sponsoredIdx < 0) continue;
|
|
@@ -14291,11 +14451,11 @@ var init_facebook_ad_routes = __esm({
|
|
|
14291
14451
|
}
|
|
14292
14452
|
if (last < bodyText.length) adChunks.push(bodyText.slice(last));
|
|
14293
14453
|
const advertiserMap = /* @__PURE__ */ new Map();
|
|
14294
|
-
for (const
|
|
14295
|
-
const lidM =
|
|
14454
|
+
for (const chunk of adChunks) {
|
|
14455
|
+
const lidM = chunk.match(/Library ID[:\s]+([0-9]{10,20})/i);
|
|
14296
14456
|
if (!lidM) continue;
|
|
14297
14457
|
const libraryId = lidM[1];
|
|
14298
|
-
const detailsParts =
|
|
14458
|
+
const detailsParts = chunk.split("See ad details ");
|
|
14299
14459
|
if (detailsParts.length < 2) continue;
|
|
14300
14460
|
const afterDetails = detailsParts[1];
|
|
14301
14461
|
const sponsoredIdx = afterDetails.indexOf(" Sponsored");
|
|
@@ -14819,8 +14979,8 @@ function runFfmpegMux(videoPath, audioPath, outPath) {
|
|
|
14819
14979
|
stdio: ["ignore", "ignore", "pipe"]
|
|
14820
14980
|
});
|
|
14821
14981
|
let stderr = "";
|
|
14822
|
-
child.stderr.on("data", (
|
|
14823
|
-
stderr += String(
|
|
14982
|
+
child.stderr.on("data", (chunk) => {
|
|
14983
|
+
stderr += String(chunk);
|
|
14824
14984
|
});
|
|
14825
14985
|
child.on("error", (err) => resolve({ ok: false, error: err.message }));
|
|
14826
14986
|
child.on("close", (code) => resolve({ ok: code === 0, error: code === 0 ? null : stderr.trim() || `ffmpeg exited ${code}` }));
|
|
@@ -16231,6 +16391,9 @@ function saveBulkSite(siteUrl, pages, seo, imageAudit) {
|
|
|
16231
16391
|
`- \`pages.jsonl\` \u2014 per-page SEO fields, one JSON object per page`,
|
|
16232
16392
|
`- \`links.jsonl\` \u2014 internal/external link edges (anchor, rel, target status)`,
|
|
16233
16393
|
`- \`link-metrics.jsonl\` \u2014 inlinks, crawl depth, orphan flags per page`,
|
|
16394
|
+
`- \`link-report.md\` \u2014 link analysis: top internal pages by inlinks, top external domains, distribution`,
|
|
16395
|
+
`- \`links-summary.json\` \u2014 link totals + top-20 internal pages + top-20 external domains`,
|
|
16396
|
+
`- \`external-domains.json\` \u2014 every external domain linked to, ordered by link count`,
|
|
16234
16397
|
...imageAudit ? [
|
|
16235
16398
|
`- \`images.jsonl\` \u2014 every image with byte size, format, over-100KB and legacy-format flags`,
|
|
16236
16399
|
`- \`images-summary.json\` \u2014 image totals (count, total weight, over-100KB, legacy)`
|
|
@@ -16266,7 +16429,13 @@ ${indexRows.join("\n")}`
|
|
|
16266
16429
|
(0, import_node_fs5.writeFileSync)(reportFile, seo.reportMd + (imageAudit ? `
|
|
16267
16430
|
|
|
16268
16431
|
${renderImageSection(imageAudit)}` : ""), "utf8");
|
|
16269
|
-
|
|
16432
|
+
const linkReportFile = (0, import_node_path7.join)(dir, "link-report.md");
|
|
16433
|
+
const linksSummaryFile = (0, import_node_path7.join)(dir, "links-summary.json");
|
|
16434
|
+
const externalDomainsFile = (0, import_node_path7.join)(dir, "external-domains.json");
|
|
16435
|
+
(0, import_node_fs5.writeFileSync)(linkReportFile, renderLinkReport(seo.linkReport), "utf8");
|
|
16436
|
+
(0, import_node_fs5.writeFileSync)(linksSummaryFile, JSON.stringify(seo.linkReport.summary, null, 2), "utf8");
|
|
16437
|
+
(0, import_node_fs5.writeFileSync)(externalDomainsFile, JSON.stringify(seo.linkReport.externalDomains, null, 2), "utf8");
|
|
16438
|
+
seoFiles = [pagesJsonl, linksJsonl, metricsJsonl, issuesFile, reportFile, linkReportFile, linksSummaryFile, externalDomainsFile];
|
|
16270
16439
|
if (imageAudit) {
|
|
16271
16440
|
const imagesJsonl = (0, import_node_path7.join)(dir, "images.jsonl");
|
|
16272
16441
|
const imagesSummary = (0, import_node_path7.join)(dir, "images-summary.json");
|
|
@@ -16718,6 +16887,7 @@ function buildSeoExport(siteUrl, pages, branding) {
|
|
|
16718
16887
|
metrics: [...metrics.values()],
|
|
16719
16888
|
issues,
|
|
16720
16889
|
reportMd: renderIssueReport(siteUrl, pages, issues, metrics),
|
|
16890
|
+
linkReport: buildLinkReport(edges, [...metrics.values()], siteUrl),
|
|
16721
16891
|
branding: branding ?? void 0
|
|
16722
16892
|
};
|
|
16723
16893
|
}
|
|
@@ -16826,6 +16996,9 @@ async function formatAuditSite(raw, input) {
|
|
|
16826
16996
|
const topIssues = Object.entries(seo.issues).sort((a, b) => b[1].count - a[1].count).slice(0, 8).map(([k, v]) => `- \`${k}\` \u2014 ${v.count}`).join("\n");
|
|
16827
16997
|
const img = imageAudit.summary;
|
|
16828
16998
|
const imgLine = `${img.unique} images \xB7 ${img.totalSize} total \xB7 ${img.over100kb} over 100 KB \xB7 ${img.legacyFormat} legacy format`;
|
|
16999
|
+
const lr = seo.linkReport.summary;
|
|
17000
|
+
const topExt = lr.external.topDomains.slice(0, 3).map((dm) => `${dm.domain} (${dm.links})`).join(", ") || "\u2014";
|
|
17001
|
+
const linkLine = `${lr.internal.totalLinks} internal / ${lr.external.totalLinks} external links \xB7 ${lr.internal.orphans} orphans \xB7 ${lr.internal.brokenInternal} broken \xB7 avg ${lr.internal.avgInlinks} inlinks/page \xB7 top external: ${topExt}`;
|
|
16829
17002
|
const durationLine = `**${pages.length} pages** \xB7 ${((d.durationMs ?? 0) / 1e3).toFixed(1)}s`;
|
|
16830
17003
|
const structuredContent = {
|
|
16831
17004
|
url: input.url,
|
|
@@ -16833,7 +17006,8 @@ async function formatAuditSite(raw, input) {
|
|
|
16833
17006
|
durationMs: d.durationMs ?? 0,
|
|
16834
17007
|
bulkFolder: bulk?.dir ?? null,
|
|
16835
17008
|
issues: Object.fromEntries(Object.entries(seo.issues).map(([k, v]) => [k, v.count])),
|
|
16836
|
-
images: { unique: img.unique, totalBytes: img.totalBytes, over100kb: img.over100kb, legacyFormat: img.legacyFormat }
|
|
17009
|
+
images: { unique: img.unique, totalBytes: img.totalBytes, over100kb: img.over100kb, legacyFormat: img.legacyFormat },
|
|
17010
|
+
links: { internal: lr.internal.totalLinks, external: lr.external.totalLinks, orphans: lr.internal.orphans, brokenInternal: lr.internal.brokenInternal, externalDomains: lr.external.uniqueDomains }
|
|
16837
17011
|
};
|
|
16838
17012
|
const location2 = bulk ? `
|
|
16839
17013
|
## \u{1F4C1} Technical audit saved
|
|
@@ -16852,6 +17026,9 @@ Report saving is disabled in this environment. Enable \`MCP_SCRAPER_SAVE_REPORTS
|
|
|
16852
17026
|
## Top issues
|
|
16853
17027
|
${topIssues || "_none found_"}`,
|
|
16854
17028
|
`
|
|
17029
|
+
## Links
|
|
17030
|
+
${linkLine}`,
|
|
17031
|
+
`
|
|
16855
17032
|
## Images
|
|
16856
17033
|
${imgLine}`
|
|
16857
17034
|
].join("\n");
|
|
@@ -18041,6 +18218,7 @@ var init_mcp_response_formatter = __esm({
|
|
|
18041
18218
|
init_errors();
|
|
18042
18219
|
init_workflow_catalog();
|
|
18043
18220
|
init_seo_link_graph();
|
|
18221
|
+
init_seo_link_report();
|
|
18044
18222
|
init_seo_issues();
|
|
18045
18223
|
init_image_audit();
|
|
18046
18224
|
reportSavingEnabled = true;
|
|
@@ -23577,7 +23755,7 @@ var PACKAGE_VERSION;
|
|
|
23577
23755
|
var init_version = __esm({
|
|
23578
23756
|
"src/version.ts"() {
|
|
23579
23757
|
"use strict";
|
|
23580
|
-
PACKAGE_VERSION = "0.3.
|
|
23758
|
+
PACKAGE_VERSION = "0.3.18";
|
|
23581
23759
|
}
|
|
23582
23760
|
});
|
|
23583
23761
|
|
|
@@ -23601,7 +23779,11 @@ ROUTING
|
|
|
23601
23779
|
- Workflows (multi-step): local directory build -> directory_workflow; rank blueprint -> rank_tracker_workflow;
|
|
23602
23780
|
AI-answer citation fan-out (AEO) -> query_fanout_workflow; deep research -> deep_research_workflow.
|
|
23603
23781
|
- Anything without a dedicated tool (e.g. Reddit, arbitrary logged-in sites) -> the browser_* agent
|
|
23604
|
-
(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.
|
|
23605
23787
|
|
|
23606
23788
|
NOTES
|
|
23607
23789
|
- Bulk / full-site crawls: call extract_site with rotateProxies:true for blocked or rate-limited sites.
|
|
@@ -23969,6 +24151,13 @@ var init_mcp_tool_schemas = __esm({
|
|
|
23969
24151
|
totalBytes: import_zod27.z.number().min(0),
|
|
23970
24152
|
over100kb: import_zod27.z.number().int().min(0),
|
|
23971
24153
|
legacyFormat: import_zod27.z.number().int().min(0)
|
|
24154
|
+
}),
|
|
24155
|
+
links: import_zod27.z.object({
|
|
24156
|
+
internal: import_zod27.z.number().int().min(0),
|
|
24157
|
+
external: import_zod27.z.number().int().min(0),
|
|
24158
|
+
orphans: import_zod27.z.number().int().min(0),
|
|
24159
|
+
brokenInternal: import_zod27.z.number().int().min(0),
|
|
24160
|
+
externalDomains: import_zod27.z.number().int().min(0)
|
|
23972
24161
|
})
|
|
23973
24162
|
};
|
|
23974
24163
|
MapsPlaceIntelOutputSchema = {
|
|
@@ -24828,7 +25017,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
24828
25017
|
}, async (input) => formatExtractSite(await executor.extractSite(input), input));
|
|
24829
25018
|
server.registerTool("audit_site", {
|
|
24830
25019
|
title: "Technical SEO Audit",
|
|
24831
|
-
description: withReportNote("Run a full technical SEO audit (Screaming-Frog-style) on a public website. Crawls up to maxPages and produces a complete on-page + crawl analysis: per-page title/meta lengths, heading breakdown, indexability and canonical status, schema, an internal link graph
|
|
25020
|
+
description: withReportNote("Run a full technical SEO audit (Screaming-Frog-style) on a public website. Crawls up to maxPages and produces a complete on-page + crawl analysis: per-page title/meta lengths, heading breakdown, indexability and canonical status, schema, an internal link graph (inlinks/orphans/crawl-depth), a link analysis (top internal pages by inlinks, top external domains by link count, broken/orphan counts), an issues report, and an image audit (byte size, format, over-100KB and legacy-format flags). Writes everything to a local folder (report.md, issues.json, pages.jsonl, links.jsonl, link-metrics.jsonl, link-report.md, links-summary.json, external-domains.json, images.jsonl) plus per-page content, and returns a headline summary plus the folder path. Use this when the user wants an audit or analysis; use extract_site when they just want page content."),
|
|
24832
25021
|
inputSchema: AuditSiteInputSchema,
|
|
24833
25022
|
outputSchema: AuditSiteOutputSchema,
|
|
24834
25023
|
annotations: liveWebToolAnnotations("Technical SEO Audit")
|
|
@@ -25390,8 +25579,64 @@ async function migrateBrowserAgent() {
|
|
|
25390
25579
|
)
|
|
25391
25580
|
`);
|
|
25392
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)`);
|
|
25393
25607
|
_ready = true;
|
|
25394
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
|
+
}
|
|
25395
25640
|
async function createSessionRow(input) {
|
|
25396
25641
|
const db = getDb();
|
|
25397
25642
|
const id = `bas_${(0, import_node_crypto6.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
|
|
@@ -26419,6 +26664,17 @@ async function findProfileConnection(k, profileName, domain) {
|
|
|
26419
26664
|
}
|
|
26420
26665
|
return null;
|
|
26421
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
|
+
}
|
|
26422
26678
|
async function findOrCreateProfileConnection(k, opts) {
|
|
26423
26679
|
try {
|
|
26424
26680
|
return await k.auth.connections.create({
|
|
@@ -26821,7 +27077,7 @@ function publicSession(row) {
|
|
|
26821
27077
|
session_id: row.id,
|
|
26822
27078
|
status: row.status,
|
|
26823
27079
|
label: row.label,
|
|
26824
|
-
|
|
27080
|
+
watch_url: `/console/${row.id}`,
|
|
26825
27081
|
created_at: row.created_at,
|
|
26826
27082
|
last_action_at: row.last_action_at,
|
|
26827
27083
|
closed_at: row.closed_at,
|
|
@@ -26877,10 +27133,23 @@ function buildBrowserAgentRoutes() {
|
|
|
26877
27133
|
const profileName = typeof body.profile === "string" ? body.profile.trim() : "";
|
|
26878
27134
|
const domain = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : "chatgpt.com";
|
|
26879
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;
|
|
26880
27138
|
if (!profileName) return c.json({ error: "profile is required" }, 400);
|
|
26881
27139
|
if (!domain) return c.json({ error: "domain is required" }, 400);
|
|
26882
27140
|
try {
|
|
26883
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
|
+
}
|
|
26884
27153
|
return c.json({
|
|
26885
27154
|
connection_id: result.connectionId,
|
|
26886
27155
|
profile: result.profileName,
|
|
@@ -26889,8 +27158,6 @@ function buildBrowserAgentRoutes() {
|
|
|
26889
27158
|
flow_status: result.flowStatus,
|
|
26890
27159
|
flow_step: result.flowStep,
|
|
26891
27160
|
flow_expires_at: result.flowExpiresAt,
|
|
26892
|
-
hosted_url: result.hostedUrl,
|
|
26893
|
-
live_view_url: result.liveViewUrl,
|
|
26894
27161
|
browser_session_id: result.browserSessionId,
|
|
26895
27162
|
post_login_url: result.postLoginUrl
|
|
26896
27163
|
});
|
|
@@ -26905,6 +27172,17 @@ function buildBrowserAgentRoutes() {
|
|
|
26905
27172
|
const domain = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : void 0;
|
|
26906
27173
|
try {
|
|
26907
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
|
+
}
|
|
26908
27186
|
return c.json({
|
|
26909
27187
|
connection_id: result.connectionId,
|
|
26910
27188
|
profile: result.profileName,
|
|
@@ -26913,8 +27191,6 @@ function buildBrowserAgentRoutes() {
|
|
|
26913
27191
|
flow_status: result.flowStatus,
|
|
26914
27192
|
flow_step: result.flowStep,
|
|
26915
27193
|
flow_expires_at: result.flowExpiresAt,
|
|
26916
|
-
hosted_url: result.hostedUrl,
|
|
26917
|
-
live_view_url: result.liveViewUrl,
|
|
26918
27194
|
browser_session_id: result.browserSessionId,
|
|
26919
27195
|
post_login_url: result.postLoginUrl
|
|
26920
27196
|
});
|
|
@@ -26922,6 +27198,37 @@ function buildBrowserAgentRoutes() {
|
|
|
26922
27198
|
return c.json(failure(err), 404);
|
|
26923
27199
|
}
|
|
26924
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
|
+
});
|
|
26925
27232
|
app2.post("/sessions", async (c) => {
|
|
26926
27233
|
const user = c.get("user");
|
|
26927
27234
|
if (Number(user.balance_mc ?? 0) < BROWSER_OPEN_MIN_BALANCE_MC) {
|
|
@@ -27294,6 +27601,50 @@ var init_browser_agent_routes = __esm({
|
|
|
27294
27601
|
});
|
|
27295
27602
|
|
|
27296
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
|
+
}
|
|
27297
27648
|
function renderConsoleHtml(initialSessionId) {
|
|
27298
27649
|
const initial = JSON.stringify(initialSessionId ?? "");
|
|
27299
27650
|
return `<!DOCTYPE html>
|
|
@@ -28079,6 +28430,7 @@ var init_server = __esm({
|
|
|
28079
28430
|
init_mcp_routes();
|
|
28080
28431
|
init_browser_agent_routes();
|
|
28081
28432
|
init_browser_agent_console();
|
|
28433
|
+
init_browser_agent_db();
|
|
28082
28434
|
init_stripe_routes();
|
|
28083
28435
|
init_site_audit_worker();
|
|
28084
28436
|
import_factory6 = require("hono/factory");
|
|
@@ -28907,6 +29259,19 @@ var init_server = __esm({
|
|
|
28907
29259
|
app.route("/mcp", mcpApp);
|
|
28908
29260
|
app.route("/agent", buildBrowserAgentRoutes());
|
|
28909
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
|
+
});
|
|
28910
29275
|
app.get("/console/:id", (c) => c.html(renderConsoleHtml(c.req.param("id"))));
|
|
28911
29276
|
app.route("/stripe", stripeApp);
|
|
28912
29277
|
if (!process.env.INNGEST_EVENT_KEY) {
|