mcp-scraper 0.3.17 → 0.3.19

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 +346 -60
  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 +2441 -785
  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-N33YEY6W.js → chunk-A46NKAAM.js} +210 -10
  20. package/dist/{chunk-N33YEY6W.js.map → chunk-A46NKAAM.js.map} +1 -1
  21. package/dist/chunk-AUKOY2IV.js +7 -0
  22. package/dist/chunk-AUKOY2IV.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-4OPKIDON.js → chunk-IGOEAKC7.js} +4 -6
  26. package/dist/{chunk-4OPKIDON.js.map → chunk-IGOEAKC7.js.map} +1 -1
  27. package/dist/{chunk-ZAUMSBV3.js → chunk-T337IGVE.js} +8 -8
  28. package/dist/chunk-T337IGVE.js.map +1 -0
  29. package/dist/{chunk-M2S27J6Z.js → chunk-ZAP7MYVO.js} +26 -1
  30. package/dist/chunk-ZAP7MYVO.js.map +1 -0
  31. package/dist/index.js +2 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/{server-TKWY47JA.js → server-EYNMXWUJ.js} +379 -167
  34. package/dist/server-EYNMXWUJ.js.map +1 -0
  35. package/dist/{worker-2WVKKCC7.js → worker-YZ2ZJE4F.js} +9 -10
  36. package/dist/{worker-2WVKKCC7.js.map → worker-YZ2ZJE4F.js.map} +1 -1
  37. package/docs/mcp-tool-manifest.generated.json +28 -27
  38. package/package.json +3 -3
  39. package/dist/bin/browser-agent-stdio-server.cjs +0 -1523
  40. package/dist/bin/browser-agent-stdio-server.cjs.map +0 -1
  41. package/dist/bin/browser-agent-stdio-server.d.cts +0 -1
  42. package/dist/bin/browser-agent-stdio-server.d.ts +0 -1
  43. package/dist/bin/browser-agent-stdio-server.js +0 -43
  44. package/dist/bin/browser-agent-stdio-server.js.map +0 -1
  45. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +0 -5718
  46. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +0 -1
  47. package/dist/bin/mcp-scraper-combined-stdio-server.d.cts +0 -1
  48. package/dist/bin/mcp-scraper-combined-stdio-server.d.ts +0 -1
  49. package/dist/bin/mcp-scraper-combined-stdio-server.js +0 -70
  50. package/dist/bin/mcp-scraper-combined-stdio-server.js.map +0 -1
  51. package/dist/chunk-3YGKXXUG.js +0 -133
  52. package/dist/chunk-3YGKXXUG.js.map +0 -1
  53. package/dist/chunk-ACIUCZ27.js.map +0 -1
  54. package/dist/chunk-M2S27J6Z.js.map +0 -1
  55. package/dist/chunk-PVWWY5NV.js +0 -7
  56. package/dist/chunk-PVWWY5NV.js.map +0 -1
  57. package/dist/chunk-UFD7N36R.js +0 -1332
  58. package/dist/chunk-UFD7N36R.js.map +0 -1
  59. package/dist/chunk-WN7PBKMV.js +0 -28
  60. package/dist/chunk-WN7PBKMV.js.map +0 -1
  61. package/dist/chunk-ZAUMSBV3.js.map +0 -1
  62. package/dist/server-TKWY47JA.js.map +0 -1
@@ -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
 
@@ -9643,8 +9661,8 @@ var init_rates = __esm({
9643
9661
  label: "Interactive browser session",
9644
9662
  aliases: ["browser_open", "browser agent", "browser_agent", "live browser", "browse", "browser control", "interactive browser"],
9645
9663
  credits: mcToCredits(MC_COSTS.browser_minute),
9646
- unit: "per minute of active time",
9647
- notes: "Metered per second of active browser work (navigation, clicks, typing, screenshots). Idle and standby time are free. Billed against your balance as you act; close the session to stop the meter."
9664
+ unit: "per minute the browser is open",
9665
+ notes: "Metered for the full time the browser session is open (active or idle) \u2014 every minute the session stays alive is billed. Close the session to stop the meter; abandoned sessions are auto-closed after a short idle window."
9648
9666
  }
9649
9667
  ];
9650
9668
  CONCURRENCY_PRICE_ID = "price_1Ta1NRS8aAcsk3TGwsRnYbix";
@@ -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 urls = await step.run("discover", async () => {
9907
+ const seed = await step.run("discover", async () => {
9895
9908
  const discovered = await discoverUrls(job.startUrl, maxPages, key);
9896
- await setExtractJobTotal(jobId, discovered.length);
9897
- return discovered;
9909
+ const out = discovered.length ? discovered : [job.startUrl];
9910
+ await setExtractJobTotal(jobId, out.length);
9911
+ return out;
9898
9912
  });
9899
- const batches = chunk(urls, BATCH);
9900
- for (let i = 0; i < batches.length; i++) {
9901
- await step.run(`crawl-batch-${i}`, async () => {
9902
- 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 });
9903
9945
  await saveExtractPages(jobId, pages);
9904
- 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)];
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 chunk2 of result.chunks) {
12251
- const start = fmtTs(chunk2.timestamp[0]);
12252
- const end = fmtTs(chunk2.timestamp[1]);
12253
- 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()}`);
12254
12311
  lines.push("");
12255
12312
  }
12256
12313
  }
@@ -12421,6 +12478,8 @@ var init_screenshot_routes = __esm({
12421
12478
  init_api_auth();
12422
12479
  init_url_utils();
12423
12480
  init_concurrency_gates();
12481
+ init_db();
12482
+ init_rates();
12424
12483
  ScreenshotBodySchema = import_zod14.z.object({
12425
12484
  url: import_zod14.z.string().trim().min(1, "url is required"),
12426
12485
  device: import_zod14.z.string().trim().optional(),
@@ -12459,7 +12518,11 @@ var init_screenshot_routes = __esm({
12459
12518
  metadata: { url: targetUrl, device }
12460
12519
  });
12461
12520
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
12521
+ let debited = false;
12462
12522
  try {
12523
+ const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.page_scrape, LedgerOperation.EXTRACT_URL, targetUrl);
12524
+ if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.page_scrape), 402);
12525
+ debited = true;
12463
12526
  const buf = await captureScreenshot(targetUrl, browserServiceApiKey(), device);
12464
12527
  return new Response(new Uint8Array(buf), {
12465
12528
  status: 200,
@@ -12470,6 +12533,7 @@ var init_screenshot_routes = __esm({
12470
12533
  }
12471
12534
  });
12472
12535
  } catch (err) {
12536
+ if (debited) await creditMc(user.id, MC_COSTS.page_scrape, LedgerOperation.EXTRACT_URL, "failed screenshot refund");
12473
12537
  const msg = err instanceof Error ? err.message : String(err);
12474
12538
  return c.json({ error: msg }, 502);
12475
12539
  } finally {
@@ -12888,20 +12952,20 @@ var init_FacebookAdExtractor = __esm({
12888
12952
  if (splitRe.lastIndex === m.index) splitRe.lastIndex++;
12889
12953
  }
12890
12954
  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((chunk2) => {
12892
- const lidM = chunk2.match(/Library ID[:\s]+([0-9]{10,20})/i);
12955
+ const ads = adChunks.filter((c) => /Library ID[:\s]+\d{10,20}/i.test(c)).slice(0, maxAds).map((chunk) => {
12956
+ const lidM = chunk.match(/Library ID[:\s]+([0-9]{10,20})/i);
12893
12957
  const libraryId = lidM ? lidM[1] : null;
12894
- const statusM = chunk2.match(/^(Active|Inactive)/i);
12958
+ const statusM = chunk.match(/^(Active|Inactive)/i);
12895
12959
  const status = statusM ? statusM[1] : null;
12896
- const startM = chunk2.match(/Started running on\s+([A-Za-z]+\.?\s+\d{1,2},?\s*\d{4})/i);
12960
+ const startM = chunk.match(/Started running on\s+([A-Za-z]+\.?\s+\d{1,2},?\s*\d{4})/i);
12897
12961
  const started = startM ? startM[1].replace(/\s+/g, " ").trim() : null;
12898
12962
  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(chunk2);
12963
+ const hasVideoText = /0:00\s*\/\s*0:00|\d+:\d+\s*\/\s*\d+:\d+/.test(chunk);
12900
12964
  const creativeType = visual.videoSrc || hasVideoText ? "video" : visual.imageSrc ? "image" : "unknown";
12901
12965
  let domain = null;
12902
12966
  const domainRe = /\b([A-Z0-9]{2,}(?:\.[A-Z]{2,})+)\b/g;
12903
12967
  let dm;
12904
- while ((dm = domainRe.exec(chunk2)) !== null) {
12968
+ while ((dm = domainRe.exec(chunk)) !== null) {
12905
12969
  const c = dm[1];
12906
12970
  if (c.length >= 6 && c !== "LIBRARY") {
12907
12971
  domain = c;
@@ -12910,9 +12974,9 @@ var init_FacebookAdExtractor = __esm({
12910
12974
  }
12911
12975
  let primaryText = null;
12912
12976
  let headline = null;
12913
- const sponsoredIdx = chunk2.search(/\bSponsored\b/i);
12977
+ const sponsoredIdx = chunk.search(/\bSponsored\b/i);
12914
12978
  if (sponsoredIdx >= 0) {
12915
- let afterSponsored = chunk2.slice(sponsoredIdx + "Sponsored".length).trim();
12979
+ let afterSponsored = chunk.slice(sponsoredIdx + "Sponsored".length).trim();
12916
12980
  afterSponsored = afterSponsored.replace(/\s*\d*:?\d+\s*\/\s*\d*:?\d+.*$/, "");
12917
12981
  if (domain) {
12918
12982
  const di = afterSponsored.indexOf(domain);
@@ -12923,18 +12987,18 @@ var init_FacebookAdExtractor = __esm({
12923
12987
  }
12924
12988
  const ctaRe2 = new RegExp("\\b(" + CTA_LABELS.map((l) => l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|") + ")\\b", "i");
12925
12989
  if (domain) {
12926
- const di = chunk2.indexOf(domain);
12990
+ const di = chunk.indexOf(domain);
12927
12991
  if (di >= 0) {
12928
- let afterDomain = chunk2.slice(di + domain.length).trim();
12992
+ let afterDomain = chunk.slice(di + domain.length).trim();
12929
12993
  const ctaMatch = afterDomain.search(ctaRe2);
12930
12994
  if (ctaMatch >= 0) afterDomain = afterDomain.slice(0, ctaMatch).trim();
12931
12995
  if (afterDomain.length > 3 && afterDomain.length < 120) headline = afterDomain;
12932
12996
  }
12933
12997
  }
12934
12998
  if (!headline) {
12935
- const ctaIdx = chunk2.search(ctaRe2);
12999
+ const ctaIdx = chunk.search(ctaRe2);
12936
13000
  if (ctaIdx > 0) {
12937
- const beforeCta = chunk2.slice(0, ctaIdx).trimEnd();
13001
+ const beforeCta = chunk.slice(0, ctaIdx).trimEnd();
12938
13002
  const sentenceEnd = Math.max(beforeCta.lastIndexOf(". "), beforeCta.lastIndexOf("! "), beforeCta.lastIndexOf("? "));
12939
13003
  if (sentenceEnd >= 0) {
12940
13004
  const candidate = beforeCta.slice(sentenceEnd + 2).trim();
@@ -12946,12 +13010,12 @@ var init_FacebookAdExtractor = __esm({
12946
13010
  }
12947
13011
  let cta = null;
12948
13012
  for (const lbl of CTA_LABELS) {
12949
- if (chunk2.toLowerCase().includes(lbl.toLowerCase())) {
13013
+ if (chunk.toLowerCase().includes(lbl.toLowerCase())) {
12950
13014
  cta = lbl;
12951
13015
  break;
12952
13016
  }
12953
13017
  }
12954
- const multiM = chunk2.match(/(\d+)\s+ad[s]?\s+use[s]?\s+this/i);
13018
+ const multiM = chunk.match(/(\d+)\s+ad[s]?\s+use[s]?\s+this/i);
12955
13019
  const clusterCount = multiM ? parseInt(multiM[1]) : null;
12956
13020
  return {
12957
13021
  libraryId,
@@ -12974,8 +13038,8 @@ var init_FacebookAdExtractor = __esm({
12974
13038
  const unknownCreativeCount = ads.filter((a) => a.creativeType === "unknown").length;
12975
13039
  const advertiserPageId = new URL(page.url()).searchParams.get("view_all_page_id") ?? null;
12976
13040
  const nameFreq = /* @__PURE__ */ new Map();
12977
- for (const chunk2 of adChunks) {
12978
- const detailsParts = chunk2.split("See ad details ");
13041
+ for (const chunk of adChunks) {
13042
+ const detailsParts = chunk.split("See ad details ");
12979
13043
  if (detailsParts.length < 2) continue;
12980
13044
  const sponsoredIdx = detailsParts[1].indexOf(" Sponsored");
12981
13045
  if (sponsoredIdx < 0) continue;
@@ -14394,11 +14458,11 @@ var init_facebook_ad_routes = __esm({
14394
14458
  }
14395
14459
  if (last < bodyText.length) adChunks.push(bodyText.slice(last));
14396
14460
  const advertiserMap = /* @__PURE__ */ new Map();
14397
- for (const chunk2 of adChunks) {
14398
- const lidM = chunk2.match(/Library ID[:\s]+([0-9]{10,20})/i);
14461
+ for (const chunk of adChunks) {
14462
+ const lidM = chunk.match(/Library ID[:\s]+([0-9]{10,20})/i);
14399
14463
  if (!lidM) continue;
14400
14464
  const libraryId = lidM[1];
14401
- const detailsParts = chunk2.split("See ad details ");
14465
+ const detailsParts = chunk.split("See ad details ");
14402
14466
  if (detailsParts.length < 2) continue;
14403
14467
  const afterDetails = detailsParts[1];
14404
14468
  const sponsoredIdx = afterDetails.indexOf(" Sponsored");
@@ -14922,8 +14986,8 @@ function runFfmpegMux(videoPath, audioPath, outPath) {
14922
14986
  stdio: ["ignore", "ignore", "pipe"]
14923
14987
  });
14924
14988
  let stderr = "";
14925
- child.stderr.on("data", (chunk2) => {
14926
- stderr += String(chunk2);
14989
+ child.stderr.on("data", (chunk) => {
14990
+ stderr += String(chunk);
14927
14991
  });
14928
14992
  child.on("error", (err) => resolve({ ok: false, error: err.message }));
14929
14993
  child.on("close", (code) => resolve({ ok: code === 0, error: code === 0 ? null : stderr.trim() || `ffmpeg exited ${code}` }));
@@ -23698,7 +23762,7 @@ var PACKAGE_VERSION;
23698
23762
  var init_version = __esm({
23699
23763
  "src/version.ts"() {
23700
23764
  "use strict";
23701
- PACKAGE_VERSION = "0.3.17";
23765
+ PACKAGE_VERSION = "0.3.19";
23702
23766
  }
23703
23767
  });
23704
23768
 
@@ -23722,7 +23786,11 @@ ROUTING
23722
23786
  - Workflows (multi-step): local directory build -> directory_workflow; rank blueprint -> rank_tracker_workflow;
23723
23787
  AI-answer citation fan-out (AEO) -> query_fanout_workflow; deep research -> deep_research_workflow.
23724
23788
  - Anything without a dedicated tool (e.g. Reddit, arbitrary logged-in sites) -> the browser_* agent
23725
- (browser_open, then navigate/read); save/reuse logins via browser_profile_*.
23789
+ (browser_open, then navigate/read).
23790
+ - Logged-in sites (ChatGPT, Claude, Reddit, any account): save the login first -> browser_profile_connect
23791
+ (returns an mcpscraper.dev sign-in link for the user; one profile holds MANY logins, call it again with
23792
+ the same profile + a new domain to add accounts) -> poll browser_profile_list until AUTHENTICATED ->
23793
+ browser_open with that profile. browser_profile_list also shows what a profile is connected to.
23726
23794
 
23727
23795
  NOTES
23728
23796
  - Bulk / full-site crawls: call extract_site with rotateProxies:true for blocked or rate-limited sites.
@@ -25518,8 +25586,64 @@ async function migrateBrowserAgent() {
25518
25586
  )
25519
25587
  `);
25520
25588
  await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_replays_session ON browser_agent_replays(session_id)`);
25589
+ await db.execute(`
25590
+ CREATE TABLE IF NOT EXISTS browser_auth_connections (
25591
+ connection_id TEXT PRIMARY KEY,
25592
+ hosted_url TEXT,
25593
+ domain TEXT,
25594
+ profile TEXT,
25595
+ account_email TEXT,
25596
+ note TEXT,
25597
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
25598
+ updated_at TEXT
25599
+ )
25600
+ `);
25601
+ try {
25602
+ await db.execute(`ALTER TABLE browser_auth_connections ADD COLUMN account_email TEXT`);
25603
+ } catch {
25604
+ }
25605
+ try {
25606
+ await db.execute(`ALTER TABLE browser_auth_connections ADD COLUMN note TEXT`);
25607
+ } catch {
25608
+ }
25609
+ try {
25610
+ await db.execute(`ALTER TABLE browser_auth_connections ADD COLUMN updated_at TEXT`);
25611
+ } catch {
25612
+ }
25613
+ await db.execute(`CREATE INDEX IF NOT EXISTS browser_auth_connections_profile ON browser_auth_connections(profile)`);
25521
25614
  _ready = true;
25522
25615
  }
25616
+ async function saveAuthConnection(input) {
25617
+ if (!input.connectionId) return;
25618
+ const db = getDb();
25619
+ await db.execute({
25620
+ sql: `INSERT INTO browser_auth_connections (connection_id, hosted_url, domain, profile, account_email, note, updated_at)
25621
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
25622
+ ON CONFLICT(connection_id) DO UPDATE SET
25623
+ hosted_url = COALESCE(excluded.hosted_url, browser_auth_connections.hosted_url),
25624
+ domain = COALESCE(excluded.domain, browser_auth_connections.domain),
25625
+ profile = COALESCE(excluded.profile, browser_auth_connections.profile),
25626
+ account_email = COALESCE(excluded.account_email, browser_auth_connections.account_email),
25627
+ note = COALESCE(excluded.note, browser_auth_connections.note),
25628
+ updated_at = datetime('now')`,
25629
+ args: [input.connectionId, input.hostedUrl, input.domain, input.profile, input.accountEmail ?? null, input.note ?? null]
25630
+ });
25631
+ }
25632
+ async function getAuthConnectionRow(connectionId) {
25633
+ if (!connectionId) return null;
25634
+ const db = getDb();
25635
+ const res = await db.execute({ sql: `SELECT * FROM browser_auth_connections WHERE connection_id = ?`, args: [connectionId] });
25636
+ return res.rows[0] ?? null;
25637
+ }
25638
+ async function listAuthConnectionsByProfile(profile) {
25639
+ if (!profile) return [];
25640
+ const db = getDb();
25641
+ const res = await db.execute({
25642
+ sql: `SELECT * FROM browser_auth_connections WHERE profile = ? ORDER BY COALESCE(updated_at, created_at) DESC`,
25643
+ args: [profile]
25644
+ });
25645
+ return res.rows;
25646
+ }
25523
25647
  async function createSessionRow(input) {
25524
25648
  const db = getDb();
25525
25649
  const id = `bas_${(0, import_node_crypto6.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
@@ -25553,6 +25677,14 @@ async function listSessionRows(userId, includeClosed = false) {
25553
25677
  });
25554
25678
  return res.rows;
25555
25679
  }
25680
+ async function listIdleOpenSessionRows(idleSeconds) {
25681
+ const db = getDb();
25682
+ const res = await db.execute({
25683
+ sql: `SELECT * FROM browser_agent_sessions WHERE status = 'open' AND COALESCE(last_action_at, created_at) <= datetime('now', ?) ORDER BY created_at LIMIT 200`,
25684
+ args: [`-${Math.max(1, Math.round(idleSeconds))} seconds`]
25685
+ });
25686
+ return res.rows;
25687
+ }
25556
25688
  async function addActiveMs(id, deltaMs) {
25557
25689
  const db = getDb();
25558
25690
  await db.execute({
@@ -26547,6 +26679,17 @@ async function findProfileConnection(k, profileName, domain) {
26547
26679
  }
26548
26680
  return null;
26549
26681
  }
26682
+ async function listProfileConnections(profileName) {
26683
+ if (!profileName) return [];
26684
+ const k = client();
26685
+ const out = [];
26686
+ for await (const connection of k.auth.connections.list({ profile_name: profileName })) {
26687
+ const data = connection;
26688
+ if (data.profile_name && data.profile_name !== profileName) continue;
26689
+ out.push(profileOnboardResult(connection));
26690
+ }
26691
+ return out;
26692
+ }
26550
26693
  async function findOrCreateProfileConnection(k, opts) {
26551
26694
  try {
26552
26695
  return await k.auth.connections.create({
@@ -26934,29 +27077,61 @@ function browserSessionLockTtlSeconds(timeoutSeconds) {
26934
27077
  }
26935
27078
  return DEFAULT_BROWSER_SESSION_LOCK_TTL_SECONDS;
26936
27079
  }
26937
- async function charge(sessionId, userId, startedAtMs) {
26938
- const elapsedMs = Date.now() - startedAtMs;
26939
- const { active_ms, billed_mc } = await addActiveMs(sessionId, elapsedMs);
26940
- const owed = browserActiveCostMc(active_ms);
26941
- const delta = owed - billed_mc;
27080
+ async function charge(sessionId, userId, _startedAtMs) {
27081
+ const row = await getSessionRow(sessionId);
27082
+ if (!row) return;
27083
+ const createdMs = parseAgentDate(row.created_at);
27084
+ if (createdMs == null) return;
27085
+ const uptimeMs = Math.max(0, Date.now() - createdMs);
27086
+ const owed = browserActiveCostMc(uptimeMs);
27087
+ await addActiveMs(sessionId, 0);
27088
+ const delta = owed - Number(row.billed_mc ?? 0);
26942
27089
  if (delta > 0) {
26943
27090
  const res = await debitMc(userId, delta, LedgerOperation.BROWSER_SESSION, sessionId);
26944
27091
  if (res.ok) await setBilledMc(sessionId, owed);
26945
27092
  }
26946
27093
  }
27094
+ async function reapIdleBrowserSessions(idleSeconds = 120) {
27095
+ const rows = await listIdleOpenSessionRows(idleSeconds);
27096
+ let reaped = 0;
27097
+ for (const row of rows) {
27098
+ if (row.user_id != null) {
27099
+ try {
27100
+ await charge(row.id, row.user_id);
27101
+ } catch {
27102
+ }
27103
+ }
27104
+ try {
27105
+ await closeSession(row.runtime_session_id);
27106
+ } catch {
27107
+ }
27108
+ await markSessionClosed(row.id).catch(() => {
27109
+ });
27110
+ if (row.concurrency_lock_id) await releaseConcurrencyGate(row.concurrency_lock_id).catch(() => {
27111
+ });
27112
+ reaped++;
27113
+ }
27114
+ return { reaped };
27115
+ }
26947
27116
  function publicSession(row) {
26948
27117
  return {
26949
27118
  session_id: row.id,
26950
27119
  status: row.status,
26951
27120
  label: row.label,
26952
- live_view_url: row.live_view_url,
27121
+ watch_url: `/console/${row.id}`,
26953
27122
  created_at: row.created_at,
26954
27123
  last_action_at: row.last_action_at,
26955
27124
  closed_at: row.closed_at,
26956
- active_seconds: Math.round((row.active_ms ?? 0) / 1e3),
27125
+ uptime_seconds: uptimeSeconds(row),
26957
27126
  credits_used: Math.round((row.billed_mc ?? 0) / 10) / 100
26958
27127
  };
26959
27128
  }
27129
+ function uptimeSeconds(row) {
27130
+ const createdMs = parseAgentDate(row.created_at);
27131
+ const endMs = row.closed_at ? parseAgentDate(row.closed_at) : Date.now();
27132
+ if (createdMs == null || endMs == null) return 0;
27133
+ return Math.max(0, Math.round((endMs - createdMs) / 1e3));
27134
+ }
26960
27135
  function failure(err) {
26961
27136
  const msg = err instanceof Error ? err.message : String(err);
26962
27137
  return { error: sanitizeVendorName(msg) };
@@ -27005,10 +27180,23 @@ function buildBrowserAgentRoutes() {
27005
27180
  const profileName = typeof body.profile === "string" ? body.profile.trim() : "";
27006
27181
  const domain = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : "chatgpt.com";
27007
27182
  const loginUrl = typeof body.login_url === "string" ? body.login_url.trim() : void 0;
27183
+ const accountEmail = typeof body.account_email === "string" ? body.account_email.trim() || null : null;
27184
+ const note = typeof body.note === "string" ? body.note.trim() || null : null;
27008
27185
  if (!profileName) return c.json({ error: "profile is required" }, 400);
27009
27186
  if (!domain) return c.json({ error: "domain is required" }, 400);
27010
27187
  try {
27011
27188
  const result = await startProfileOnboarding({ profileName, domain, loginUrl });
27189
+ try {
27190
+ await saveAuthConnection({
27191
+ connectionId: result.connectionId,
27192
+ hostedUrl: result.hostedUrl,
27193
+ domain: result.domain,
27194
+ profile: result.profileName,
27195
+ accountEmail,
27196
+ note
27197
+ });
27198
+ } catch {
27199
+ }
27012
27200
  return c.json({
27013
27201
  connection_id: result.connectionId,
27014
27202
  profile: result.profileName,
@@ -27017,8 +27205,6 @@ function buildBrowserAgentRoutes() {
27017
27205
  flow_status: result.flowStatus,
27018
27206
  flow_step: result.flowStep,
27019
27207
  flow_expires_at: result.flowExpiresAt,
27020
- hosted_url: result.hostedUrl,
27021
- live_view_url: result.liveViewUrl,
27022
27208
  browser_session_id: result.browserSessionId,
27023
27209
  post_login_url: result.postLoginUrl
27024
27210
  });
@@ -27033,6 +27219,17 @@ function buildBrowserAgentRoutes() {
27033
27219
  const domain = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : void 0;
27034
27220
  try {
27035
27221
  const result = await getProfileOnboardingStatus({ connectionId, profileName, domain });
27222
+ if (result.hostedUrl) {
27223
+ try {
27224
+ await saveAuthConnection({
27225
+ connectionId: result.connectionId,
27226
+ hostedUrl: result.hostedUrl,
27227
+ domain: result.domain,
27228
+ profile: result.profileName
27229
+ });
27230
+ } catch {
27231
+ }
27232
+ }
27036
27233
  return c.json({
27037
27234
  connection_id: result.connectionId,
27038
27235
  profile: result.profileName,
@@ -27041,8 +27238,6 @@ function buildBrowserAgentRoutes() {
27041
27238
  flow_status: result.flowStatus,
27042
27239
  flow_step: result.flowStep,
27043
27240
  flow_expires_at: result.flowExpiresAt,
27044
- hosted_url: result.hostedUrl,
27045
- live_view_url: result.liveViewUrl,
27046
27241
  browser_session_id: result.browserSessionId,
27047
27242
  post_login_url: result.postLoginUrl
27048
27243
  });
@@ -27050,6 +27245,37 @@ function buildBrowserAgentRoutes() {
27050
27245
  return c.json(failure(err), 404);
27051
27246
  }
27052
27247
  });
27248
+ app2.post("/profiles/list", async (c) => {
27249
+ const body = await c.req.json().catch(() => ({}));
27250
+ const profileName = typeof body.profile === "string" ? body.profile.trim() : "";
27251
+ if (!profileName) return c.json({ error: "profile is required" }, 400);
27252
+ const domainFilter = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : void 0;
27253
+ const connectionIdFilter = typeof body.connection_id === "string" ? body.connection_id.trim() : void 0;
27254
+ try {
27255
+ const [live, stored] = await Promise.all([
27256
+ listProfileConnections(profileName),
27257
+ listAuthConnectionsByProfile(profileName).catch(() => [])
27258
+ ]);
27259
+ const notesByDomain = new Map(stored.map((row) => [row.domain ?? "", row]));
27260
+ const notesByConnection = new Map(stored.map((row) => [row.connection_id, row]));
27261
+ let connections = live.map((conn) => {
27262
+ const record = notesByConnection.get(conn.connectionId) ?? notesByDomain.get(conn.domain);
27263
+ return {
27264
+ connection_id: conn.connectionId,
27265
+ domain: conn.domain,
27266
+ status: conn.status,
27267
+ account_email: record?.account_email ?? null,
27268
+ note: record?.note ?? null,
27269
+ last_connected_at: record?.updated_at ?? record?.created_at ?? null
27270
+ };
27271
+ });
27272
+ if (connectionIdFilter) connections = connections.filter((c2) => c2.connection_id === connectionIdFilter);
27273
+ if (domainFilter) connections = connections.filter((c2) => c2.domain === domainFilter);
27274
+ return c.json({ profile: profileName, connections });
27275
+ } catch (err) {
27276
+ return c.json(failure(err), 502);
27277
+ }
27278
+ });
27053
27279
  app2.post("/sessions", async (c) => {
27054
27280
  const user = c.get("user");
27055
27281
  if (Number(user.balance_mc ?? 0) < BROWSER_OPEN_MIN_BALANCE_MC) {
@@ -27117,6 +27343,7 @@ function buildBrowserAgentRoutes() {
27117
27343
  const user = c.get("user");
27118
27344
  const row = await loadOpenSession(c.req.param("id"), user.id);
27119
27345
  if (!row) return c.json({ error: "not found" }, 404);
27346
+ await charge(row.id, user.id);
27120
27347
  try {
27121
27348
  await closeSession(row.runtime_session_id);
27122
27349
  } catch {
@@ -27422,6 +27649,50 @@ var init_browser_agent_routes = __esm({
27422
27649
  });
27423
27650
 
27424
27651
  // src/api/browser-agent-console.ts
27652
+ function renderAuthConsoleHtml(connectionId, hostedUrl) {
27653
+ const safeId = String(connectionId).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
27654
+ const goUrl = `/console/auth/${encodeURIComponent(connectionId)}/go`;
27655
+ const body = hostedUrl ? `<iframe class="stage" allow="clipboard-read; clipboard-write" src="${goUrl}"></iframe>
27656
+ <div class="fallback">
27657
+ <span>Sign-in window not showing?</span>
27658
+ <a class="btn" href="${goUrl}" target="_blank" rel="noopener">Open sign-in in a new tab</a>
27659
+ </div>` : `<div class="empty">
27660
+ <div>This sign-in link is no longer active.</div>
27661
+ <div class="muted">Ask the assistant to reconnect the profile, then open the new link.</div>
27662
+ </div>`;
27663
+ return `<!DOCTYPE html>
27664
+ <html lang="en">
27665
+ <head>
27666
+ <meta charset="UTF-8" />
27667
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
27668
+ <title>MCP Scraper \u2014 Secure Sign-in</title>
27669
+ <style>
27670
+ :root { color-scheme: dark; }
27671
+ :where(*) { box-sizing: border-box; }
27672
+ 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; }
27673
+ header { display: flex; align-items: center; gap: 10px; padding: 12px 18px; border-bottom: 1px solid #1c2230; background: #0f131c; }
27674
+ header h1 { font-size: 14px; margin: 0; font-weight: 600; color: #fff; }
27675
+ header .muted { color: #6b7689; font-size: 12px; }
27676
+ .spacer { flex: 1; }
27677
+ .stage { flex: 1; width: 100%; border: 0; display: block; background: #05070b; }
27678
+ .fallback { display: flex; align-items: center; gap: 12px; padding: 10px 18px; border-top: 1px solid #1c2230; background: #0f131c; font-size: 13px; color: #8893a7; }
27679
+ .btn { background: #2b6cff; color: #fff; border-radius: 7px; padding: 7px 12px; text-decoration: none; font-weight: 500; }
27680
+ .empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: #8893a7; }
27681
+ .muted { color: #6b7689; font-size: 12px; }
27682
+ </style>
27683
+ ${ATTRIBUTION_PIXEL_SCRIPT}
27684
+ </head>
27685
+ <body>
27686
+ <header>
27687
+ <h1>MCP Scraper</h1>
27688
+ <span class="muted">Secure sign-in \xB7 ${safeId}</span>
27689
+ <span class="spacer"></span>
27690
+ <span class="muted">Complete the login below \u2014 your session is saved to your profile.</span>
27691
+ </header>
27692
+ ${body}
27693
+ </body>
27694
+ </html>`;
27695
+ }
27425
27696
  function renderConsoleHtml(initialSessionId) {
27426
27697
  const initial = JSON.stringify(initialSessionId ?? "");
27427
27698
  return `<!DOCTYPE html>
@@ -28207,6 +28478,7 @@ var init_server = __esm({
28207
28478
  init_mcp_routes();
28208
28479
  init_browser_agent_routes();
28209
28480
  init_browser_agent_console();
28481
+ init_browser_agent_db();
28210
28482
  init_stripe_routes();
28211
28483
  init_site_audit_worker();
28212
28484
  import_factory6 = require("hono/factory");
@@ -29016,11 +29288,12 @@ var init_server = __esm({
29016
29288
  const { drainQueue: drainQueue2 } = await Promise.resolve().then(() => (init_worker(), worker_exports));
29017
29289
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
29018
29290
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
29019
- const [results, sweepResult] = await Promise.all([
29291
+ const [results, sweepResult, reapResult] = await Promise.all([
29020
29292
  drainQueue2(budget),
29021
- runMonthlyRefreshSweep()
29293
+ runMonthlyRefreshSweep(),
29294
+ reapIdleBrowserSessions(120).catch(() => ({ reaped: 0 }))
29022
29295
  ]);
29023
- return c.json({ drained: results.length, results, sweepResult, workflowDispatch: workflowDispatchResult });
29296
+ return c.json({ drained: results.length, results, sweepResult, reaped: reapResult, workflowDispatch: workflowDispatchResult });
29024
29297
  });
29025
29298
  app.on(["GET", "POST", "PUT"], "/api/inngest", (0, import_hono14.serve)({ client: inngest, functions: [siteAuditFn, siteExtractFn] }));
29026
29299
  app.route("/api/internal/site-architecture-auditor", siteAuditApp);
@@ -29035,6 +29308,19 @@ var init_server = __esm({
29035
29308
  app.route("/mcp", mcpApp);
29036
29309
  app.route("/agent", buildBrowserAgentRoutes());
29037
29310
  app.get("/console", (c) => c.html(renderConsoleHtml()));
29311
+ app.get("/console/auth/:id", async (c) => {
29312
+ const id = c.req.param("id");
29313
+ await migrateBrowserAgent();
29314
+ const row = await getAuthConnectionRow(id);
29315
+ return c.html(renderAuthConsoleHtml(id, row?.hosted_url ?? null));
29316
+ });
29317
+ app.get("/console/auth/:id/go", async (c) => {
29318
+ const id = c.req.param("id");
29319
+ await migrateBrowserAgent();
29320
+ const row = await getAuthConnectionRow(id);
29321
+ if (!row?.hosted_url) return c.text("This sign-in link is no longer active.", 404);
29322
+ return c.redirect(row.hosted_url, 302);
29323
+ });
29038
29324
  app.get("/console/:id", (c) => c.html(renderConsoleHtml(c.req.param("id"))));
29039
29325
  app.route("/stripe", stripeApp);
29040
29326
  if (!process.env.INNGEST_EVENT_KEY) {