mcp-scraper 0.3.17 → 0.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +26 -58
  2. package/dist/bin/api-server.cjs +286 -49
  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-2XCNU22L.js} +210 -10
  20. package/dist/{chunk-N33YEY6W.js.map → chunk-2XCNU22L.js.map} +1 -1
  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-TKWY47JA.js → server-3M3XCVOT.js} +323 -158
  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 -5718
  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-ACIUCZ27.js.map +0 -1
  53. package/dist/chunk-M2S27J6Z.js.map +0 -1
  54. package/dist/chunk-PVWWY5NV.js +0 -7
  55. package/dist/chunk-PVWWY5NV.js.map +0 -1
  56. package/dist/chunk-UFD7N36R.js +0 -1332
  57. package/dist/chunk-UFD7N36R.js.map +0 -1
  58. package/dist/chunk-WN7PBKMV.js +0 -28
  59. package/dist/chunk-WN7PBKMV.js.map +0 -1
  60. package/dist/server-TKWY47JA.js.map +0 -1
  61. /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
 
@@ -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
  }
@@ -12888,20 +12945,20 @@ var init_FacebookAdExtractor = __esm({
12888
12945
  if (splitRe.lastIndex === m.index) splitRe.lastIndex++;
12889
12946
  }
12890
12947
  if (last < bodyText.length) adChunks.push(bodyText.slice(last));
12891
- const ads = adChunks.filter((c) => /Library ID[:\s]+\d{10,20}/i.test(c)).slice(0, maxAds).map((chunk2) => {
12892
- 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);
12893
12950
  const libraryId = lidM ? lidM[1] : null;
12894
- const statusM = chunk2.match(/^(Active|Inactive)/i);
12951
+ const statusM = chunk.match(/^(Active|Inactive)/i);
12895
12952
  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);
12953
+ const startM = chunk.match(/Started running on\s+([A-Za-z]+\.?\s+\d{1,2},?\s*\d{4})/i);
12897
12954
  const started = startM ? startM[1].replace(/\s+/g, " ").trim() : null;
12898
12955
  const visual = adVisuals[libraryId ?? ""] ?? { imageSrc: null, videoSrc: null, videoPoster: null };
12899
- const hasVideoText = /0:00\s*\/\s*0:00|\d+:\d+\s*\/\s*\d+:\d+/.test(chunk2);
12956
+ const hasVideoText = /0:00\s*\/\s*0:00|\d+:\d+\s*\/\s*\d+:\d+/.test(chunk);
12900
12957
  const creativeType = visual.videoSrc || hasVideoText ? "video" : visual.imageSrc ? "image" : "unknown";
12901
12958
  let domain = null;
12902
12959
  const domainRe = /\b([A-Z0-9]{2,}(?:\.[A-Z]{2,})+)\b/g;
12903
12960
  let dm;
12904
- while ((dm = domainRe.exec(chunk2)) !== null) {
12961
+ while ((dm = domainRe.exec(chunk)) !== null) {
12905
12962
  const c = dm[1];
12906
12963
  if (c.length >= 6 && c !== "LIBRARY") {
12907
12964
  domain = c;
@@ -12910,9 +12967,9 @@ var init_FacebookAdExtractor = __esm({
12910
12967
  }
12911
12968
  let primaryText = null;
12912
12969
  let headline = null;
12913
- const sponsoredIdx = chunk2.search(/\bSponsored\b/i);
12970
+ const sponsoredIdx = chunk.search(/\bSponsored\b/i);
12914
12971
  if (sponsoredIdx >= 0) {
12915
- let afterSponsored = chunk2.slice(sponsoredIdx + "Sponsored".length).trim();
12972
+ let afterSponsored = chunk.slice(sponsoredIdx + "Sponsored".length).trim();
12916
12973
  afterSponsored = afterSponsored.replace(/\s*\d*:?\d+\s*\/\s*\d*:?\d+.*$/, "");
12917
12974
  if (domain) {
12918
12975
  const di = afterSponsored.indexOf(domain);
@@ -12923,18 +12980,18 @@ var init_FacebookAdExtractor = __esm({
12923
12980
  }
12924
12981
  const ctaRe2 = new RegExp("\\b(" + CTA_LABELS.map((l) => l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|") + ")\\b", "i");
12925
12982
  if (domain) {
12926
- const di = chunk2.indexOf(domain);
12983
+ const di = chunk.indexOf(domain);
12927
12984
  if (di >= 0) {
12928
- let afterDomain = chunk2.slice(di + domain.length).trim();
12985
+ let afterDomain = chunk.slice(di + domain.length).trim();
12929
12986
  const ctaMatch = afterDomain.search(ctaRe2);
12930
12987
  if (ctaMatch >= 0) afterDomain = afterDomain.slice(0, ctaMatch).trim();
12931
12988
  if (afterDomain.length > 3 && afterDomain.length < 120) headline = afterDomain;
12932
12989
  }
12933
12990
  }
12934
12991
  if (!headline) {
12935
- const ctaIdx = chunk2.search(ctaRe2);
12992
+ const ctaIdx = chunk.search(ctaRe2);
12936
12993
  if (ctaIdx > 0) {
12937
- const beforeCta = chunk2.slice(0, ctaIdx).trimEnd();
12994
+ const beforeCta = chunk.slice(0, ctaIdx).trimEnd();
12938
12995
  const sentenceEnd = Math.max(beforeCta.lastIndexOf(". "), beforeCta.lastIndexOf("! "), beforeCta.lastIndexOf("? "));
12939
12996
  if (sentenceEnd >= 0) {
12940
12997
  const candidate = beforeCta.slice(sentenceEnd + 2).trim();
@@ -12946,12 +13003,12 @@ var init_FacebookAdExtractor = __esm({
12946
13003
  }
12947
13004
  let cta = null;
12948
13005
  for (const lbl of CTA_LABELS) {
12949
- if (chunk2.toLowerCase().includes(lbl.toLowerCase())) {
13006
+ if (chunk.toLowerCase().includes(lbl.toLowerCase())) {
12950
13007
  cta = lbl;
12951
13008
  break;
12952
13009
  }
12953
13010
  }
12954
- 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);
12955
13012
  const clusterCount = multiM ? parseInt(multiM[1]) : null;
12956
13013
  return {
12957
13014
  libraryId,
@@ -12974,8 +13031,8 @@ var init_FacebookAdExtractor = __esm({
12974
13031
  const unknownCreativeCount = ads.filter((a) => a.creativeType === "unknown").length;
12975
13032
  const advertiserPageId = new URL(page.url()).searchParams.get("view_all_page_id") ?? null;
12976
13033
  const nameFreq = /* @__PURE__ */ new Map();
12977
- for (const chunk2 of adChunks) {
12978
- const detailsParts = chunk2.split("See ad details ");
13034
+ for (const chunk of adChunks) {
13035
+ const detailsParts = chunk.split("See ad details ");
12979
13036
  if (detailsParts.length < 2) continue;
12980
13037
  const sponsoredIdx = detailsParts[1].indexOf(" Sponsored");
12981
13038
  if (sponsoredIdx < 0) continue;
@@ -14394,11 +14451,11 @@ var init_facebook_ad_routes = __esm({
14394
14451
  }
14395
14452
  if (last < bodyText.length) adChunks.push(bodyText.slice(last));
14396
14453
  const advertiserMap = /* @__PURE__ */ new Map();
14397
- for (const chunk2 of adChunks) {
14398
- 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);
14399
14456
  if (!lidM) continue;
14400
14457
  const libraryId = lidM[1];
14401
- const detailsParts = chunk2.split("See ad details ");
14458
+ const detailsParts = chunk.split("See ad details ");
14402
14459
  if (detailsParts.length < 2) continue;
14403
14460
  const afterDetails = detailsParts[1];
14404
14461
  const sponsoredIdx = afterDetails.indexOf(" Sponsored");
@@ -14922,8 +14979,8 @@ function runFfmpegMux(videoPath, audioPath, outPath) {
14922
14979
  stdio: ["ignore", "ignore", "pipe"]
14923
14980
  });
14924
14981
  let stderr = "";
14925
- child.stderr.on("data", (chunk2) => {
14926
- stderr += String(chunk2);
14982
+ child.stderr.on("data", (chunk) => {
14983
+ stderr += String(chunk);
14927
14984
  });
14928
14985
  child.on("error", (err) => resolve({ ok: false, error: err.message }));
14929
14986
  child.on("close", (code) => resolve({ ok: code === 0, error: code === 0 ? null : stderr.trim() || `ffmpeg exited ${code}` }));
@@ -23698,7 +23755,7 @@ var PACKAGE_VERSION;
23698
23755
  var init_version = __esm({
23699
23756
  "src/version.ts"() {
23700
23757
  "use strict";
23701
- PACKAGE_VERSION = "0.3.17";
23758
+ PACKAGE_VERSION = "0.3.18";
23702
23759
  }
23703
23760
  });
23704
23761
 
@@ -23722,7 +23779,11 @@ ROUTING
23722
23779
  - Workflows (multi-step): local directory build -> directory_workflow; rank blueprint -> rank_tracker_workflow;
23723
23780
  AI-answer citation fan-out (AEO) -> query_fanout_workflow; deep research -> deep_research_workflow.
23724
23781
  - Anything without a dedicated tool (e.g. Reddit, arbitrary logged-in sites) -> the browser_* agent
23725
- (browser_open, then navigate/read); 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.
23726
23787
 
23727
23788
  NOTES
23728
23789
  - Bulk / full-site crawls: call extract_site with rotateProxies:true for blocked or rate-limited sites.
@@ -25518,8 +25579,64 @@ async function migrateBrowserAgent() {
25518
25579
  )
25519
25580
  `);
25520
25581
  await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_replays_session ON browser_agent_replays(session_id)`);
25582
+ await db.execute(`
25583
+ CREATE TABLE IF NOT EXISTS browser_auth_connections (
25584
+ connection_id TEXT PRIMARY KEY,
25585
+ hosted_url TEXT,
25586
+ domain TEXT,
25587
+ profile TEXT,
25588
+ account_email TEXT,
25589
+ note TEXT,
25590
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
25591
+ updated_at TEXT
25592
+ )
25593
+ `);
25594
+ try {
25595
+ await db.execute(`ALTER TABLE browser_auth_connections ADD COLUMN account_email TEXT`);
25596
+ } catch {
25597
+ }
25598
+ try {
25599
+ await db.execute(`ALTER TABLE browser_auth_connections ADD COLUMN note TEXT`);
25600
+ } catch {
25601
+ }
25602
+ try {
25603
+ await db.execute(`ALTER TABLE browser_auth_connections ADD COLUMN updated_at TEXT`);
25604
+ } catch {
25605
+ }
25606
+ await db.execute(`CREATE INDEX IF NOT EXISTS browser_auth_connections_profile ON browser_auth_connections(profile)`);
25521
25607
  _ready = true;
25522
25608
  }
25609
+ async function saveAuthConnection(input) {
25610
+ if (!input.connectionId) return;
25611
+ const db = getDb();
25612
+ await db.execute({
25613
+ sql: `INSERT INTO browser_auth_connections (connection_id, hosted_url, domain, profile, account_email, note, updated_at)
25614
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
25615
+ ON CONFLICT(connection_id) DO UPDATE SET
25616
+ hosted_url = COALESCE(excluded.hosted_url, browser_auth_connections.hosted_url),
25617
+ domain = COALESCE(excluded.domain, browser_auth_connections.domain),
25618
+ profile = COALESCE(excluded.profile, browser_auth_connections.profile),
25619
+ account_email = COALESCE(excluded.account_email, browser_auth_connections.account_email),
25620
+ note = COALESCE(excluded.note, browser_auth_connections.note),
25621
+ updated_at = datetime('now')`,
25622
+ args: [input.connectionId, input.hostedUrl, input.domain, input.profile, input.accountEmail ?? null, input.note ?? null]
25623
+ });
25624
+ }
25625
+ async function getAuthConnectionRow(connectionId) {
25626
+ if (!connectionId) return null;
25627
+ const db = getDb();
25628
+ const res = await db.execute({ sql: `SELECT * FROM browser_auth_connections WHERE connection_id = ?`, args: [connectionId] });
25629
+ return res.rows[0] ?? null;
25630
+ }
25631
+ async function listAuthConnectionsByProfile(profile) {
25632
+ if (!profile) return [];
25633
+ const db = getDb();
25634
+ const res = await db.execute({
25635
+ sql: `SELECT * FROM browser_auth_connections WHERE profile = ? ORDER BY COALESCE(updated_at, created_at) DESC`,
25636
+ args: [profile]
25637
+ });
25638
+ return res.rows;
25639
+ }
25523
25640
  async function createSessionRow(input) {
25524
25641
  const db = getDb();
25525
25642
  const id = `bas_${(0, import_node_crypto6.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
@@ -26547,6 +26664,17 @@ async function findProfileConnection(k, profileName, domain) {
26547
26664
  }
26548
26665
  return null;
26549
26666
  }
26667
+ async function listProfileConnections(profileName) {
26668
+ if (!profileName) return [];
26669
+ const k = client();
26670
+ const out = [];
26671
+ for await (const connection of k.auth.connections.list({ profile_name: profileName })) {
26672
+ const data = connection;
26673
+ if (data.profile_name && data.profile_name !== profileName) continue;
26674
+ out.push(profileOnboardResult(connection));
26675
+ }
26676
+ return out;
26677
+ }
26550
26678
  async function findOrCreateProfileConnection(k, opts) {
26551
26679
  try {
26552
26680
  return await k.auth.connections.create({
@@ -26949,7 +27077,7 @@ function publicSession(row) {
26949
27077
  session_id: row.id,
26950
27078
  status: row.status,
26951
27079
  label: row.label,
26952
- live_view_url: row.live_view_url,
27080
+ watch_url: `/console/${row.id}`,
26953
27081
  created_at: row.created_at,
26954
27082
  last_action_at: row.last_action_at,
26955
27083
  closed_at: row.closed_at,
@@ -27005,10 +27133,23 @@ function buildBrowserAgentRoutes() {
27005
27133
  const profileName = typeof body.profile === "string" ? body.profile.trim() : "";
27006
27134
  const domain = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : "chatgpt.com";
27007
27135
  const loginUrl = typeof body.login_url === "string" ? body.login_url.trim() : void 0;
27136
+ const accountEmail = typeof body.account_email === "string" ? body.account_email.trim() || null : null;
27137
+ const note = typeof body.note === "string" ? body.note.trim() || null : null;
27008
27138
  if (!profileName) return c.json({ error: "profile is required" }, 400);
27009
27139
  if (!domain) return c.json({ error: "domain is required" }, 400);
27010
27140
  try {
27011
27141
  const result = await startProfileOnboarding({ profileName, domain, loginUrl });
27142
+ try {
27143
+ await saveAuthConnection({
27144
+ connectionId: result.connectionId,
27145
+ hostedUrl: result.hostedUrl,
27146
+ domain: result.domain,
27147
+ profile: result.profileName,
27148
+ accountEmail,
27149
+ note
27150
+ });
27151
+ } catch {
27152
+ }
27012
27153
  return c.json({
27013
27154
  connection_id: result.connectionId,
27014
27155
  profile: result.profileName,
@@ -27017,8 +27158,6 @@ function buildBrowserAgentRoutes() {
27017
27158
  flow_status: result.flowStatus,
27018
27159
  flow_step: result.flowStep,
27019
27160
  flow_expires_at: result.flowExpiresAt,
27020
- hosted_url: result.hostedUrl,
27021
- live_view_url: result.liveViewUrl,
27022
27161
  browser_session_id: result.browserSessionId,
27023
27162
  post_login_url: result.postLoginUrl
27024
27163
  });
@@ -27033,6 +27172,17 @@ function buildBrowserAgentRoutes() {
27033
27172
  const domain = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : void 0;
27034
27173
  try {
27035
27174
  const result = await getProfileOnboardingStatus({ connectionId, profileName, domain });
27175
+ if (result.hostedUrl) {
27176
+ try {
27177
+ await saveAuthConnection({
27178
+ connectionId: result.connectionId,
27179
+ hostedUrl: result.hostedUrl,
27180
+ domain: result.domain,
27181
+ profile: result.profileName
27182
+ });
27183
+ } catch {
27184
+ }
27185
+ }
27036
27186
  return c.json({
27037
27187
  connection_id: result.connectionId,
27038
27188
  profile: result.profileName,
@@ -27041,8 +27191,6 @@ function buildBrowserAgentRoutes() {
27041
27191
  flow_status: result.flowStatus,
27042
27192
  flow_step: result.flowStep,
27043
27193
  flow_expires_at: result.flowExpiresAt,
27044
- hosted_url: result.hostedUrl,
27045
- live_view_url: result.liveViewUrl,
27046
27194
  browser_session_id: result.browserSessionId,
27047
27195
  post_login_url: result.postLoginUrl
27048
27196
  });
@@ -27050,6 +27198,37 @@ function buildBrowserAgentRoutes() {
27050
27198
  return c.json(failure(err), 404);
27051
27199
  }
27052
27200
  });
27201
+ app2.post("/profiles/list", async (c) => {
27202
+ const body = await c.req.json().catch(() => ({}));
27203
+ const profileName = typeof body.profile === "string" ? body.profile.trim() : "";
27204
+ if (!profileName) return c.json({ error: "profile is required" }, 400);
27205
+ const domainFilter = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : void 0;
27206
+ const connectionIdFilter = typeof body.connection_id === "string" ? body.connection_id.trim() : void 0;
27207
+ try {
27208
+ const [live, stored] = await Promise.all([
27209
+ listProfileConnections(profileName),
27210
+ listAuthConnectionsByProfile(profileName).catch(() => [])
27211
+ ]);
27212
+ const notesByDomain = new Map(stored.map((row) => [row.domain ?? "", row]));
27213
+ const notesByConnection = new Map(stored.map((row) => [row.connection_id, row]));
27214
+ let connections = live.map((conn) => {
27215
+ const record = notesByConnection.get(conn.connectionId) ?? notesByDomain.get(conn.domain);
27216
+ return {
27217
+ connection_id: conn.connectionId,
27218
+ domain: conn.domain,
27219
+ status: conn.status,
27220
+ account_email: record?.account_email ?? null,
27221
+ note: record?.note ?? null,
27222
+ last_connected_at: record?.updated_at ?? record?.created_at ?? null
27223
+ };
27224
+ });
27225
+ if (connectionIdFilter) connections = connections.filter((c2) => c2.connection_id === connectionIdFilter);
27226
+ if (domainFilter) connections = connections.filter((c2) => c2.domain === domainFilter);
27227
+ return c.json({ profile: profileName, connections });
27228
+ } catch (err) {
27229
+ return c.json(failure(err), 502);
27230
+ }
27231
+ });
27053
27232
  app2.post("/sessions", async (c) => {
27054
27233
  const user = c.get("user");
27055
27234
  if (Number(user.balance_mc ?? 0) < BROWSER_OPEN_MIN_BALANCE_MC) {
@@ -27422,6 +27601,50 @@ var init_browser_agent_routes = __esm({
27422
27601
  });
27423
27602
 
27424
27603
  // src/api/browser-agent-console.ts
27604
+ function renderAuthConsoleHtml(connectionId, hostedUrl) {
27605
+ const safeId = String(connectionId).replace(/[&<>"]/g, (c) => ({ "&": "&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
+ }
27425
27648
  function renderConsoleHtml(initialSessionId) {
27426
27649
  const initial = JSON.stringify(initialSessionId ?? "");
27427
27650
  return `<!DOCTYPE html>
@@ -28207,6 +28430,7 @@ var init_server = __esm({
28207
28430
  init_mcp_routes();
28208
28431
  init_browser_agent_routes();
28209
28432
  init_browser_agent_console();
28433
+ init_browser_agent_db();
28210
28434
  init_stripe_routes();
28211
28435
  init_site_audit_worker();
28212
28436
  import_factory6 = require("hono/factory");
@@ -29035,6 +29259,19 @@ var init_server = __esm({
29035
29259
  app.route("/mcp", mcpApp);
29036
29260
  app.route("/agent", buildBrowserAgentRoutes());
29037
29261
  app.get("/console", (c) => c.html(renderConsoleHtml()));
29262
+ app.get("/console/auth/:id", async (c) => {
29263
+ const id = c.req.param("id");
29264
+ await migrateBrowserAgent();
29265
+ const row = await getAuthConnectionRow(id);
29266
+ return c.html(renderAuthConsoleHtml(id, row?.hosted_url ?? null));
29267
+ });
29268
+ app.get("/console/auth/:id/go", async (c) => {
29269
+ const id = c.req.param("id");
29270
+ await migrateBrowserAgent();
29271
+ const row = await getAuthConnectionRow(id);
29272
+ if (!row?.hosted_url) return c.text("This sign-in link is no longer active.", 404);
29273
+ return c.redirect(row.hosted_url, 302);
29274
+ });
29038
29275
  app.get("/console/:id", (c) => c.html(renderConsoleHtml(c.req.param("id"))));
29039
29276
  app.route("/stripe", stripeApp);
29040
29277
  if (!process.env.INNGEST_EVENT_KEY) {