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.
Files changed (62) hide show
  1. package/README.md +26 -58
  2. package/dist/bin/api-server.cjs +418 -53
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +2 -2
  5. package/dist/bin/mcp-scraper-cli.cjs +4 -4
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +5 -5
  8. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs +12 -12
  10. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  11. package/dist/bin/mcp-scraper-install.js +3 -3
  12. package/dist/bin/mcp-scraper-install.js.map +1 -1
  13. package/dist/bin/mcp-stdio-server.cjs +2554 -781
  14. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  15. package/dist/bin/mcp-stdio-server.js +1377 -9
  16. package/dist/bin/mcp-stdio-server.js.map +1 -1
  17. package/dist/bin/paa-harvest.js +2 -3
  18. package/dist/bin/paa-harvest.js.map +1 -1
  19. package/dist/{chunk-7XARJHS6.js → chunk-2XCNU22L.js} +332 -13
  20. package/dist/chunk-2XCNU22L.js.map +1 -0
  21. package/dist/chunk-C7ZNIXOA.js +7 -0
  22. package/dist/chunk-C7ZNIXOA.js.map +1 -0
  23. package/dist/{chunk-ACIUCZ27.js → chunk-DE7WP4UU.js} +11 -11
  24. package/dist/chunk-DE7WP4UU.js.map +1 -0
  25. package/dist/{chunk-ZAUMSBV3.js → chunk-HDPWLYK7.js} +6 -6
  26. package/dist/{chunk-4OPKIDON.js → chunk-IGOEAKC7.js} +4 -6
  27. package/dist/{chunk-4OPKIDON.js.map → chunk-IGOEAKC7.js.map} +1 -1
  28. package/dist/{chunk-M2S27J6Z.js → chunk-ZAP7MYVO.js} +26 -1
  29. package/dist/chunk-ZAP7MYVO.js.map +1 -0
  30. package/dist/index.js +2 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/{server-LPVBSE2E.js → server-3M3XCVOT.js} +328 -157
  33. package/dist/server-3M3XCVOT.js.map +1 -0
  34. package/dist/{worker-2WVKKCC7.js → worker-J6I5XP2F.js} +9 -10
  35. package/dist/{worker-2WVKKCC7.js.map → worker-J6I5XP2F.js.map} +1 -1
  36. package/docs/mcp-tool-manifest.generated.json +28 -27
  37. package/package.json +3 -3
  38. package/dist/bin/browser-agent-stdio-server.cjs +0 -1523
  39. package/dist/bin/browser-agent-stdio-server.cjs.map +0 -1
  40. package/dist/bin/browser-agent-stdio-server.d.cts +0 -1
  41. package/dist/bin/browser-agent-stdio-server.d.ts +0 -1
  42. package/dist/bin/browser-agent-stdio-server.js +0 -43
  43. package/dist/bin/browser-agent-stdio-server.js.map +0 -1
  44. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +0 -5601
  45. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +0 -1
  46. package/dist/bin/mcp-scraper-combined-stdio-server.d.cts +0 -1
  47. package/dist/bin/mcp-scraper-combined-stdio-server.d.ts +0 -1
  48. package/dist/bin/mcp-scraper-combined-stdio-server.js +0 -70
  49. package/dist/bin/mcp-scraper-combined-stdio-server.js.map +0 -1
  50. package/dist/chunk-3YGKXXUG.js +0 -133
  51. package/dist/chunk-3YGKXXUG.js.map +0 -1
  52. package/dist/chunk-7XARJHS6.js.map +0 -1
  53. package/dist/chunk-ACIUCZ27.js.map +0 -1
  54. package/dist/chunk-ISZWGIWL.js +0 -1332
  55. package/dist/chunk-ISZWGIWL.js.map +0 -1
  56. package/dist/chunk-M2S27J6Z.js.map +0 -1
  57. package/dist/chunk-SOMBZK3V.js +0 -7
  58. package/dist/chunk-SOMBZK3V.js.map +0 -1
  59. package/dist/chunk-WN7PBKMV.js +0 -28
  60. package/dist/chunk-WN7PBKMV.js.map +0 -1
  61. package/dist/server-LPVBSE2E.js.map +0 -1
  62. /package/dist/{chunk-ZAUMSBV3.js.map → chunk-HDPWLYK7.js.map} +0 -0
@@ -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
- if (SANITIZED_VALUE_KEYS.test(parentKey) && /kernel/i.test(value)) {
123
- return sanitizeVendorName(value);
125
+ let out = redactVendorUrls(value);
126
+ if (SANITIZED_VALUE_KEYS.test(parentKey) && /kernel/i.test(out)) {
127
+ out = sanitizeVendorName(out);
124
128
  }
125
- return value;
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 urls = await step.run("discover", async () => {
9907
+ const seed = await step.run("discover", async () => {
9792
9908
  const discovered = await discoverUrls(job.startUrl, maxPages, key);
9793
- await setExtractJobTotal(jobId, discovered.length);
9794
- return discovered;
9909
+ const out = discovered.length ? discovered : [job.startUrl];
9910
+ await setExtractJobTotal(jobId, out.length);
9911
+ return out;
9795
9912
  });
9796
- const batches = chunk(urls, BATCH);
9797
- for (let i = 0; i < batches.length; i++) {
9798
- await step.run(`crawl-batch-${i}`, async () => {
9799
- const pages = await extractPagesRotating(batches[i], { kernelApiKey: key, concurrency, urlsPerBrowser });
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
- return pages.length;
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 chunk2 of result.chunks) {
12148
- const start = fmtTs(chunk2.timestamp[0]);
12149
- const end = fmtTs(chunk2.timestamp[1]);
12150
- lines.push(`**[${start} \u2192 ${end}]** ${chunk2.text.trim()}`);
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((chunk2) => {
12789
- const lidM = chunk2.match(/Library ID[:\s]+([0-9]{10,20})/i);
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 = chunk2.match(/^(Active|Inactive)/i);
12951
+ const statusM = chunk.match(/^(Active|Inactive)/i);
12792
12952
  const status = statusM ? statusM[1] : null;
12793
- const startM = chunk2.match(/Started running on\s+([A-Za-z]+\.?\s+\d{1,2},?\s*\d{4})/i);
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(chunk2);
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(chunk2)) !== null) {
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 = chunk2.search(/\bSponsored\b/i);
12970
+ const sponsoredIdx = chunk.search(/\bSponsored\b/i);
12811
12971
  if (sponsoredIdx >= 0) {
12812
- let afterSponsored = chunk2.slice(sponsoredIdx + "Sponsored".length).trim();
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 = chunk2.indexOf(domain);
12983
+ const di = chunk.indexOf(domain);
12824
12984
  if (di >= 0) {
12825
- let afterDomain = chunk2.slice(di + domain.length).trim();
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 = chunk2.search(ctaRe2);
12992
+ const ctaIdx = chunk.search(ctaRe2);
12833
12993
  if (ctaIdx > 0) {
12834
- const beforeCta = chunk2.slice(0, ctaIdx).trimEnd();
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 (chunk2.toLowerCase().includes(lbl.toLowerCase())) {
13006
+ if (chunk.toLowerCase().includes(lbl.toLowerCase())) {
12847
13007
  cta = lbl;
12848
13008
  break;
12849
13009
  }
12850
13010
  }
12851
- const multiM = chunk2.match(/(\d+)\s+ad[s]?\s+use[s]?\s+this/i);
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 chunk2 of adChunks) {
12875
- const detailsParts = chunk2.split("See ad details ");
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 chunk2 of adChunks) {
14295
- const lidM = chunk2.match(/Library ID[:\s]+([0-9]{10,20})/i);
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 = chunk2.split("See ad details ");
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", (chunk2) => {
14823
- stderr += String(chunk2);
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
- seoFiles = [pagesJsonl, linksJsonl, metricsJsonl, issuesFile, reportFile];
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.16";
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); save/reuse logins via browser_profile_*.
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 with inlinks/orphans/crawl-depth, 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, 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."),
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
- live_view_url: row.live_view_url,
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[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) {