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
@@ -1,3 +1,39 @@
1
+ import {
2
+ CaptureSerpPageSnapshotsInputSchema,
3
+ CaptureSerpPageSnapshotsOutputSchema,
4
+ CaptureSerpSnapshotInputSchema,
5
+ CaptureSerpSnapshotOutputSchema,
6
+ HttpMcpToolExecutor,
7
+ auditImages,
8
+ buildLinkGraph,
9
+ buildLinkReport,
10
+ buildPaaExtractorMcpServer,
11
+ computeIssues,
12
+ configureReportSaving,
13
+ exportFanout,
14
+ formatCaptureSerpPageSnapshots,
15
+ formatCaptureSerpSnapshot,
16
+ harvestTimeoutBudget,
17
+ liveWebToolAnnotations,
18
+ outputBaseDir,
19
+ renderImageSection,
20
+ renderIssueReport,
21
+ renderLinkReport,
22
+ sanitizeAttempts,
23
+ sanitizeHarvestResult
24
+ } from "./chunk-A46NKAAM.js";
25
+ import {
26
+ csvRecords,
27
+ listWorkflowDefinitions,
28
+ rowsToCsv,
29
+ runWorkflow,
30
+ runWorkflowStep,
31
+ workflowDefinition,
32
+ workflowOutputBaseDir,
33
+ workflowStepCount,
34
+ workflowSupportsSteps
35
+ } from "./chunk-H2R232HK.js";
36
+ import "./chunk-AUKOY2IV.js";
1
37
  import {
2
38
  BALANCE_PACK_LABELS,
3
39
  BALANCE_PRICE_IDS,
@@ -16,7 +52,34 @@ import {
16
52
  harvestProblemResponse,
17
53
  insufficientBalanceResponse,
18
54
  serializeHarvestProblem
19
- } from "./chunk-ZAUMSBV3.js";
55
+ } from "./chunk-T337IGVE.js";
56
+ import {
57
+ BrowserDriver,
58
+ MapsSelectors,
59
+ buildYouTubeChannelVideosUrl,
60
+ deleteKernelProxyId,
61
+ harvest,
62
+ resolveKernelProxyId
63
+ } from "./chunk-IGOEAKC7.js";
64
+ import {
65
+ CaptchaError,
66
+ RECAPTCHA_INSTRUCTIONS,
67
+ browserServiceApiKey,
68
+ browserServiceProfileName,
69
+ browserServiceProfileSaveChanges,
70
+ browserServiceProxyId,
71
+ sanitizeVendorName
72
+ } from "./chunk-ZAP7MYVO.js";
73
+ import {
74
+ DEFAULT_MAPS_PROXY_MODE,
75
+ DEFAULT_PROXY_MODE,
76
+ MapsPlaceOptionsSchema,
77
+ MapsSearchOptionsSchema,
78
+ RawMapsAboutAttributeSchema,
79
+ RawMapsHoursRowSchema,
80
+ RawMapsOverviewSchema,
81
+ RawMapsReviewStatsSchema
82
+ } from "./chunk-LFATOGDF.js";
20
83
  import {
21
84
  SiteAuditJobRowSchema,
22
85
  advanceWorkflowRunStep,
@@ -78,118 +141,6 @@ import {
78
141
  setStripeCustomerId,
79
142
  verifyPassword
80
143
  } from "./chunk-5H22TOXQ.js";
81
- import {
82
- BrowserDriver,
83
- MapsSelectors,
84
- buildYouTubeChannelVideosUrl,
85
- deleteKernelProxyId,
86
- harvest,
87
- resolveKernelProxyId
88
- } from "./chunk-4OPKIDON.js";
89
- import {
90
- CaptureSerpPageSnapshotsInputSchema,
91
- CaptureSerpPageSnapshotsOutputSchema,
92
- CaptureSerpSnapshotInputSchema,
93
- CaptureSerpSnapshotOutputSchema,
94
- HttpMcpToolExecutor,
95
- auditImages,
96
- buildLinkGraph,
97
- buildLinkReport,
98
- buildPaaExtractorMcpServer,
99
- computeIssues,
100
- configureReportSaving,
101
- formatCaptureSerpPageSnapshots,
102
- formatCaptureSerpSnapshot,
103
- harvestTimeoutBudget,
104
- liveWebToolAnnotations,
105
- outputBaseDir,
106
- renderImageSection,
107
- renderIssueReport,
108
- renderLinkReport
109
- } from "./chunk-N33YEY6W.js";
110
- import {
111
- CaptchaError,
112
- RECAPTCHA_INSTRUCTIONS,
113
- sanitizeVendorName
114
- } from "./chunk-M2S27J6Z.js";
115
- import {
116
- exportFanout
117
- } from "./chunk-3YGKXXUG.js";
118
- import {
119
- browserServiceApiKey,
120
- browserServiceProfileName,
121
- browserServiceProfileSaveChanges,
122
- browserServiceProxyId
123
- } from "./chunk-WN7PBKMV.js";
124
- import {
125
- csvRecords,
126
- listWorkflowDefinitions,
127
- rowsToCsv,
128
- runWorkflow,
129
- runWorkflowStep,
130
- workflowDefinition,
131
- workflowOutputBaseDir,
132
- workflowStepCount,
133
- workflowSupportsSteps
134
- } from "./chunk-H2R232HK.js";
135
- import {
136
- DEFAULT_MAPS_PROXY_MODE,
137
- DEFAULT_PROXY_MODE,
138
- MapsPlaceOptionsSchema,
139
- MapsSearchOptionsSchema,
140
- RawMapsAboutAttributeSchema,
141
- RawMapsHoursRowSchema,
142
- RawMapsOverviewSchema,
143
- RawMapsReviewStatsSchema
144
- } from "./chunk-LFATOGDF.js";
145
- import "./chunk-PVWWY5NV.js";
146
-
147
- // src/api/outbound-sanitize.ts
148
- var KEY_RENAMES = {
149
- kernel: "browserRuntime",
150
- kernel_session_id: "browser_session_id",
151
- kernel_delete_started: "session_cleanup_started",
152
- kernel_delete_succeeded: "session_cleanup_succeeded",
153
- kernel_delete_error: "session_cleanup_error",
154
- kernelSessionId: "browserSessionId",
155
- kernelDeleteStarted: "sessionCleanupStarted",
156
- kernelDeleteSucceeded: "sessionCleanupSucceeded",
157
- kernelDeleteError: "sessionCleanupError",
158
- kernelProxyId: "proxyId"
159
- };
160
- var SANITIZED_VALUE_KEYS = /error|message/i;
161
- function sanitizeOutboundDiagnostics(value, parentKey = "") {
162
- if (typeof value === "string") {
163
- if (SANITIZED_VALUE_KEYS.test(parentKey) && /kernel/i.test(value)) {
164
- return sanitizeVendorName(value);
165
- }
166
- return value;
167
- }
168
- if (Array.isArray(value)) return value.map((v) => sanitizeOutboundDiagnostics(v, parentKey));
169
- if (value !== null && typeof value === "object") {
170
- const out = {};
171
- for (const [key, val] of Object.entries(value)) {
172
- const renamed = KEY_RENAMES[key] ?? key;
173
- out[renamed] = sanitizeOutboundDiagnostics(val, key);
174
- }
175
- return out;
176
- }
177
- return value;
178
- }
179
- function sanitizeAttempts(attempts) {
180
- return attempts.map((a) => sanitizeOutboundDiagnostics(a));
181
- }
182
- function sanitizeHarvestResult(result) {
183
- const diagnostics = result?.diagnostics;
184
- if (!diagnostics?.debug) return result;
185
- return {
186
- ...result,
187
- diagnostics: {
188
- ...diagnostics,
189
- debug: sanitizeOutboundDiagnostics(diagnostics.debug)
190
- }
191
- };
192
- }
193
144
 
194
145
  // src/blog/registry.ts
195
146
  var posts = [
@@ -7586,11 +7537,6 @@ async function settleExtractJob(jobId, userId, refundMc, netChargeMc, reference)
7586
7537
 
7587
7538
  // src/inngest/functions/site-extract.ts
7588
7539
  var BATCH = 30;
7589
- function chunk(items, size) {
7590
- const out = [];
7591
- for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
7592
- return out;
7593
- }
7594
7540
  function buildArtifacts(job, pages, branding, imageAudit) {
7595
7541
  const { edges, metrics } = buildLinkGraph(pages, job.startUrl);
7596
7542
  const issues = computeIssues(pages, metrics);
@@ -7649,18 +7595,62 @@ var siteExtractFn = inngest.createFunction(
7649
7595
  const maxPages = Number(job.options.maxPages ?? 1e4);
7650
7596
  const concurrency = Number(job.options.concurrency ?? 1);
7651
7597
  const urlsPerBrowser = Number(job.options.urlsPerBrowser ?? job.options.rotateProxyEvery ?? 10);
7652
- const urls = await step.run("discover", async () => {
7598
+ const seed = await step.run("discover", async () => {
7653
7599
  const discovered = await discoverUrls(job.startUrl, maxPages, key);
7654
- await setExtractJobTotal(jobId, discovered.length);
7655
- return discovered;
7600
+ const out = discovered.length ? discovered : [job.startUrl];
7601
+ await setExtractJobTotal(jobId, out.length);
7602
+ return out;
7656
7603
  });
7657
- const batches = chunk(urls, BATCH);
7658
- for (let i = 0; i < batches.length; i++) {
7659
- await step.run(`crawl-batch-${i}`, async () => {
7660
- const pages = await extractPagesRotating(batches[i], { kernelApiKey: key, concurrency, urlsPerBrowser });
7604
+ const siteHost = (() => {
7605
+ try {
7606
+ return new URL(job.startUrl).hostname.replace(/^www\./, "");
7607
+ } catch {
7608
+ return "";
7609
+ }
7610
+ })();
7611
+ const norm = (u) => {
7612
+ try {
7613
+ const x = new URL(u);
7614
+ x.hash = "";
7615
+ return (x.origin + x.pathname).replace(/\/+$/, "") + x.search;
7616
+ } catch {
7617
+ return u;
7618
+ }
7619
+ };
7620
+ const sameSite = (u) => {
7621
+ try {
7622
+ return new URL(u).hostname.replace(/^www\./, "") === siteHost;
7623
+ } catch {
7624
+ return false;
7625
+ }
7626
+ };
7627
+ const queue = [...seed];
7628
+ const seen = new Set(seed.map(norm));
7629
+ let crawled = 0;
7630
+ const MAX_BATCHES = Math.ceil(maxPages / BATCH) + 30;
7631
+ for (let i = 0; i < MAX_BATCHES && crawled < maxPages && crawled < queue.length; i++) {
7632
+ const batch = queue.slice(crawled, Math.min(crawled + BATCH, maxPages));
7633
+ if (!batch.length) break;
7634
+ const newLinks = await step.run(`crawl-batch-${i}`, async () => {
7635
+ const pages = await extractPagesRotating(batch, { kernelApiKey: key, concurrency, urlsPerBrowser });
7661
7636
  await saveExtractPages(jobId, pages);
7662
- return pages.length;
7637
+ const found = [];
7638
+ for (const p of pages) for (const l of p.outlinks ?? []) {
7639
+ if (l.internal && l.href && sameSite(l.href)) found.push(l.href);
7640
+ }
7641
+ return [...new Set(found)];
7663
7642
  });
7643
+ crawled += batch.length;
7644
+ let added = false;
7645
+ for (const href of newLinks) {
7646
+ const n = norm(href);
7647
+ if (!seen.has(n) && seen.size < maxPages) {
7648
+ seen.add(n);
7649
+ queue.push(href);
7650
+ added = true;
7651
+ }
7652
+ }
7653
+ if (added) await setExtractJobTotal(jobId, Math.min(seen.size, maxPages));
7664
7654
  }
7665
7655
  const branding = Array.isArray(job.options.formats) && job.options.formats.includes("branding") ? await step.run("branding", () => extractBranding(job.startUrl, key).catch(() => null)) : null;
7666
7656
  const imageAudit = Array.isArray(job.options.formats) && job.options.formats.includes("images") ? await step.run("image-audit", async () => {
@@ -9343,10 +9333,10 @@ function buildTranscriptMarkdown(result) {
9343
9333
  if (result.chunks?.length) {
9344
9334
  lines.push("## Timestamped Segments");
9345
9335
  lines.push("");
9346
- for (const chunk2 of result.chunks) {
9347
- const start = fmtTs(chunk2.timestamp[0]);
9348
- const end = fmtTs(chunk2.timestamp[1]);
9349
- lines.push(`**[${start} \u2192 ${end}]** ${chunk2.text.trim()}`);
9336
+ for (const chunk of result.chunks) {
9337
+ const start = fmtTs(chunk.timestamp[0]);
9338
+ const end = fmtTs(chunk.timestamp[1]);
9339
+ lines.push(`**[${start} \u2192 ${end}]** ${chunk.text.trim()}`);
9350
9340
  lines.push("");
9351
9341
  }
9352
9342
  }
@@ -9420,7 +9410,11 @@ screenshotApp.post("/", async (c) => {
9420
9410
  metadata: { url: targetUrl, device }
9421
9411
  });
9422
9412
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
9413
+ let debited = false;
9423
9414
  try {
9415
+ const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.page_scrape, LedgerOperation.EXTRACT_URL, targetUrl);
9416
+ if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.page_scrape), 402);
9417
+ debited = true;
9424
9418
  const buf = await captureScreenshot(targetUrl, browserServiceApiKey(), device);
9425
9419
  return new Response(new Uint8Array(buf), {
9426
9420
  status: 200,
@@ -9431,6 +9425,7 @@ screenshotApp.post("/", async (c) => {
9431
9425
  }
9432
9426
  });
9433
9427
  } catch (err) {
9428
+ if (debited) await creditMc(user.id, MC_COSTS.page_scrape, LedgerOperation.EXTRACT_URL, "failed screenshot refund");
9434
9429
  const msg = err instanceof Error ? err.message : String(err);
9435
9430
  return c.json({ error: msg }, 502);
9436
9431
  } finally {
@@ -9847,20 +9842,20 @@ var FacebookAdExtractor = class {
9847
9842
  if (splitRe.lastIndex === m.index) splitRe.lastIndex++;
9848
9843
  }
9849
9844
  if (last < bodyText.length) adChunks.push(bodyText.slice(last));
9850
- const ads = adChunks.filter((c) => /Library ID[:\s]+\d{10,20}/i.test(c)).slice(0, maxAds).map((chunk2) => {
9851
- const lidM = chunk2.match(/Library ID[:\s]+([0-9]{10,20})/i);
9845
+ const ads = adChunks.filter((c) => /Library ID[:\s]+\d{10,20}/i.test(c)).slice(0, maxAds).map((chunk) => {
9846
+ const lidM = chunk.match(/Library ID[:\s]+([0-9]{10,20})/i);
9852
9847
  const libraryId = lidM ? lidM[1] : null;
9853
- const statusM = chunk2.match(/^(Active|Inactive)/i);
9848
+ const statusM = chunk.match(/^(Active|Inactive)/i);
9854
9849
  const status = statusM ? statusM[1] : null;
9855
- const startM = chunk2.match(/Started running on\s+([A-Za-z]+\.?\s+\d{1,2},?\s*\d{4})/i);
9850
+ const startM = chunk.match(/Started running on\s+([A-Za-z]+\.?\s+\d{1,2},?\s*\d{4})/i);
9856
9851
  const started = startM ? startM[1].replace(/\s+/g, " ").trim() : null;
9857
9852
  const visual = adVisuals[libraryId ?? ""] ?? { imageSrc: null, videoSrc: null, videoPoster: null };
9858
- const hasVideoText = /0:00\s*\/\s*0:00|\d+:\d+\s*\/\s*\d+:\d+/.test(chunk2);
9853
+ const hasVideoText = /0:00\s*\/\s*0:00|\d+:\d+\s*\/\s*\d+:\d+/.test(chunk);
9859
9854
  const creativeType = visual.videoSrc || hasVideoText ? "video" : visual.imageSrc ? "image" : "unknown";
9860
9855
  let domain = null;
9861
9856
  const domainRe = /\b([A-Z0-9]{2,}(?:\.[A-Z]{2,})+)\b/g;
9862
9857
  let dm;
9863
- while ((dm = domainRe.exec(chunk2)) !== null) {
9858
+ while ((dm = domainRe.exec(chunk)) !== null) {
9864
9859
  const c = dm[1];
9865
9860
  if (c.length >= 6 && c !== "LIBRARY") {
9866
9861
  domain = c;
@@ -9869,9 +9864,9 @@ var FacebookAdExtractor = class {
9869
9864
  }
9870
9865
  let primaryText = null;
9871
9866
  let headline = null;
9872
- const sponsoredIdx = chunk2.search(/\bSponsored\b/i);
9867
+ const sponsoredIdx = chunk.search(/\bSponsored\b/i);
9873
9868
  if (sponsoredIdx >= 0) {
9874
- let afterSponsored = chunk2.slice(sponsoredIdx + "Sponsored".length).trim();
9869
+ let afterSponsored = chunk.slice(sponsoredIdx + "Sponsored".length).trim();
9875
9870
  afterSponsored = afterSponsored.replace(/\s*\d*:?\d+\s*\/\s*\d*:?\d+.*$/, "");
9876
9871
  if (domain) {
9877
9872
  const di = afterSponsored.indexOf(domain);
@@ -9882,18 +9877,18 @@ var FacebookAdExtractor = class {
9882
9877
  }
9883
9878
  const ctaRe2 = new RegExp("\\b(" + CTA_LABELS.map((l) => l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|") + ")\\b", "i");
9884
9879
  if (domain) {
9885
- const di = chunk2.indexOf(domain);
9880
+ const di = chunk.indexOf(domain);
9886
9881
  if (di >= 0) {
9887
- let afterDomain = chunk2.slice(di + domain.length).trim();
9882
+ let afterDomain = chunk.slice(di + domain.length).trim();
9888
9883
  const ctaMatch = afterDomain.search(ctaRe2);
9889
9884
  if (ctaMatch >= 0) afterDomain = afterDomain.slice(0, ctaMatch).trim();
9890
9885
  if (afterDomain.length > 3 && afterDomain.length < 120) headline = afterDomain;
9891
9886
  }
9892
9887
  }
9893
9888
  if (!headline) {
9894
- const ctaIdx = chunk2.search(ctaRe2);
9889
+ const ctaIdx = chunk.search(ctaRe2);
9895
9890
  if (ctaIdx > 0) {
9896
- const beforeCta = chunk2.slice(0, ctaIdx).trimEnd();
9891
+ const beforeCta = chunk.slice(0, ctaIdx).trimEnd();
9897
9892
  const sentenceEnd = Math.max(beforeCta.lastIndexOf(". "), beforeCta.lastIndexOf("! "), beforeCta.lastIndexOf("? "));
9898
9893
  if (sentenceEnd >= 0) {
9899
9894
  const candidate = beforeCta.slice(sentenceEnd + 2).trim();
@@ -9905,12 +9900,12 @@ var FacebookAdExtractor = class {
9905
9900
  }
9906
9901
  let cta = null;
9907
9902
  for (const lbl of CTA_LABELS) {
9908
- if (chunk2.toLowerCase().includes(lbl.toLowerCase())) {
9903
+ if (chunk.toLowerCase().includes(lbl.toLowerCase())) {
9909
9904
  cta = lbl;
9910
9905
  break;
9911
9906
  }
9912
9907
  }
9913
- const multiM = chunk2.match(/(\d+)\s+ad[s]?\s+use[s]?\s+this/i);
9908
+ const multiM = chunk.match(/(\d+)\s+ad[s]?\s+use[s]?\s+this/i);
9914
9909
  const clusterCount = multiM ? parseInt(multiM[1]) : null;
9915
9910
  return {
9916
9911
  libraryId,
@@ -9933,8 +9928,8 @@ var FacebookAdExtractor = class {
9933
9928
  const unknownCreativeCount = ads.filter((a) => a.creativeType === "unknown").length;
9934
9929
  const advertiserPageId = new URL(page.url()).searchParams.get("view_all_page_id") ?? null;
9935
9930
  const nameFreq = /* @__PURE__ */ new Map();
9936
- for (const chunk2 of adChunks) {
9937
- const detailsParts = chunk2.split("See ad details ");
9931
+ for (const chunk of adChunks) {
9932
+ const detailsParts = chunk.split("See ad details ");
9938
9933
  if (detailsParts.length < 2) continue;
9939
9934
  const sponsoredIdx = detailsParts[1].indexOf(" Sponsored");
9940
9935
  if (sponsoredIdx < 0) continue;
@@ -10674,11 +10669,11 @@ facebookAdApp.post("/search", createApiKeyAuth(), async (c) => {
10674
10669
  }
10675
10670
  if (last < bodyText.length) adChunks.push(bodyText.slice(last));
10676
10671
  const advertiserMap = /* @__PURE__ */ new Map();
10677
- for (const chunk2 of adChunks) {
10678
- const lidM = chunk2.match(/Library ID[:\s]+([0-9]{10,20})/i);
10672
+ for (const chunk of adChunks) {
10673
+ const lidM = chunk.match(/Library ID[:\s]+([0-9]{10,20})/i);
10679
10674
  if (!lidM) continue;
10680
10675
  const libraryId = lidM[1];
10681
- const detailsParts = chunk2.split("See ad details ");
10676
+ const detailsParts = chunk.split("See ad details ");
10682
10677
  if (detailsParts.length < 2) continue;
10683
10678
  const afterDetails = detailsParts[1];
10684
10679
  const sponsoredIdx = afterDetails.indexOf(" Sponsored");
@@ -11230,8 +11225,8 @@ function runFfmpegMux(videoPath, audioPath, outPath) {
11230
11225
  stdio: ["ignore", "ignore", "pipe"]
11231
11226
  });
11232
11227
  let stderr = "";
11233
- child.stderr.on("data", (chunk2) => {
11234
- stderr += String(chunk2);
11228
+ child.stderr.on("data", (chunk) => {
11229
+ stderr += String(chunk);
11235
11230
  });
11236
11231
  child.on("error", (err) => resolve({ ok: false, error: err.message }));
11237
11232
  child.on("close", (code) => resolve({ ok: code === 0, error: code === 0 ? null : stderr.trim() || `ffmpeg exited ${code}` }));
@@ -14363,8 +14358,64 @@ async function migrateBrowserAgent() {
14363
14358
  )
14364
14359
  `);
14365
14360
  await db.execute(`CREATE INDEX IF NOT EXISTS browser_agent_replays_session ON browser_agent_replays(session_id)`);
14361
+ await db.execute(`
14362
+ CREATE TABLE IF NOT EXISTS browser_auth_connections (
14363
+ connection_id TEXT PRIMARY KEY,
14364
+ hosted_url TEXT,
14365
+ domain TEXT,
14366
+ profile TEXT,
14367
+ account_email TEXT,
14368
+ note TEXT,
14369
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
14370
+ updated_at TEXT
14371
+ )
14372
+ `);
14373
+ try {
14374
+ await db.execute(`ALTER TABLE browser_auth_connections ADD COLUMN account_email TEXT`);
14375
+ } catch {
14376
+ }
14377
+ try {
14378
+ await db.execute(`ALTER TABLE browser_auth_connections ADD COLUMN note TEXT`);
14379
+ } catch {
14380
+ }
14381
+ try {
14382
+ await db.execute(`ALTER TABLE browser_auth_connections ADD COLUMN updated_at TEXT`);
14383
+ } catch {
14384
+ }
14385
+ await db.execute(`CREATE INDEX IF NOT EXISTS browser_auth_connections_profile ON browser_auth_connections(profile)`);
14366
14386
  _ready = true;
14367
14387
  }
14388
+ async function saveAuthConnection(input) {
14389
+ if (!input.connectionId) return;
14390
+ const db = getDb();
14391
+ await db.execute({
14392
+ sql: `INSERT INTO browser_auth_connections (connection_id, hosted_url, domain, profile, account_email, note, updated_at)
14393
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
14394
+ ON CONFLICT(connection_id) DO UPDATE SET
14395
+ hosted_url = COALESCE(excluded.hosted_url, browser_auth_connections.hosted_url),
14396
+ domain = COALESCE(excluded.domain, browser_auth_connections.domain),
14397
+ profile = COALESCE(excluded.profile, browser_auth_connections.profile),
14398
+ account_email = COALESCE(excluded.account_email, browser_auth_connections.account_email),
14399
+ note = COALESCE(excluded.note, browser_auth_connections.note),
14400
+ updated_at = datetime('now')`,
14401
+ args: [input.connectionId, input.hostedUrl, input.domain, input.profile, input.accountEmail ?? null, input.note ?? null]
14402
+ });
14403
+ }
14404
+ async function getAuthConnectionRow(connectionId) {
14405
+ if (!connectionId) return null;
14406
+ const db = getDb();
14407
+ const res = await db.execute({ sql: `SELECT * FROM browser_auth_connections WHERE connection_id = ?`, args: [connectionId] });
14408
+ return res.rows[0] ?? null;
14409
+ }
14410
+ async function listAuthConnectionsByProfile(profile) {
14411
+ if (!profile) return [];
14412
+ const db = getDb();
14413
+ const res = await db.execute({
14414
+ sql: `SELECT * FROM browser_auth_connections WHERE profile = ? ORDER BY COALESCE(updated_at, created_at) DESC`,
14415
+ args: [profile]
14416
+ });
14417
+ return res.rows;
14418
+ }
14368
14419
  async function createSessionRow(input) {
14369
14420
  const db = getDb();
14370
14421
  const id = `bas_${randomUUID2().replace(/-/g, "").slice(0, 20)}`;
@@ -14398,6 +14449,14 @@ async function listSessionRows(userId, includeClosed = false) {
14398
14449
  });
14399
14450
  return res.rows;
14400
14451
  }
14452
+ async function listIdleOpenSessionRows(idleSeconds) {
14453
+ const db = getDb();
14454
+ const res = await db.execute({
14455
+ sql: `SELECT * FROM browser_agent_sessions WHERE status = 'open' AND COALESCE(last_action_at, created_at) <= datetime('now', ?) ORDER BY created_at LIMIT 200`,
14456
+ args: [`-${Math.max(1, Math.round(idleSeconds))} seconds`]
14457
+ });
14458
+ return res.rows;
14459
+ }
14401
14460
  async function addActiveMs(id, deltaMs) {
14402
14461
  const db = getDb();
14403
14462
  await db.execute({
@@ -15197,6 +15256,17 @@ async function findProfileConnection(k, profileName, domain) {
15197
15256
  }
15198
15257
  return null;
15199
15258
  }
15259
+ async function listProfileConnections(profileName) {
15260
+ if (!profileName) return [];
15261
+ const k = client();
15262
+ const out = [];
15263
+ for await (const connection of k.auth.connections.list({ profile_name: profileName })) {
15264
+ const data = connection;
15265
+ if (data.profile_name && data.profile_name !== profileName) continue;
15266
+ out.push(profileOnboardResult(connection));
15267
+ }
15268
+ return out;
15269
+ }
15200
15270
  async function findOrCreateProfileConnection(k, opts) {
15201
15271
  try {
15202
15272
  return await k.auth.connections.create({
@@ -15575,29 +15645,61 @@ function browserSessionLockTtlSeconds(timeoutSeconds) {
15575
15645
  }
15576
15646
  return DEFAULT_BROWSER_SESSION_LOCK_TTL_SECONDS;
15577
15647
  }
15578
- async function charge(sessionId, userId, startedAtMs) {
15579
- const elapsedMs = Date.now() - startedAtMs;
15580
- const { active_ms, billed_mc } = await addActiveMs(sessionId, elapsedMs);
15581
- const owed = browserActiveCostMc(active_ms);
15582
- const delta = owed - billed_mc;
15648
+ async function charge(sessionId, userId, _startedAtMs) {
15649
+ const row = await getSessionRow(sessionId);
15650
+ if (!row) return;
15651
+ const createdMs = parseAgentDate(row.created_at);
15652
+ if (createdMs == null) return;
15653
+ const uptimeMs = Math.max(0, Date.now() - createdMs);
15654
+ const owed = browserActiveCostMc(uptimeMs);
15655
+ await addActiveMs(sessionId, 0);
15656
+ const delta = owed - Number(row.billed_mc ?? 0);
15583
15657
  if (delta > 0) {
15584
15658
  const res = await debitMc(userId, delta, LedgerOperation.BROWSER_SESSION, sessionId);
15585
15659
  if (res.ok) await setBilledMc(sessionId, owed);
15586
15660
  }
15587
15661
  }
15662
+ async function reapIdleBrowserSessions(idleSeconds = 120) {
15663
+ const rows = await listIdleOpenSessionRows(idleSeconds);
15664
+ let reaped = 0;
15665
+ for (const row of rows) {
15666
+ if (row.user_id != null) {
15667
+ try {
15668
+ await charge(row.id, row.user_id);
15669
+ } catch {
15670
+ }
15671
+ }
15672
+ try {
15673
+ await closeSession(row.runtime_session_id);
15674
+ } catch {
15675
+ }
15676
+ await markSessionClosed(row.id).catch(() => {
15677
+ });
15678
+ if (row.concurrency_lock_id) await releaseConcurrencyGate(row.concurrency_lock_id).catch(() => {
15679
+ });
15680
+ reaped++;
15681
+ }
15682
+ return { reaped };
15683
+ }
15588
15684
  function publicSession(row) {
15589
15685
  return {
15590
15686
  session_id: row.id,
15591
15687
  status: row.status,
15592
15688
  label: row.label,
15593
- live_view_url: row.live_view_url,
15689
+ watch_url: `/console/${row.id}`,
15594
15690
  created_at: row.created_at,
15595
15691
  last_action_at: row.last_action_at,
15596
15692
  closed_at: row.closed_at,
15597
- active_seconds: Math.round((row.active_ms ?? 0) / 1e3),
15693
+ uptime_seconds: uptimeSeconds(row),
15598
15694
  credits_used: Math.round((row.billed_mc ?? 0) / 10) / 100
15599
15695
  };
15600
15696
  }
15697
+ function uptimeSeconds(row) {
15698
+ const createdMs = parseAgentDate(row.created_at);
15699
+ const endMs = row.closed_at ? parseAgentDate(row.closed_at) : Date.now();
15700
+ if (createdMs == null || endMs == null) return 0;
15701
+ return Math.max(0, Math.round((endMs - createdMs) / 1e3));
15702
+ }
15601
15703
  function failure(err) {
15602
15704
  const msg = err instanceof Error ? err.message : String(err);
15603
15705
  return { error: sanitizeVendorName(msg) };
@@ -15646,10 +15748,23 @@ function buildBrowserAgentRoutes() {
15646
15748
  const profileName = typeof body.profile === "string" ? body.profile.trim() : "";
15647
15749
  const domain = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : "chatgpt.com";
15648
15750
  const loginUrl = typeof body.login_url === "string" ? body.login_url.trim() : void 0;
15751
+ const accountEmail = typeof body.account_email === "string" ? body.account_email.trim() || null : null;
15752
+ const note = typeof body.note === "string" ? body.note.trim() || null : null;
15649
15753
  if (!profileName) return c.json({ error: "profile is required" }, 400);
15650
15754
  if (!domain) return c.json({ error: "domain is required" }, 400);
15651
15755
  try {
15652
15756
  const result = await startProfileOnboarding({ profileName, domain, loginUrl });
15757
+ try {
15758
+ await saveAuthConnection({
15759
+ connectionId: result.connectionId,
15760
+ hostedUrl: result.hostedUrl,
15761
+ domain: result.domain,
15762
+ profile: result.profileName,
15763
+ accountEmail,
15764
+ note
15765
+ });
15766
+ } catch {
15767
+ }
15653
15768
  return c.json({
15654
15769
  connection_id: result.connectionId,
15655
15770
  profile: result.profileName,
@@ -15658,8 +15773,6 @@ function buildBrowserAgentRoutes() {
15658
15773
  flow_status: result.flowStatus,
15659
15774
  flow_step: result.flowStep,
15660
15775
  flow_expires_at: result.flowExpiresAt,
15661
- hosted_url: result.hostedUrl,
15662
- live_view_url: result.liveViewUrl,
15663
15776
  browser_session_id: result.browserSessionId,
15664
15777
  post_login_url: result.postLoginUrl
15665
15778
  });
@@ -15674,6 +15787,17 @@ function buildBrowserAgentRoutes() {
15674
15787
  const domain = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : void 0;
15675
15788
  try {
15676
15789
  const result = await getProfileOnboardingStatus({ connectionId, profileName, domain });
15790
+ if (result.hostedUrl) {
15791
+ try {
15792
+ await saveAuthConnection({
15793
+ connectionId: result.connectionId,
15794
+ hostedUrl: result.hostedUrl,
15795
+ domain: result.domain,
15796
+ profile: result.profileName
15797
+ });
15798
+ } catch {
15799
+ }
15800
+ }
15677
15801
  return c.json({
15678
15802
  connection_id: result.connectionId,
15679
15803
  profile: result.profileName,
@@ -15682,8 +15806,6 @@ function buildBrowserAgentRoutes() {
15682
15806
  flow_status: result.flowStatus,
15683
15807
  flow_step: result.flowStep,
15684
15808
  flow_expires_at: result.flowExpiresAt,
15685
- hosted_url: result.hostedUrl,
15686
- live_view_url: result.liveViewUrl,
15687
15809
  browser_session_id: result.browserSessionId,
15688
15810
  post_login_url: result.postLoginUrl
15689
15811
  });
@@ -15691,6 +15813,37 @@ function buildBrowserAgentRoutes() {
15691
15813
  return c.json(failure(err), 404);
15692
15814
  }
15693
15815
  });
15816
+ app2.post("/profiles/list", async (c) => {
15817
+ const body = await c.req.json().catch(() => ({}));
15818
+ const profileName = typeof body.profile === "string" ? body.profile.trim() : "";
15819
+ if (!profileName) return c.json({ error: "profile is required" }, 400);
15820
+ const domainFilter = typeof body.domain === "string" ? body.domain.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "") : void 0;
15821
+ const connectionIdFilter = typeof body.connection_id === "string" ? body.connection_id.trim() : void 0;
15822
+ try {
15823
+ const [live, stored] = await Promise.all([
15824
+ listProfileConnections(profileName),
15825
+ listAuthConnectionsByProfile(profileName).catch(() => [])
15826
+ ]);
15827
+ const notesByDomain = new Map(stored.map((row) => [row.domain ?? "", row]));
15828
+ const notesByConnection = new Map(stored.map((row) => [row.connection_id, row]));
15829
+ let connections = live.map((conn) => {
15830
+ const record = notesByConnection.get(conn.connectionId) ?? notesByDomain.get(conn.domain);
15831
+ return {
15832
+ connection_id: conn.connectionId,
15833
+ domain: conn.domain,
15834
+ status: conn.status,
15835
+ account_email: record?.account_email ?? null,
15836
+ note: record?.note ?? null,
15837
+ last_connected_at: record?.updated_at ?? record?.created_at ?? null
15838
+ };
15839
+ });
15840
+ if (connectionIdFilter) connections = connections.filter((c2) => c2.connection_id === connectionIdFilter);
15841
+ if (domainFilter) connections = connections.filter((c2) => c2.domain === domainFilter);
15842
+ return c.json({ profile: profileName, connections });
15843
+ } catch (err) {
15844
+ return c.json(failure(err), 502);
15845
+ }
15846
+ });
15694
15847
  app2.post("/sessions", async (c) => {
15695
15848
  const user = c.get("user");
15696
15849
  if (Number(user.balance_mc ?? 0) < BROWSER_OPEN_MIN_BALANCE_MC) {
@@ -15758,6 +15911,7 @@ function buildBrowserAgentRoutes() {
15758
15911
  const user = c.get("user");
15759
15912
  const row = await loadOpenSession(c.req.param("id"), user.id);
15760
15913
  if (!row) return c.json({ error: "not found" }, 404);
15914
+ await charge(row.id, user.id);
15761
15915
  try {
15762
15916
  await closeSession(row.runtime_session_id);
15763
15917
  } catch {
@@ -16047,6 +16201,50 @@ function buildBrowserAgentRoutes() {
16047
16201
  }
16048
16202
 
16049
16203
  // src/api/browser-agent-console.ts
16204
+ function renderAuthConsoleHtml(connectionId, hostedUrl) {
16205
+ const safeId = String(connectionId).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
16206
+ const goUrl = `/console/auth/${encodeURIComponent(connectionId)}/go`;
16207
+ const body = hostedUrl ? `<iframe class="stage" allow="clipboard-read; clipboard-write" src="${goUrl}"></iframe>
16208
+ <div class="fallback">
16209
+ <span>Sign-in window not showing?</span>
16210
+ <a class="btn" href="${goUrl}" target="_blank" rel="noopener">Open sign-in in a new tab</a>
16211
+ </div>` : `<div class="empty">
16212
+ <div>This sign-in link is no longer active.</div>
16213
+ <div class="muted">Ask the assistant to reconnect the profile, then open the new link.</div>
16214
+ </div>`;
16215
+ return `<!DOCTYPE html>
16216
+ <html lang="en">
16217
+ <head>
16218
+ <meta charset="UTF-8" />
16219
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
16220
+ <title>MCP Scraper \u2014 Secure Sign-in</title>
16221
+ <style>
16222
+ :root { color-scheme: dark; }
16223
+ :where(*) { box-sizing: border-box; }
16224
+ 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; }
16225
+ header { display: flex; align-items: center; gap: 10px; padding: 12px 18px; border-bottom: 1px solid #1c2230; background: #0f131c; }
16226
+ header h1 { font-size: 14px; margin: 0; font-weight: 600; color: #fff; }
16227
+ header .muted { color: #6b7689; font-size: 12px; }
16228
+ .spacer { flex: 1; }
16229
+ .stage { flex: 1; width: 100%; border: 0; display: block; background: #05070b; }
16230
+ .fallback { display: flex; align-items: center; gap: 12px; padding: 10px 18px; border-top: 1px solid #1c2230; background: #0f131c; font-size: 13px; color: #8893a7; }
16231
+ .btn { background: #2b6cff; color: #fff; border-radius: 7px; padding: 7px 12px; text-decoration: none; font-weight: 500; }
16232
+ .empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: #8893a7; }
16233
+ .muted { color: #6b7689; font-size: 12px; }
16234
+ </style>
16235
+ ${ATTRIBUTION_PIXEL_SCRIPT}
16236
+ </head>
16237
+ <body>
16238
+ <header>
16239
+ <h1>MCP Scraper</h1>
16240
+ <span class="muted">Secure sign-in \xB7 ${safeId}</span>
16241
+ <span class="spacer"></span>
16242
+ <span class="muted">Complete the login below \u2014 your session is saved to your profile.</span>
16243
+ </header>
16244
+ ${body}
16245
+ </body>
16246
+ </html>`;
16247
+ }
16050
16248
  function renderConsoleHtml(initialSessionId) {
16051
16249
  const initial = JSON.stringify(initialSessionId ?? "");
16052
16250
  return `<!DOCTYPE html>
@@ -17378,14 +17576,15 @@ app.get("/cron/tick", async (c) => {
17378
17576
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
17379
17577
  return c.json({ error: "Unauthorized" }, 401);
17380
17578
  }
17381
- const { drainQueue } = await import("./worker-2WVKKCC7.js");
17579
+ const { drainQueue } = await import("./worker-YZ2ZJE4F.js");
17382
17580
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
17383
17581
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
17384
- const [results, sweepResult] = await Promise.all([
17582
+ const [results, sweepResult, reapResult] = await Promise.all([
17385
17583
  drainQueue(budget),
17386
- runMonthlyRefreshSweep()
17584
+ runMonthlyRefreshSweep(),
17585
+ reapIdleBrowserSessions(120).catch(() => ({ reaped: 0 }))
17387
17586
  ]);
17388
- return c.json({ drained: results.length, results, sweepResult, workflowDispatch: workflowDispatchResult });
17587
+ return c.json({ drained: results.length, results, sweepResult, reaped: reapResult, workflowDispatch: workflowDispatchResult });
17389
17588
  });
17390
17589
  app.on(["GET", "POST", "PUT"], "/api/inngest", serveInngest({ client: inngest, functions: [siteAuditFn, siteExtractFn] }));
17391
17590
  app.route("/api/internal/site-architecture-auditor", siteAuditApp);
@@ -17400,6 +17599,19 @@ app.route("/serp-intelligence", serpIntelligenceApp);
17400
17599
  app.route("/mcp", mcpApp);
17401
17600
  app.route("/agent", buildBrowserAgentRoutes());
17402
17601
  app.get("/console", (c) => c.html(renderConsoleHtml()));
17602
+ app.get("/console/auth/:id", async (c) => {
17603
+ const id = c.req.param("id");
17604
+ await migrateBrowserAgent();
17605
+ const row = await getAuthConnectionRow(id);
17606
+ return c.html(renderAuthConsoleHtml(id, row?.hosted_url ?? null));
17607
+ });
17608
+ app.get("/console/auth/:id/go", async (c) => {
17609
+ const id = c.req.param("id");
17610
+ await migrateBrowserAgent();
17611
+ const row = await getAuthConnectionRow(id);
17612
+ if (!row?.hosted_url) return c.text("This sign-in link is no longer active.", 404);
17613
+ return c.redirect(row.hosted_url, 302);
17614
+ });
17403
17615
  app.get("/console/:id", (c) => c.html(renderConsoleHtml(c.req.param("id"))));
17404
17616
  app.route("/stripe", stripeApp);
17405
17617
  if (!process.env.INNGEST_EVENT_KEY) {
@@ -17508,4 +17720,4 @@ app.get("/blog/:slug/", (c) => {
17508
17720
  export {
17509
17721
  app
17510
17722
  };
17511
- //# sourceMappingURL=server-TKWY47JA.js.map
17723
+ //# sourceMappingURL=server-EYNMXWUJ.js.map