mcp-scraper 0.3.27 → 0.3.40

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 (45) hide show
  1. package/dist/bin/api-server.cjs +2475 -1154
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +3 -3
  4. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +1 -1
  7. package/dist/bin/mcp-scraper-install.cjs +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-install.js +1 -1
  10. package/dist/bin/mcp-stdio-server.cjs +346 -7
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +7 -4
  13. package/dist/bin/mcp-stdio-server.js.map +1 -1
  14. package/dist/bin/paa-harvest.cjs +109 -5
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +2 -2
  17. package/dist/{chunk-TRNSXR46.js → chunk-DVRPXPYH.js} +22 -3
  18. package/dist/chunk-DVRPXPYH.js.map +1 -0
  19. package/dist/{chunk-QSSH4RCX.js → chunk-GMTS35L6.js} +111 -7
  20. package/dist/chunk-GMTS35L6.js.map +1 -0
  21. package/dist/chunk-I5EFBTYE.js +7 -0
  22. package/dist/chunk-I5EFBTYE.js.map +1 -0
  23. package/dist/{chunk-H74L743B.js → chunk-KJWNVQFL.js} +39 -2
  24. package/dist/chunk-KJWNVQFL.js.map +1 -0
  25. package/dist/{chunk-6KHGYJIT.js → chunk-QLQYNE7E.js} +255 -14
  26. package/dist/chunk-QLQYNE7E.js.map +1 -0
  27. package/dist/{db-FP2ABUU4.js → db-LYQENFPW.js} +2 -2
  28. package/dist/index.cjs +109 -5
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +8 -0
  31. package/dist/index.d.ts +8 -0
  32. package/dist/index.js +2 -2
  33. package/dist/{server-KUNOK47B.js → server-HB7WJO3N.js} +1155 -274
  34. package/dist/server-HB7WJO3N.js.map +1 -0
  35. package/dist/{worker-NV4GTIRG.js → worker-TL4J65DE.js} +4 -4
  36. package/package.json +1 -1
  37. package/dist/chunk-6KHGYJIT.js.map +0 -1
  38. package/dist/chunk-DYV72F6A.js +0 -7
  39. package/dist/chunk-DYV72F6A.js.map +0 -1
  40. package/dist/chunk-H74L743B.js.map +0 -1
  41. package/dist/chunk-QSSH4RCX.js.map +0 -1
  42. package/dist/chunk-TRNSXR46.js.map +0 -1
  43. package/dist/server-KUNOK47B.js.map +0 -1
  44. /package/dist/{db-FP2ABUU4.js.map → db-LYQENFPW.js.map} +0 -0
  45. /package/dist/{worker-NV4GTIRG.js.map → worker-TL4J65DE.js.map} +0 -0
@@ -1,8 +1,4 @@
1
1
  import {
2
- CaptureSerpPageSnapshotsInputSchema,
3
- CaptureSerpPageSnapshotsOutputSchema,
4
- CaptureSerpSnapshotInputSchema,
5
- CaptureSerpSnapshotOutputSchema,
6
2
  HttpMcpToolExecutor,
7
3
  auditImages,
8
4
  buildLinkGraph,
@@ -11,18 +7,16 @@ import {
11
7
  computeIssues,
12
8
  configureReportSaving,
13
9
  exportFanout,
14
- formatCaptureSerpPageSnapshots,
15
- formatCaptureSerpSnapshot,
16
10
  harvestTimeoutBudget,
17
- liveWebToolAnnotations,
18
11
  outputBaseDir,
19
12
  registerBrowserAgentMcpTools,
13
+ registerSerpIntelligenceCaptureTools,
20
14
  renderImageSection,
21
15
  renderIssueReport,
22
16
  renderLinkReport,
23
17
  sanitizeAttempts,
24
18
  sanitizeHarvestResult
25
- } from "./chunk-6KHGYJIT.js";
19
+ } from "./chunk-QLQYNE7E.js";
26
20
  import {
27
21
  csvRecords,
28
22
  listWorkflowDefinitions,
@@ -35,7 +29,7 @@ import {
35
29
  workflowStepCount,
36
30
  workflowSupportsSteps
37
31
  } from "./chunk-NNEIXK5L.js";
38
- import "./chunk-DYV72F6A.js";
32
+ import "./chunk-I5EFBTYE.js";
39
33
  import {
40
34
  BROWSER_OPEN_MIN_BALANCE_MC,
41
35
  CONCURRENCY_PRICE_ID,
@@ -46,6 +40,7 @@ import {
46
40
  MC_PER_CREDIT,
47
41
  MEMORY_PLANS,
48
42
  MEMORY_PLAN_QUOTA,
43
+ SCHEDULING_PLANS,
49
44
  SUBSCRIPTION_TIERS,
50
45
  SUBSCRIPTION_TIER_BY_KEY,
51
46
  browserActiveCostMc,
@@ -55,7 +50,7 @@ import {
55
50
  harvestProblemResponse,
56
51
  insufficientBalanceResponse,
57
52
  serializeHarvestProblem
58
- } from "./chunk-H74L743B.js";
53
+ } from "./chunk-KJWNVQFL.js";
59
54
  import {
60
55
  BrowserDriver,
61
56
  MapsSelectors,
@@ -68,7 +63,7 @@ import {
68
63
  recordVendorUsage,
69
64
  resolveKernelProxyId,
70
65
  runWithCostContext
71
- } from "./chunk-QSSH4RCX.js";
66
+ } from "./chunk-GMTS35L6.js";
72
67
  import {
73
68
  CaptchaError,
74
69
  RECAPTCHA_INSTRUCTIONS,
@@ -164,7 +159,7 @@ import {
164
159
  setSubscriptionTier,
165
160
  setUserMemoryCreds,
166
161
  verifyPassword
167
- } from "./chunk-TRNSXR46.js";
162
+ } from "./chunk-DVRPXPYH.js";
168
163
 
169
164
  // src/blog/registry.ts
170
165
  var posts = [
@@ -5262,7 +5257,7 @@ async function extractSite(opts) {
5262
5257
  }
5263
5258
 
5264
5259
  // src/api/server.ts
5265
- import { Hono as Hono16 } from "hono";
5260
+ import { Hono as Hono18 } from "hono";
5266
5261
  import { serve as serveInngest } from "inngest/hono";
5267
5262
 
5268
5263
  // src/inngest/client.ts
@@ -7811,6 +7806,49 @@ var CATALOG = {
7811
7806
  }
7812
7807
  ]
7813
7808
  },
7809
+ {
7810
+ key: "google_ads",
7811
+ label: "Google Ads",
7812
+ icon: "megaphone",
7813
+ tools: [
7814
+ {
7815
+ key: "google_ads_search",
7816
+ label: "Ad Search",
7817
+ kind: "simple",
7818
+ endpoint: "/google-ads/search",
7819
+ mcpName: "google_ads_search",
7820
+ tagline: "Find advertisers in Ads Transparency",
7821
+ params: [
7822
+ { key: "query", label: "Domain or advertiser", type: "string", required: true, placeholder: "getviktor.com" },
7823
+ { key: "region", label: "Region", type: "string", default: "US" },
7824
+ { key: "maxResults", label: "Max results", type: "number", min: 1, max: 20, default: 10, advanced: true }
7825
+ ],
7826
+ examples: ["Find Google ads pointing to this domain"]
7827
+ },
7828
+ {
7829
+ key: "google_ads_page_intel",
7830
+ label: "Advertiser Intel",
7831
+ kind: "simple",
7832
+ endpoint: "/google-ads/page-intel",
7833
+ mcpName: "google_ads_page_intel",
7834
+ tagline: "An advertiser's ad creatives + media",
7835
+ params: [
7836
+ { key: "domain", label: "Domain or advertiser ID", type: "string", required: true, placeholder: "getviktor.com" },
7837
+ { key: "region", label: "Region", type: "string", default: "US" },
7838
+ { key: "maxAds", label: "Max ads", type: "number", min: 1, max: 200, default: 50, advanced: true }
7839
+ ]
7840
+ },
7841
+ {
7842
+ key: "google_ads_transcribe",
7843
+ label: "Ad Transcribe",
7844
+ kind: "simple",
7845
+ endpoint: "/google-ads/transcribe",
7846
+ mcpName: "google_ads_transcribe",
7847
+ tagline: "Transcribe a video ad",
7848
+ params: [{ ...URL_PARAM, label: "Ad video URL" }]
7849
+ }
7850
+ ]
7851
+ },
7814
7852
  {
7815
7853
  key: "instagram",
7816
7854
  label: "Instagram",
@@ -10796,9 +10834,330 @@ facebookAdApp.post("/media", createApiKeyAuth(), async (c) => {
10796
10834
  });
10797
10835
  });
10798
10836
 
10799
- // src/api/instagram-routes.ts
10837
+ // src/api/google-ads-routes.ts
10800
10838
  import { Hono as Hono5 } from "hono";
10801
10839
  import { z as z14 } from "zod";
10840
+
10841
+ // src/extractor/GoogleAdsExtractor.ts
10842
+ var ORIGIN = "https://adstransparency.google.com";
10843
+ function looksLikeDomain(q) {
10844
+ return /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(q.trim()) && !q.includes(" ");
10845
+ }
10846
+ var GoogleAdsExtractor = class {
10847
+ constructor(driver) {
10848
+ this.driver = driver;
10849
+ }
10850
+ driver;
10851
+ page() {
10852
+ return this.driver.getPage();
10853
+ }
10854
+ async search(query, region, maxResults) {
10855
+ const q = query.trim();
10856
+ const url = looksLikeDomain(q) ? `${ORIGIN}/?region=${encodeURIComponent(region)}&domain=${encodeURIComponent(q)}` : `${ORIGIN}/?region=${encodeURIComponent(region)}&query=${encodeURIComponent(q)}`;
10857
+ await this.driver.navigateTo(url);
10858
+ const page = this.page();
10859
+ await page.waitForTimeout(3500);
10860
+ await this.autoScroll(page, 6);
10861
+ const advertisers = await page.evaluate((max) => {
10862
+ const map = /* @__PURE__ */ new Map();
10863
+ const links = Array.from(document.querySelectorAll('a[href*="/advertiser/"]'));
10864
+ for (const a of links) {
10865
+ const m = a.getAttribute("href")?.match(/\/advertiser\/(AR[0-9]+)/);
10866
+ if (!m) continue;
10867
+ const id = m[1];
10868
+ const card = a.closest("creative-preview, advertiser-preview") || a.parentElement;
10869
+ const nameEl = card?.querySelector('[class*="advertiser"], [class*="name"]');
10870
+ const name = (nameEl?.innerText || "").trim() || null;
10871
+ const cur = map.get(id);
10872
+ if (cur) cur.count++;
10873
+ else map.set(id, { advertiserId: id, name, count: 1 });
10874
+ }
10875
+ return Array.from(map.values()).slice(0, max).map((v) => ({
10876
+ advertiserId: v.advertiserId,
10877
+ name: v.name,
10878
+ approxAdCount: v.count
10879
+ }));
10880
+ }, maxResults);
10881
+ return {
10882
+ query: q,
10883
+ region,
10884
+ advertiserCount: advertisers.length,
10885
+ advertisers: advertisers.map((a) => ({
10886
+ advertiserId: a.advertiserId,
10887
+ name: a.name,
10888
+ domain: looksLikeDomain(q) ? q : null,
10889
+ approxAdCount: a.approxAdCount,
10890
+ detailUrl: `${ORIGIN}/advertiser/${a.advertiserId}?region=${region}`
10891
+ }))
10892
+ };
10893
+ }
10894
+ async pageIntel(opts) {
10895
+ const { region, maxAds } = opts;
10896
+ let advertiserId = opts.advertiserId?.trim() || null;
10897
+ if (!advertiserId && opts.domain) {
10898
+ const s = await this.search(opts.domain, region, 1);
10899
+ advertiserId = s.advertisers[0]?.advertiserId ?? null;
10900
+ }
10901
+ if (!advertiserId) {
10902
+ return { advertiserId: null, advertiserName: null, domain: opts.domain ?? null, region, totalCreatives: 0, videoCount: 0, imageCount: 0, textCount: 0, ads: [] };
10903
+ }
10904
+ const listUrl = `${ORIGIN}/advertiser/${advertiserId}?region=${region}`;
10905
+ await this.driver.navigateTo(listUrl);
10906
+ const page = this.page();
10907
+ await page.waitForTimeout(3500);
10908
+ await this.autoScroll(page, 10);
10909
+ const listing = await page.evaluate(() => {
10910
+ const nameEl = document.querySelector('h1, [class*="advertiser-name"]');
10911
+ const advertiserName = (nameEl?.innerText || "").trim() || null;
10912
+ const ids = [];
10913
+ const seen = /* @__PURE__ */ new Set();
10914
+ for (const a of Array.from(document.querySelectorAll('a[href*="/creative/"]'))) {
10915
+ const m = a.getAttribute("href")?.match(/\/creative\/(CR[0-9]+)/);
10916
+ if (m && !seen.has(m[1])) {
10917
+ seen.add(m[1]);
10918
+ ids.push(m[1]);
10919
+ }
10920
+ }
10921
+ return { advertiserName, creativeIds: ids };
10922
+ });
10923
+ const creativeIds = listing.creativeIds.slice(0, maxAds);
10924
+ const ads = [];
10925
+ for (const creativeId of creativeIds) {
10926
+ try {
10927
+ const hydrated = await this.hydrateCreative(page, advertiserId, creativeId);
10928
+ ads.push({
10929
+ creativeId,
10930
+ advertiserId,
10931
+ detailUrl: `${ORIGIN}/advertiser/${advertiserId}/creative/${creativeId}?region=${region}`,
10932
+ ...hydrated
10933
+ });
10934
+ } catch {
10935
+ ads.push({
10936
+ creativeId,
10937
+ advertiserId,
10938
+ detailUrl: `${ORIGIN}/advertiser/${advertiserId}/creative/${creativeId}?region=${region}`,
10939
+ format: null,
10940
+ lastShown: null,
10941
+ landingDomain: null,
10942
+ imageUrls: [],
10943
+ youtubeVideoId: null,
10944
+ videoUrl: null,
10945
+ variations: null
10946
+ });
10947
+ }
10948
+ }
10949
+ const videoCount = ads.filter((a) => a.format === "video").length;
10950
+ const imageCount = ads.filter((a) => a.format === "image").length;
10951
+ const textCount = ads.filter((a) => a.format === "text").length;
10952
+ return {
10953
+ advertiserId,
10954
+ advertiserName: listing.advertiserName,
10955
+ domain: opts.domain ?? null,
10956
+ region,
10957
+ totalCreatives: ads.length,
10958
+ videoCount,
10959
+ imageCount,
10960
+ textCount,
10961
+ ads
10962
+ };
10963
+ }
10964
+ async hydrateCreative(page, advertiserId, creativeId) {
10965
+ return page.evaluate(async ({ advertiserId: advertiserId2, creativeId: creativeId2 }) => {
10966
+ const empty = { format: null, lastShown: null, landingDomain: null, imageUrls: [], youtubeVideoId: null, videoUrl: null, variations: null };
10967
+ const rpcUrl = "https://adstransparency.google.com/anji/_/rpc/LookupService/GetCreativeById?authuser=0";
10968
+ const payload = { "1": advertiserId2, "2": creativeId2, "5": { "1": 1, "2": 1 } };
10969
+ const resp = await fetch(rpcUrl, {
10970
+ method: "POST",
10971
+ headers: { "content-type": "application/x-www-form-urlencoded", "x-same-domain": "1" },
10972
+ body: "f.req=" + encodeURIComponent(JSON.stringify(payload)),
10973
+ credentials: "include"
10974
+ }).then((r) => r.text()).catch(() => "");
10975
+ if (!resp) return empty;
10976
+ const decode = (s) => s.replace(/\\u002f/gi, "/").replace(/\\u([0-9a-fA-F]{4})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)));
10977
+ const previewUrls = Array.from(resp.matchAll(/https:(?:\\u002f\\u002f|\/\/)[^"\\]*content\.js(?:[^"\\]|\\u[0-9a-fA-F]{4})*/g)).map((m) => decode(m[0]));
10978
+ const variations = previewUrls.length || null;
10979
+ let imageUrls = [];
10980
+ let youtubeVideoId = null;
10981
+ let videoUrl = null;
10982
+ const previewUrl = previewUrls[0];
10983
+ if (previewUrl) {
10984
+ const rendered = await fetch(previewUrl, { credentials: "omit" }).then((r) => r.text()).catch(() => "");
10985
+ const yt = rendered.match(/i\.ytimg\.com\/vi\/([A-Za-z0-9_-]{11})/);
10986
+ if (yt) youtubeVideoId = yt[1];
10987
+ const gv = rendered.match(/https:\/\/[a-z0-9-]+\.googlevideo\.com\/videoplayback[^"'\\ ]+/);
10988
+ if (gv) videoUrl = gv[0].replace(/\\u0026/g, "&").replace(/\\u003d/g, "=");
10989
+ const simgads = Array.from(rendered.matchAll(/(?:tpc\.googlesyndication\.com\/(?:archive\/)?simgad\/|\/simgad\/)([0-9]+)/g)).map((m) => "https://tpc.googlesyndication.com/simgad/" + m[1]);
10990
+ imageUrls = Array.from(new Set(simgads)).slice(0, 8);
10991
+ }
10992
+ const format = youtubeVideoId || videoUrl ? "video" : imageUrls.length ? "image" : "text";
10993
+ return { format, lastShown: null, landingDomain: null, imageUrls, youtubeVideoId, videoUrl, variations };
10994
+ }, { advertiserId, creativeId });
10995
+ }
10996
+ async autoScroll(page, rounds) {
10997
+ for (let i = 0; i < rounds; i++) {
10998
+ await page.evaluate(() => {
10999
+ if (document.scrollingElement) window.scrollTo(0, document.scrollingElement.scrollHeight);
11000
+ }).catch(() => {
11001
+ });
11002
+ await page.waitForTimeout(700);
11003
+ }
11004
+ }
11005
+ };
11006
+
11007
+ // src/api/google-ads-routes.ts
11008
+ import { fal as fal4 } from "@fal-ai/client";
11009
+ var GoogleAdsSearchBodySchema = z14.object({
11010
+ query: z14.string().trim().min(1, "query is required"),
11011
+ region: z14.string().trim().toUpperCase().length(2).optional(),
11012
+ maxResults: z14.number().int().min(1).max(20).optional()
11013
+ });
11014
+ var GoogleAdsPageIntelBodySchema = z14.object({
11015
+ advertiserId: z14.string().trim().optional(),
11016
+ domain: z14.string().trim().optional(),
11017
+ region: z14.string().trim().toUpperCase().length(2).optional(),
11018
+ maxAds: z14.number().int().min(1).max(200).optional()
11019
+ }).refine((d) => !!d.advertiserId || !!d.domain, { message: "advertiserId or domain is required" });
11020
+ var GoogleAdsTranscribeBodySchema = z14.object({
11021
+ videoUrl: z14.string().trim().min(1, "videoUrl is required")
11022
+ });
11023
+ function invalidRequest2(message) {
11024
+ return { error_code: "invalid_request", message };
11025
+ }
11026
+ function configuredKernelProxy2() {
11027
+ return (process.env.BROWSER_SERVICE_PROXY_ID ?? process.env.KERNEL_PROXY_ID)?.trim() || void 0;
11028
+ }
11029
+ async function kernelLaunchOptsResidential2() {
11030
+ let proxyId = configuredKernelProxy2();
11031
+ try {
11032
+ const resolution = await resolveKernelProxyId({
11033
+ kernelApiKey: browserServiceApiKey(),
11034
+ proxyMode: DEFAULT_PROXY_MODE,
11035
+ configuredKernelProxyId: configuredKernelProxy2(),
11036
+ gl: "us"
11037
+ });
11038
+ if (resolution.kernelProxyId) proxyId = resolution.kernelProxyId;
11039
+ } catch {
11040
+ proxyId = configuredKernelProxy2();
11041
+ }
11042
+ return { headless: true, kernelApiKey: browserServiceApiKey(), kernelProxyId: proxyId, viewport: { width: 1280, height: 900 }, locale: "en-US" };
11043
+ }
11044
+ function isBlockedMessage(msg) {
11045
+ return msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha");
11046
+ }
11047
+ var googleAdsApp = new Hono5();
11048
+ googleAdsApp.post("/search", createApiKeyAuth(), async (c) => {
11049
+ const raw = await c.req.json().catch(() => ({}));
11050
+ const parsed = GoogleAdsSearchBodySchema.safeParse(raw);
11051
+ if (!parsed.success) return c.json(invalidRequest2(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
11052
+ const body = parsed.data;
11053
+ const region = body.region ?? "US";
11054
+ const maxResults = Math.min(20, Math.max(1, body.maxResults ?? 10));
11055
+ const user = c.get("user");
11056
+ const gate = await acquireConcurrencyGate(user, "google_ads_search", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { query: body.query.trim(), region } });
11057
+ if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
11058
+ const driver = new BrowserDriver();
11059
+ let debited = false, refunded = false;
11060
+ try {
11061
+ const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.google_ads_search, LedgerOperation.GOOGLE_ADS_SEARCH, body.query.trim());
11062
+ if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.google_ads_search), 402);
11063
+ debited = true;
11064
+ await driver.launch(await kernelLaunchOptsResidential2());
11065
+ const extractor = new GoogleAdsExtractor(driver);
11066
+ const result = await extractor.search(body.query.trim(), region, maxResults);
11067
+ if (result.advertisers.length === 0) {
11068
+ await creditMc(user.id, MC_COSTS.google_ads_search, LedgerOperation.GOOGLE_ADS_SEARCH_REFUND, "empty result");
11069
+ refunded = true;
11070
+ await logRequestEvent({ userId: user.id, source: "google_ads_search", status: "failed", query: body.query.trim(), error: "empty result refunded" });
11071
+ return c.json({ error: "no advertisers found (refunded)" }, 503);
11072
+ }
11073
+ await logRequestEvent({ userId: user.id, source: "google_ads_search", status: "done", query: body.query.trim(), resultCount: result.advertisers.length, result });
11074
+ return c.json(result);
11075
+ } catch (err) {
11076
+ const msg = err instanceof Error ? err.message : String(err);
11077
+ if (debited && !refunded) await creditMc(user.id, MC_COSTS.google_ads_search, LedgerOperation.GOOGLE_ADS_SEARCH_REFUND, "failed call");
11078
+ await logRequestEvent({ userId: user.id, source: "google_ads_search", status: "failed", query: body.query.trim(), error: msg });
11079
+ return c.json({ error: msg }, isBlockedMessage(msg) ? 503 : 500);
11080
+ } finally {
11081
+ await driver.close();
11082
+ await releaseConcurrencyGate(gate.lockId);
11083
+ }
11084
+ });
11085
+ googleAdsApp.post("/page-intel", createApiKeyAuth(), async (c) => {
11086
+ const raw = await c.req.json().catch(() => ({}));
11087
+ const parsed = GoogleAdsPageIntelBodySchema.safeParse(raw);
11088
+ if (!parsed.success) return c.json(invalidRequest2(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
11089
+ const body = parsed.data;
11090
+ const region = body.region ?? "US";
11091
+ const maxAds = Math.min(200, Math.max(1, body.maxAds ?? 50));
11092
+ const queryLabel = body.advertiserId ?? body.domain ?? "";
11093
+ const user = c.get("user");
11094
+ const gate = await acquireConcurrencyGate(user, "google_ads_page_intel", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { advertiserId: body.advertiserId, domain: body.domain, region } });
11095
+ if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
11096
+ const driver = new BrowserDriver();
11097
+ let debited = false, refunded = false;
11098
+ try {
11099
+ const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.google_ads_intel, LedgerOperation.GOOGLE_ADS_INTEL, queryLabel);
11100
+ if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.google_ads_intel), 402);
11101
+ debited = true;
11102
+ await driver.launch(await kernelLaunchOptsResidential2());
11103
+ const extractor = new GoogleAdsExtractor(driver);
11104
+ const result = await extractor.pageIntel({ advertiserId: body.advertiserId, domain: body.domain, region, maxAds });
11105
+ if (result.ads.length === 0) {
11106
+ await creditMc(user.id, MC_COSTS.google_ads_intel, LedgerOperation.GOOGLE_ADS_INTEL_REFUND, "empty result");
11107
+ refunded = true;
11108
+ await logRequestEvent({ userId: user.id, source: "google_ads_page_intel", status: "failed", query: queryLabel, error: "empty result refunded" });
11109
+ return c.json({ error: "no ads found for this advertiser/domain (refunded)" }, 503);
11110
+ }
11111
+ await logRequestEvent({ userId: user.id, source: "google_ads_page_intel", status: "done", query: queryLabel, resultCount: result.ads.length, result });
11112
+ return c.json(result);
11113
+ } catch (err) {
11114
+ const msg = err instanceof Error ? err.message : String(err);
11115
+ if (debited && !refunded) await creditMc(user.id, MC_COSTS.google_ads_intel, LedgerOperation.GOOGLE_ADS_INTEL_REFUND, "failed call");
11116
+ await logRequestEvent({ userId: user.id, source: "google_ads_page_intel", status: "failed", query: queryLabel, error: msg });
11117
+ return c.json({ error: msg }, isBlockedMessage(msg) ? 503 : 500);
11118
+ } finally {
11119
+ await driver.close();
11120
+ await releaseConcurrencyGate(gate.lockId);
11121
+ }
11122
+ });
11123
+ googleAdsApp.post("/transcribe", createApiKeyAuth(), async (c) => {
11124
+ const raw = await c.req.json().catch(() => ({}));
11125
+ const parsed = GoogleAdsTranscribeBodySchema.safeParse(raw);
11126
+ if (!parsed.success) return c.json(invalidRequest2(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
11127
+ const body = parsed.data;
11128
+ const urlCheck = await validatePublicHttpUrl(body.videoUrl, { field: "videoUrl", requireHttps: false });
11129
+ if (urlCheck.error) return c.json(invalidRequest2(urlCheck.error), 400);
11130
+ const videoUrl = urlCheck.parsed.href;
11131
+ const user = c.get("user");
11132
+ const gate = await acquireConcurrencyGate(user, "google_ads_transcribe", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { videoUrl } });
11133
+ if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
11134
+ fal4.config({ credentials: process.env.FAL_KEY });
11135
+ const holdMc = MC_COSTS.google_ads_transcribe * 5;
11136
+ let debited = false;
11137
+ try {
11138
+ const { ok, balance_mc } = await debitMc(user.id, holdMc, LedgerOperation.GOOGLE_ADS_TRANSCRIBE, videoUrl);
11139
+ if (!ok) return c.json(insufficientBalanceResponse(balance_mc, holdMc), 402);
11140
+ debited = true;
11141
+ const transcript = await transcribeMediaUrl(videoUrl, "# Google Ad Transcript");
11142
+ const actualMins = Math.max(1, Math.ceil((Number.isFinite(transcript.durationMs) ? transcript.durationMs / 1e3 : 60) / 60));
11143
+ const diff = holdMc - MC_COSTS.google_ads_transcribe * actualMins;
11144
+ if (diff > 0) await creditMc(user.id, diff, LedgerOperation.GOOGLE_ADS_TRANSCRIBE_REFUND, "overestimate refund");
11145
+ else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.GOOGLE_ADS_TRANSCRIBE, videoUrl);
11146
+ await logRequestEvent({ userId: user.id, source: "google_ads_transcribe", status: "done", query: videoUrl, resultCount: transcript.chunks.length, result: { text: transcript.text, chunks: transcript.chunks, durationMs: transcript.durationMs } });
11147
+ return c.json(transcript);
11148
+ } catch (err) {
11149
+ const msg = err instanceof Error ? err.message : String(err);
11150
+ if (debited) await creditMc(user.id, holdMc, LedgerOperation.GOOGLE_ADS_TRANSCRIBE_REFUND, "failed call");
11151
+ await logRequestEvent({ userId: user.id, source: "google_ads_transcribe", status: "failed", query: videoUrl, error: msg });
11152
+ return c.json({ error: msg }, 500);
11153
+ } finally {
11154
+ await releaseConcurrencyGate(gate.lockId);
11155
+ }
11156
+ });
11157
+
11158
+ // src/api/instagram-routes.ts
11159
+ import { Hono as Hono6 } from "hono";
11160
+ import { z as z15 } from "zod";
10802
11161
  import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
10803
11162
  import { homedir as homedir3 } from "os";
10804
11163
  import { join as join5 } from "path";
@@ -11162,33 +11521,33 @@ async function extractInstagramMediaFromPage(page, sourceUrl, capturedMediaUrls
11162
11521
  }
11163
11522
 
11164
11523
  // src/api/instagram-routes.ts
11165
- var InstagramProfileContentBodySchema = z14.object({
11166
- handle: z14.string().trim().optional(),
11167
- url: z14.string().trim().optional(),
11168
- profile: z14.string().trim().min(1).optional(),
11169
- saveProfileChanges: z14.boolean().optional(),
11170
- save_profile_changes: z14.boolean().optional(),
11171
- maxItems: z14.number().int().min(1).max(2e3).default(50),
11172
- maxScrolls: z14.number().int().min(0).max(250).default(10),
11173
- scrollDelayMs: z14.number().int().min(250).max(5e3).default(1200),
11174
- stableScrollLimit: z14.number().int().min(1).max(10).default(4)
11524
+ var InstagramProfileContentBodySchema = z15.object({
11525
+ handle: z15.string().trim().optional(),
11526
+ url: z15.string().trim().optional(),
11527
+ profile: z15.string().trim().min(1).optional(),
11528
+ saveProfileChanges: z15.boolean().optional(),
11529
+ save_profile_changes: z15.boolean().optional(),
11530
+ maxItems: z15.number().int().min(1).max(2e3).default(50),
11531
+ maxScrolls: z15.number().int().min(0).max(250).default(10),
11532
+ scrollDelayMs: z15.number().int().min(250).max(5e3).default(1200),
11533
+ stableScrollLimit: z15.number().int().min(1).max(10).default(4)
11175
11534
  }).refine((d) => !!d.handle || !!d.url, {
11176
11535
  message: "handle or url is required"
11177
11536
  });
11178
- var InstagramMediaTypeSchema = z14.enum(["image", "video", "audio"]);
11179
- var InstagramMediaDownloadBodySchema = z14.object({
11180
- url: z14.string().trim().min(1, "url is required"),
11181
- profile: z14.string().trim().min(1).optional(),
11182
- saveProfileChanges: z14.boolean().optional(),
11183
- save_profile_changes: z14.boolean().optional(),
11184
- mediaTypes: z14.array(InstagramMediaTypeSchema).default(["image", "video", "audio"]),
11185
- downloadMedia: z14.boolean().default(true),
11186
- downloadAllTracks: z14.boolean().default(false),
11187
- includeTranscript: z14.boolean().default(false),
11188
- mux: z14.boolean().default(true)
11537
+ var InstagramMediaTypeSchema = z15.enum(["image", "video", "audio"]);
11538
+ var InstagramMediaDownloadBodySchema = z15.object({
11539
+ url: z15.string().trim().min(1, "url is required"),
11540
+ profile: z15.string().trim().min(1).optional(),
11541
+ saveProfileChanges: z15.boolean().optional(),
11542
+ save_profile_changes: z15.boolean().optional(),
11543
+ mediaTypes: z15.array(InstagramMediaTypeSchema).default(["image", "video", "audio"]),
11544
+ downloadMedia: z15.boolean().default(true),
11545
+ downloadAllTracks: z15.boolean().default(false),
11546
+ includeTranscript: z15.boolean().default(false),
11547
+ mux: z15.boolean().default(true)
11189
11548
  });
11190
- var instagramApp = new Hono5();
11191
- function invalidRequest2(message) {
11549
+ var instagramApp = new Hono6();
11550
+ function invalidRequest3(message) {
11192
11551
  return { error_code: "invalid_request", message };
11193
11552
  }
11194
11553
  function resolveSaveProfileChanges(body) {
@@ -11198,14 +11557,14 @@ function resolveProfileName(body) {
11198
11557
  const explicit = body.profile?.trim();
11199
11558
  return explicit || browserServiceProfileName();
11200
11559
  }
11201
- function configuredKernelProxy2() {
11560
+ function configuredKernelProxy3() {
11202
11561
  return (process.env.BROWSER_SERVICE_PROXY_ID ?? process.env.KERNEL_PROXY_ID)?.trim() || void 0;
11203
11562
  }
11204
11563
  async function kernelLaunchOptsDirect(profileName, saveProfileChanges) {
11205
11564
  return {
11206
11565
  headless: true,
11207
11566
  kernelApiKey: browserServiceApiKey(),
11208
- kernelProxyId: configuredKernelProxy2(),
11567
+ kernelProxyId: configuredKernelProxy3(),
11209
11568
  ...profileName ? { kernelProfileName: profileName } : {},
11210
11569
  ...typeof saveProfileChanges === "boolean" ? { kernelProfileSaveChanges: saveProfileChanges } : {},
11211
11570
  viewport: { width: 1280, height: 900 },
@@ -11291,13 +11650,13 @@ function trackFilename(track, index, shortcode) {
11291
11650
  instagramApp.post("/profile-content", createApiKeyAuth(), async (c) => {
11292
11651
  const raw = await c.req.json().catch(() => ({}));
11293
11652
  const parsed = InstagramProfileContentBodySchema.safeParse(raw);
11294
- if (!parsed.success) return c.json(invalidRequest2(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
11653
+ if (!parsed.success) return c.json(invalidRequest3(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
11295
11654
  const body = parsed.data;
11296
11655
  let target;
11297
11656
  try {
11298
11657
  target = normalizeInstagramProfileInput(body.url ?? body.handle ?? "");
11299
11658
  } catch (err) {
11300
- return c.json(invalidRequest2(err instanceof Error ? err.message : String(err)), 400);
11659
+ return c.json(invalidRequest3(err instanceof Error ? err.message : String(err)), 400);
11301
11660
  }
11302
11661
  const user = c.get("user");
11303
11662
  const gate = await acquireConcurrencyGate(user, "instagram_profile_content", {
@@ -11339,12 +11698,12 @@ instagramApp.post("/profile-content", createApiKeyAuth(), async (c) => {
11339
11698
  instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
11340
11699
  const raw = await c.req.json().catch(() => ({}));
11341
11700
  const parsed = InstagramMediaDownloadBodySchema.safeParse(raw);
11342
- if (!parsed.success) return c.json(invalidRequest2(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
11701
+ if (!parsed.success) return c.json(invalidRequest3(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
11343
11702
  const body = parsed.data;
11344
11703
  const urlCheck = await validatePublicHttpUrl(body.url, { field: "url", requireHttps: false });
11345
- if (urlCheck.error) return c.json(invalidRequest2(urlCheck.error), 400);
11704
+ if (urlCheck.error) return c.json(invalidRequest3(urlCheck.error), 400);
11346
11705
  const sourceUrl = urlCheck.parsed;
11347
- if (!isAllowedInstagramPageUrl(sourceUrl)) return c.json(invalidRequest2("url must be an Instagram post, reel, or tv URL"), 400);
11706
+ if (!isAllowedInstagramPageUrl(sourceUrl)) return c.json(invalidRequest3("url must be an Instagram post, reel, or tv URL"), 400);
11348
11707
  const user = c.get("user");
11349
11708
  const gate = await acquireConcurrencyGate(user, "instagram_media_download", {
11350
11709
  reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
@@ -11461,13 +11820,13 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
11461
11820
  });
11462
11821
 
11463
11822
  // src/api/reddit-routes.ts
11464
- import { Hono as Hono6 } from "hono";
11465
- import { z as z15 } from "zod";
11466
- var RedditThreadBodySchema = z15.object({
11467
- url: z15.string().trim().min(1, "url is required"),
11468
- maxComments: z15.number().int().min(1).max(2e3).optional()
11823
+ import { Hono as Hono7 } from "hono";
11824
+ import { z as z16 } from "zod";
11825
+ var RedditThreadBodySchema = z16.object({
11826
+ url: z16.string().trim().min(1, "url is required"),
11827
+ maxComments: z16.number().int().min(1).max(2e3).optional()
11469
11828
  });
11470
- function invalidRequest3(message) {
11829
+ function invalidRequest4(message) {
11471
11830
  return { error_code: "invalid_request", message };
11472
11831
  }
11473
11832
  function toOldReddit(url) {
@@ -11521,14 +11880,14 @@ var PARSE_REDDIT = `(() => {
11521
11880
  });
11522
11881
  return { title: title, author: author, score: score, postBody: postBody, blocked: blocked, numComments: comments.length, comments: comments };
11523
11882
  })()`;
11524
- var redditApp = new Hono6();
11883
+ var redditApp = new Hono7();
11525
11884
  redditApp.post("/thread", createApiKeyAuth(), async (c) => {
11526
11885
  const raw = await c.req.json().catch(() => ({}));
11527
11886
  const parsed = RedditThreadBodySchema.safeParse(raw);
11528
- if (!parsed.success) return c.json(invalidRequest3(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
11887
+ if (!parsed.success) return c.json(invalidRequest4(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
11529
11888
  const body = parsed.data;
11530
11889
  const oldUrl = toOldReddit(body.url);
11531
- if (!oldUrl) return c.json(invalidRequest3("url must be a reddit.com link"), 400);
11890
+ if (!oldUrl) return c.json(invalidRequest4("url must be a reddit.com link"), 400);
11532
11891
  const user = c.get("user");
11533
11892
  const gate = await acquireConcurrencyGate(user, "reddit_thread", {
11534
11893
  reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
@@ -11589,7 +11948,7 @@ redditApp.post("/thread", createApiKeyAuth(), async (c) => {
11589
11948
  });
11590
11949
 
11591
11950
  // src/api/maps-routes.ts
11592
- import { Hono as Hono7 } from "hono";
11951
+ import { Hono as Hono8 } from "hono";
11593
11952
 
11594
11953
  // src/extractor/MapsNavigator.ts
11595
11954
  var MapsNavigator = class {
@@ -11764,7 +12123,7 @@ var MapsReviewCollector = class {
11764
12123
  };
11765
12124
 
11766
12125
  // src/extractor/MapsExtractor.ts
11767
- import { z as z16 } from "zod";
12126
+ import { z as z17 } from "zod";
11768
12127
  var MapsExtractor = class {
11769
12128
  constructor(driver) {
11770
12129
  this.driver = driver;
@@ -11950,7 +12309,7 @@ var MapsExtractor = class {
11950
12309
  });
11951
12310
  return rows;
11952
12311
  }, { hoursTable: MapsSelectors.hoursTable, hoursTableAlt: MapsSelectors.hoursTableAlt });
11953
- const result = z16.array(RawMapsHoursRowSchema).safeParse(raw);
12312
+ const result = z17.array(RawMapsHoursRowSchema).safeParse(raw);
11954
12313
  if (!result.success) {
11955
12314
  console.warn("[MapsExtractor] hours parse failed", result.error.flatten());
11956
12315
  return [];
@@ -12014,7 +12373,7 @@ var MapsExtractor = class {
12014
12373
  });
12015
12374
  return results;
12016
12375
  });
12017
- const result = z16.array(RawMapsAboutAttributeSchema).safeParse(raw);
12376
+ const result = z17.array(RawMapsAboutAttributeSchema).safeParse(raw);
12018
12377
  if (!result.success) {
12019
12378
  console.warn("[MapsExtractor] about parse failed", result.error.flatten());
12020
12379
  return [];
@@ -12390,7 +12749,7 @@ function mapsErrorResponse(c, err, errorCode) {
12390
12749
  attempts: rotationError?.attempts ?? void 0
12391
12750
  }, retryable ? 503 : 500);
12392
12751
  }
12393
- var mapsApp = new Hono7();
12752
+ var mapsApp = new Hono8();
12394
12753
  mapsApp.post("/search", createApiKeyAuth(), async (c) => {
12395
12754
  const user = c.get("user");
12396
12755
  const body = await c.req.json().catch(() => ({}));
@@ -12528,12 +12887,12 @@ mapsApp.post("/place", createApiKeyAuth(), async (c) => {
12528
12887
  });
12529
12888
 
12530
12889
  // src/api/directory-routes.ts
12531
- import { Hono as Hono8 } from "hono";
12890
+ import { Hono as Hono9 } from "hono";
12532
12891
 
12533
12892
  // src/directory/directory-workflow.ts
12534
12893
  import { mkdir as mkdir2, writeFile } from "fs/promises";
12535
12894
  import { join as join6 } from "path";
12536
- import { z as z17 } from "zod";
12895
+ import { z as z18 } from "zod";
12537
12896
 
12538
12897
  // src/directory/location-db.ts
12539
12898
  import { access, readFile } from "fs/promises";
@@ -12694,24 +13053,24 @@ async function resolveDirectoryMarkets(options) {
12694
13053
  }
12695
13054
 
12696
13055
  // src/directory/directory-workflow.ts
12697
- var DirectoryWorkflowOptionsSchema = z17.object({
12698
- query: z17.string().min(1),
12699
- state: z17.string().min(2).default("TN"),
12700
- minPopulation: z17.number().int().min(0).default(1e5),
12701
- populationYear: z17.union(POPULATION_YEARS.map((year) => z17.literal(year))).default(2025),
12702
- maxCities: z17.number().int().min(1).max(100).default(25),
12703
- maxResultsPerCity: z17.number().int().min(1).max(50).default(50),
12704
- concurrency: z17.number().int().min(1).max(5).default(5),
12705
- includeZipGroups: z17.boolean().default(true),
12706
- usZipsCsvPath: z17.string().optional(),
12707
- saveCsv: z17.boolean().default(true),
12708
- gl: z17.string().length(2).default("us"),
12709
- hl: z17.string().length(2).default("en"),
12710
- proxyMode: z17.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
12711
- proxyZip: z17.string().regex(/^\d{5}$/).optional(),
12712
- debug: z17.boolean().default(false),
12713
- headless: z17.boolean().default(true),
12714
- kernelApiKey: z17.string().optional()
13056
+ var DirectoryWorkflowOptionsSchema = z18.object({
13057
+ query: z18.string().min(1),
13058
+ state: z18.string().min(2).default("TN"),
13059
+ minPopulation: z18.number().int().min(0).default(1e5),
13060
+ populationYear: z18.union(POPULATION_YEARS.map((year) => z18.literal(year))).default(2025),
13061
+ maxCities: z18.number().int().min(1).max(100).default(25),
13062
+ maxResultsPerCity: z18.number().int().min(1).max(50).default(50),
13063
+ concurrency: z18.number().int().min(1).max(5).default(5),
13064
+ includeZipGroups: z18.boolean().default(true),
13065
+ usZipsCsvPath: z18.string().optional(),
13066
+ saveCsv: z18.boolean().default(true),
13067
+ gl: z18.string().length(2).default("us"),
13068
+ hl: z18.string().length(2).default("en"),
13069
+ proxyMode: z18.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
13070
+ proxyZip: z18.string().regex(/^\d{5}$/).optional(),
13071
+ debug: z18.boolean().default(false),
13072
+ headless: z18.boolean().default(true),
13073
+ kernelApiKey: z18.string().optional()
12715
13074
  });
12716
13075
  function errorMessage(err) {
12717
13076
  return err instanceof Error ? err.message : String(err);
@@ -12932,7 +13291,7 @@ async function runDirectoryWorkflowFromPlan(options, plan) {
12932
13291
  }
12933
13292
 
12934
13293
  // src/api/directory-routes.ts
12935
- var directoryApp = new Hono8();
13294
+ var directoryApp = new Hono9();
12936
13295
  directoryApp.post("/run", createApiKeyAuth(), async (c) => {
12937
13296
  const user = c.get("user");
12938
13297
  const body = await c.req.json().catch(() => ({}));
@@ -13005,35 +13364,35 @@ directoryApp.post("/run", createApiKeyAuth(), async (c) => {
13005
13364
  // src/api/workflow-routes.ts
13006
13365
  import { createHmac } from "crypto";
13007
13366
  import { readFile as readFile2 } from "fs/promises";
13008
- import { Hono as Hono9 } from "hono";
13009
- import { z as z18 } from "zod";
13010
- var workflowApp = new Hono9();
13011
- var WorkflowInputSchema = z18.record(z18.unknown()).default({});
13012
- var WorkflowIdSchema = z18.string().min(1);
13013
- var CadenceSchema = z18.enum(["daily", "weekly", "monthly"]);
13014
- var ScheduleStatusSchema = z18.enum(["active", "paused"]);
13015
- var RunBodySchema = z18.object({
13367
+ import { Hono as Hono10 } from "hono";
13368
+ import { z as z19 } from "zod";
13369
+ var workflowApp = new Hono10();
13370
+ var WorkflowInputSchema = z19.record(z19.unknown()).default({});
13371
+ var WorkflowIdSchema = z19.string().min(1);
13372
+ var CadenceSchema = z19.enum(["daily", "weekly", "monthly"]);
13373
+ var ScheduleStatusSchema = z19.enum(["active", "paused"]);
13374
+ var RunBodySchema = z19.object({
13016
13375
  workflowId: WorkflowIdSchema,
13017
13376
  input: WorkflowInputSchema,
13018
- webhookUrl: z18.string().url().optional()
13377
+ webhookUrl: z19.string().url().optional()
13019
13378
  });
13020
- var ScheduleCreateSchema = z18.object({
13379
+ var ScheduleCreateSchema = z19.object({
13021
13380
  workflowId: WorkflowIdSchema,
13022
- name: z18.string().min(1).max(120).optional(),
13381
+ name: z19.string().min(1).max(120).optional(),
13023
13382
  input: WorkflowInputSchema,
13024
13383
  cadence: CadenceSchema.default("weekly"),
13025
- timezone: z18.string().min(1).max(64).default("UTC"),
13026
- webhookUrl: z18.string().url().optional(),
13027
- nextRunAt: z18.string().datetime().optional()
13384
+ timezone: z19.string().min(1).max(64).default("UTC"),
13385
+ webhookUrl: z19.string().url().optional(),
13386
+ nextRunAt: z19.string().datetime().optional()
13028
13387
  });
13029
- var SchedulePatchSchema = z18.object({
13030
- name: z18.string().min(1).max(120).optional(),
13388
+ var SchedulePatchSchema = z19.object({
13389
+ name: z19.string().min(1).max(120).optional(),
13031
13390
  status: ScheduleStatusSchema.optional(),
13032
13391
  input: WorkflowInputSchema.optional(),
13033
13392
  cadence: CadenceSchema.optional(),
13034
- timezone: z18.string().min(1).max(64).optional(),
13035
- webhookUrl: z18.string().url().nullable().optional(),
13036
- nextRunAt: z18.string().datetime().nullable().optional()
13393
+ timezone: z19.string().min(1).max(64).optional(),
13394
+ webhookUrl: z19.string().url().nullable().optional(),
13395
+ nextRunAt: z19.string().datetime().nullable().optional()
13037
13396
  });
13038
13397
  function hostedWorkflowOutputDir() {
13039
13398
  return workflowOutputBaseDir(process.env.MCP_SCRAPER_WORKFLOW_OUTPUT_DIR?.trim() || "/tmp/mcp-scraper-workflows");
@@ -13537,7 +13896,7 @@ workflowApp.post("/cron/dispatch", async (c) => {
13537
13896
  });
13538
13897
 
13539
13898
  // src/api/serp-intelligence-routes.ts
13540
- import { Hono as Hono10 } from "hono";
13899
+ import { Hono as Hono11 } from "hono";
13541
13900
 
13542
13901
  // src/serp-intelligence/page-snapshot-extractor.ts
13543
13902
  import { createHash as createHash2 } from "crypto";
@@ -13849,7 +14208,7 @@ async function capturePageSnapshots(targets, options = {}) {
13849
14208
  }
13850
14209
 
13851
14210
  // src/serp-intelligence/schemas.ts
13852
- import { z as z19 } from "zod";
14211
+ import { z as z20 } from "zod";
13853
14212
  var SerpIntelligenceDeviceValues = ["desktop", "mobile"];
13854
14213
  var SerpIntelligenceProxyModeValues = ["location", "configured", "none"];
13855
14214
  var SerpIntelligenceAttemptOutcomeValues = [
@@ -13911,171 +14270,171 @@ function isPublicHttpUrl(value) {
13911
14270
  return false;
13912
14271
  }
13913
14272
  }
13914
- var SerpIntelligencePublicHttpUrlSchema = z19.string().url().refine(isPublicHttpUrl, "url must be a public HTTP or HTTPS URL");
13915
- var SerpIntelligenceCaptureBodySchema = z19.object({
13916
- query: z19.string().trim().min(1, "query is required"),
13917
- location: z19.string().trim().min(1).optional(),
13918
- gl: z19.string().trim().length(2).default("us"),
13919
- hl: z19.string().trim().length(2).default("en"),
13920
- device: z19.enum(SerpIntelligenceDeviceValues).default("desktop"),
13921
- proxyMode: z19.enum(SerpIntelligenceProxyModeValues).default(DEFAULT_PROXY_MODE),
13922
- proxyZip: z19.string().regex(/^\d{5}$/).optional(),
13923
- pages: z19.number().int().min(1).max(2).default(1),
13924
- debug: z19.boolean().default(false),
13925
- includePageSnapshots: z19.boolean().default(false),
13926
- pageSnapshotLimit: z19.number().int().min(0).max(10).default(0)
14273
+ var SerpIntelligencePublicHttpUrlSchema = z20.string().url().refine(isPublicHttpUrl, "url must be a public HTTP or HTTPS URL");
14274
+ var SerpIntelligenceCaptureBodySchema = z20.object({
14275
+ query: z20.string().trim().min(1, "query is required"),
14276
+ location: z20.string().trim().min(1).optional(),
14277
+ gl: z20.string().trim().length(2).default("us"),
14278
+ hl: z20.string().trim().length(2).default("en"),
14279
+ device: z20.enum(SerpIntelligenceDeviceValues).default("desktop"),
14280
+ proxyMode: z20.enum(SerpIntelligenceProxyModeValues).default(DEFAULT_PROXY_MODE),
14281
+ proxyZip: z20.string().regex(/^\d{5}$/).optional(),
14282
+ pages: z20.number().int().min(1).max(2).default(1),
14283
+ debug: z20.boolean().default(false),
14284
+ includePageSnapshots: z20.boolean().default(false),
14285
+ pageSnapshotLimit: z20.number().int().min(0).max(10).default(0)
13927
14286
  }).strict();
13928
- var SerpIntelligencePageSnapshotRequestSchema = z19.object({
14287
+ var SerpIntelligencePageSnapshotRequestSchema = z20.object({
13929
14288
  url: SerpIntelligencePublicHttpUrlSchema,
13930
- sourceKind: z19.enum(SerpPageSnapshotSourceKindValues).default("configured_target"),
13931
- sourcePosition: z19.number().int().min(1).optional()
14289
+ sourceKind: z20.enum(SerpPageSnapshotSourceKindValues).default("configured_target"),
14290
+ sourcePosition: z20.number().int().min(1).optional()
13932
14291
  }).strict();
13933
- var SerpIntelligencePageSnapshotsBodySchema = z19.object({
13934
- urls: z19.array(SerpIntelligencePublicHttpUrlSchema).min(1).max(25),
13935
- targets: z19.array(SerpIntelligencePageSnapshotRequestSchema).min(1).max(25).optional(),
13936
- maxConcurrency: z19.number().int().min(1).max(5).default(2),
13937
- timeoutMs: z19.number().int().min(1e3).max(6e4).default(15e3),
13938
- debug: z19.boolean().default(false)
14292
+ var SerpIntelligencePageSnapshotsBodySchema = z20.object({
14293
+ urls: z20.array(SerpIntelligencePublicHttpUrlSchema).min(1).max(25),
14294
+ targets: z20.array(SerpIntelligencePageSnapshotRequestSchema).min(1).max(25).optional(),
14295
+ maxConcurrency: z20.number().int().min(1).max(5).default(2),
14296
+ timeoutMs: z20.number().int().min(1e3).max(6e4).default(15e3),
14297
+ debug: z20.boolean().default(false)
13939
14298
  }).strict();
13940
- var SerpIntelligenceAICitationSchema = z19.object({
13941
- text: z19.string(),
13942
- href: z19.string()
14299
+ var SerpIntelligenceAICitationSchema = z20.object({
14300
+ text: z20.string(),
14301
+ href: z20.string()
13943
14302
  }).strict();
13944
- var SerpIntelligenceOrganicResultSchema = z19.object({
13945
- position: z19.number().int().min(1),
13946
- title: z19.string(),
13947
- url: z19.string(),
13948
- domain: z19.string(),
13949
- cite: z19.string().nullable(),
13950
- snippet: z19.string().nullable(),
13951
- isRedditStyle: z19.boolean(),
13952
- inlineRating: z19.object({
13953
- value: z19.string(),
13954
- count: z19.string()
14303
+ var SerpIntelligenceOrganicResultSchema = z20.object({
14304
+ position: z20.number().int().min(1),
14305
+ title: z20.string(),
14306
+ url: z20.string(),
14307
+ domain: z20.string(),
14308
+ cite: z20.string().nullable(),
14309
+ snippet: z20.string().nullable(),
14310
+ isRedditStyle: z20.boolean(),
14311
+ inlineRating: z20.object({
14312
+ value: z20.string(),
14313
+ count: z20.string()
13955
14314
  }).strict().nullable()
13956
14315
  }).strict();
13957
- var SerpIntelligenceLocationEvidenceSchema = z19.object({
13958
- status: z19.enum(SerpIntelligenceLocalizationStatusValues),
13959
- expected: z19.object({
13960
- city: z19.string(),
13961
- regionCode: z19.string().nullable(),
13962
- canonicalLocation: z19.string()
14316
+ var SerpIntelligenceLocationEvidenceSchema = z20.object({
14317
+ status: z20.enum(SerpIntelligenceLocalizationStatusValues),
14318
+ expected: z20.object({
14319
+ city: z20.string(),
14320
+ regionCode: z20.string().nullable(),
14321
+ canonicalLocation: z20.string()
13963
14322
  }).strict().nullable(),
13964
- candidates: z19.array(z19.object({
13965
- city: z19.string(),
13966
- regionCode: z19.string(),
13967
- count: z19.number().int().min(0),
13968
- examples: z19.array(z19.string())
14323
+ candidates: z20.array(z20.object({
14324
+ city: z20.string(),
14325
+ regionCode: z20.string(),
14326
+ count: z20.number().int().min(0),
14327
+ examples: z20.array(z20.string())
13969
14328
  }).strict())
13970
14329
  }).strict();
13971
- var SerpIntelligenceHarvestResultSchema = z19.object({
13972
- seed: z19.string(),
13973
- location: z19.string().nullable(),
13974
- extractedAt: z19.string(),
13975
- totalQuestions: z19.number().int().min(0),
13976
- surface: z19.enum(["web", "aim", "unknown"]),
13977
- aiOverview: z19.object({
13978
- detected: z19.boolean(),
13979
- text: z19.string().nullable(),
13980
- citations: z19.array(SerpIntelligenceAICitationSchema),
13981
- expanded: z19.boolean().optional(),
13982
- fullyExpanded: z19.boolean().optional(),
13983
- sections: z19.array(z19.string()).optional()
14330
+ var SerpIntelligenceHarvestResultSchema = z20.object({
14331
+ seed: z20.string(),
14332
+ location: z20.string().nullable(),
14333
+ extractedAt: z20.string(),
14334
+ totalQuestions: z20.number().int().min(0),
14335
+ surface: z20.enum(["web", "aim", "unknown"]),
14336
+ aiOverview: z20.object({
14337
+ detected: z20.boolean(),
14338
+ text: z20.string().nullable(),
14339
+ citations: z20.array(SerpIntelligenceAICitationSchema),
14340
+ expanded: z20.boolean().optional(),
14341
+ fullyExpanded: z20.boolean().optional(),
14342
+ sections: z20.array(z20.string()).optional()
13984
14343
  }).strict(),
13985
- aiMode: z19.object({
13986
- detected: z19.boolean(),
13987
- text: z19.string().nullable(),
13988
- citations: z19.array(SerpIntelligenceAICitationSchema)
14344
+ aiMode: z20.object({
14345
+ detected: z20.boolean(),
14346
+ text: z20.string().nullable(),
14347
+ citations: z20.array(SerpIntelligenceAICitationSchema)
13989
14348
  }).strict(),
13990
- tree: z19.array(z19.unknown()),
13991
- flat: z19.array(z19.unknown()),
13992
- videos: z19.array(z19.unknown()),
13993
- forums: z19.array(z19.unknown()),
13994
- organicResults: z19.array(SerpIntelligenceOrganicResultSchema),
13995
- localPack: z19.array(z19.unknown()),
13996
- entityIds: z19.object({
13997
- entities: z19.array(z19.object({
13998
- name: z19.string(),
13999
- kgId: z19.string().nullable(),
14000
- cid: z19.string().nullable(),
14001
- gcid: z19.string().nullable()
14349
+ tree: z20.array(z20.unknown()),
14350
+ flat: z20.array(z20.unknown()),
14351
+ videos: z20.array(z20.unknown()),
14352
+ forums: z20.array(z20.unknown()),
14353
+ organicResults: z20.array(SerpIntelligenceOrganicResultSchema),
14354
+ localPack: z20.array(z20.unknown()),
14355
+ entityIds: z20.object({
14356
+ entities: z20.array(z20.object({
14357
+ name: z20.string(),
14358
+ kgId: z20.string().nullable(),
14359
+ cid: z20.string().nullable(),
14360
+ gcid: z20.string().nullable()
14002
14361
  }).strict()),
14003
- kgIds: z19.array(z19.string()),
14004
- cids: z19.array(z19.string()),
14005
- gcids: z19.array(z19.string())
14362
+ kgIds: z20.array(z20.string()),
14363
+ cids: z20.array(z20.string()),
14364
+ gcids: z20.array(z20.string())
14006
14365
  }).strict(),
14007
- stats: z19.object({
14008
- seed: z19.string(),
14009
- totalQuestions: z19.number().int().min(0),
14010
- maxDepthReached: z19.number().int().min(0),
14011
- durationMs: z19.number().min(0),
14012
- errorCount: z19.number().int().min(0)
14366
+ stats: z20.object({
14367
+ seed: z20.string(),
14368
+ totalQuestions: z20.number().int().min(0),
14369
+ maxDepthReached: z20.number().int().min(0),
14370
+ durationMs: z20.number().min(0),
14371
+ errorCount: z20.number().int().min(0)
14013
14372
  }).strict(),
14014
- diagnostics: z19.object({
14015
- completionStatus: z19.enum(["paa_found", "no_paa", "serp_only"]),
14016
- problem: z19.null(),
14017
- warnings: z19.array(z19.unknown()).optional(),
14018
- debug: z19.object({
14373
+ diagnostics: z20.object({
14374
+ completionStatus: z20.enum(["paa_found", "no_paa", "serp_only"]),
14375
+ problem: z20.null(),
14376
+ warnings: z20.array(z20.unknown()).optional(),
14377
+ debug: z20.object({
14019
14378
  locationEvidence: SerpIntelligenceLocationEvidenceSchema.optional()
14020
14379
  }).passthrough().optional()
14021
14380
  }).passthrough(),
14022
- whatPeopleSaying: z19.array(z19.unknown())
14381
+ whatPeopleSaying: z20.array(z20.unknown())
14023
14382
  }).strict();
14024
- var SerpIntelligenceCaptureAttemptSchema = z19.object({
14025
- attemptNumber: z19.number().int().min(1),
14026
- outcome: z19.enum(SerpIntelligenceAttemptOutcomeValues),
14027
- startedAt: z19.string().optional(),
14028
- completedAt: z19.string().optional(),
14029
- durationMs: z19.number().min(0).optional(),
14030
- problemCode: z19.string().optional(),
14031
- message: z19.string().optional(),
14032
- kernelSessionId: z19.string().nullable().optional(),
14033
- cleanupSucceeded: z19.boolean().nullable().optional()
14383
+ var SerpIntelligenceCaptureAttemptSchema = z20.object({
14384
+ attemptNumber: z20.number().int().min(1),
14385
+ outcome: z20.enum(SerpIntelligenceAttemptOutcomeValues),
14386
+ startedAt: z20.string().optional(),
14387
+ completedAt: z20.string().optional(),
14388
+ durationMs: z20.number().min(0).optional(),
14389
+ problemCode: z20.string().optional(),
14390
+ message: z20.string().optional(),
14391
+ kernelSessionId: z20.string().nullable().optional(),
14392
+ cleanupSucceeded: z20.boolean().nullable().optional()
14034
14393
  }).strict();
14035
- var SerpPageSnapshotCaptureSchema = z19.object({
14394
+ var SerpPageSnapshotCaptureSchema = z20.object({
14036
14395
  url: SerpIntelligencePublicHttpUrlSchema,
14037
14396
  requestedUrl: SerpIntelligencePublicHttpUrlSchema,
14038
14397
  finalUrl: SerpIntelligencePublicHttpUrlSchema.nullable(),
14039
- sourceKind: z19.enum(SerpPageSnapshotSourceKindValues),
14040
- sourcePosition: z19.number().int().min(1).nullable(),
14041
- status: z19.enum(SerpPageFetchStatusValues),
14042
- fetchedVia: z19.enum(SerpPageFetchedViaValues).nullable(),
14043
- httpStatus: z19.number().int().min(100).max(599).nullable(),
14044
- contentType: z19.string().nullable(),
14045
- title: z19.string().nullable(),
14398
+ sourceKind: z20.enum(SerpPageSnapshotSourceKindValues),
14399
+ sourcePosition: z20.number().int().min(1).nullable(),
14400
+ status: z20.enum(SerpPageFetchStatusValues),
14401
+ fetchedVia: z20.enum(SerpPageFetchedViaValues).nullable(),
14402
+ httpStatus: z20.number().int().min(100).max(599).nullable(),
14403
+ contentType: z20.string().nullable(),
14404
+ title: z20.string().nullable(),
14046
14405
  canonicalUrl: SerpIntelligencePublicHttpUrlSchema.nullable(),
14047
- metaDescription: z19.string().nullable(),
14048
- headings: z19.array(z19.object({
14049
- level: z19.number().int().min(1).max(6),
14050
- text: z19.string()
14406
+ metaDescription: z20.string().nullable(),
14407
+ headings: z20.array(z20.object({
14408
+ level: z20.number().int().min(1).max(6),
14409
+ text: z20.string()
14051
14410
  }).strict()).default([]),
14052
- artifact: z19.object({
14053
- htmlBlobUrl: z19.string().url().nullable(),
14054
- textBlobUrl: z19.string().url().nullable(),
14055
- markdownBlobUrl: z19.string().url().nullable(),
14056
- screenshotBlobUrl: z19.string().url().nullable(),
14057
- contentSha256: z19.string().nullable(),
14058
- capturedAt: z19.string().nullable()
14411
+ artifact: z20.object({
14412
+ htmlBlobUrl: z20.string().url().nullable(),
14413
+ textBlobUrl: z20.string().url().nullable(),
14414
+ markdownBlobUrl: z20.string().url().nullable(),
14415
+ screenshotBlobUrl: z20.string().url().nullable(),
14416
+ contentSha256: z20.string().nullable(),
14417
+ capturedAt: z20.string().nullable()
14059
14418
  }).strict(),
14060
- error: z19.object({
14061
- code: z19.string(),
14062
- message: z19.string()
14419
+ error: z20.object({
14420
+ code: z20.string(),
14421
+ message: z20.string()
14063
14422
  }).strict().nullable()
14064
14423
  }).strict();
14065
- var SerpIntelligenceCaptureResponseSchema = z19.object({
14424
+ var SerpIntelligenceCaptureResponseSchema = z20.object({
14066
14425
  harvestResult: SerpIntelligenceHarvestResultSchema,
14067
- attempts: z19.array(SerpIntelligenceCaptureAttemptSchema),
14426
+ attempts: z20.array(SerpIntelligenceCaptureAttemptSchema),
14068
14427
  locationEvidence: SerpIntelligenceLocationEvidenceSchema.nullable(),
14069
- pageSnapshotArtifacts: z19.array(SerpPageSnapshotCaptureSchema),
14070
- billing: z19.object({
14071
- creditsUsed: z19.number().min(0).optional(),
14072
- requestId: z19.string().optional(),
14073
- jobId: z19.string().optional()
14428
+ pageSnapshotArtifacts: z20.array(SerpPageSnapshotCaptureSchema),
14429
+ billing: z20.object({
14430
+ creditsUsed: z20.number().min(0).optional(),
14431
+ requestId: z20.string().optional(),
14432
+ jobId: z20.string().optional()
14074
14433
  }).strict().optional()
14075
14434
  }).strict();
14076
- var SerpIntelligencePageSnapshotsResponseSchema = z19.object({
14077
- pageSnapshotArtifacts: z19.array(SerpPageSnapshotCaptureSchema),
14078
- attempts: z19.array(SerpIntelligenceCaptureAttemptSchema).default([])
14435
+ var SerpIntelligencePageSnapshotsResponseSchema = z20.object({
14436
+ pageSnapshotArtifacts: z20.array(SerpPageSnapshotCaptureSchema),
14437
+ attempts: z20.array(SerpIntelligenceCaptureAttemptSchema).default([])
14079
14438
  }).strict();
14080
14439
 
14081
14440
  // src/serp-intelligence/serp-capture-service.ts
@@ -14247,7 +14606,7 @@ var SERP_INTELLIGENCE_RATE_LIMIT = 60;
14247
14606
  var SERP_INTELLIGENCE_RATE_WINDOW_SECONDS = 60;
14248
14607
  var POST_CAPTURE_ROUTE_LABEL = "POST /capture";
14249
14608
  var POST_PAGE_SNAPSHOTS_ROUTE_LABEL = "POST /page-snapshots";
14250
- var serpIntelligenceApp = new Hono10();
14609
+ var serpIntelligenceApp = new Hono11();
14251
14610
  serpIntelligenceApp.use("*", createApiKeyAuth());
14252
14611
  function structuredError(input) {
14253
14612
  return {
@@ -14416,7 +14775,7 @@ serpIntelligenceApp.post("/page-snapshots", async (c) => {
14416
14775
  });
14417
14776
 
14418
14777
  // src/mcp/mcp-routes.ts
14419
- import { Hono as Hono11 } from "hono";
14778
+ import { Hono as Hono12 } from "hono";
14420
14779
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
14421
14780
 
14422
14781
  // src/mcp/mcp-oauth.ts
@@ -14500,23 +14859,7 @@ async function requireMcpCallerKey(c) {
14500
14859
  if (!user) return mcpAuthError();
14501
14860
  return callerKey;
14502
14861
  }
14503
- var mcpApp = new Hono11();
14504
- function registerSerpIntelligenceCaptureTools(server, executor) {
14505
- server.registerTool("capture_serp_snapshot", {
14506
- title: "SERP Intelligence Snapshot",
14507
- description: "Capture a structured SERP Intelligence Google snapshot through POST /serp-intelligence/capture, the same product capture path used by Phoenix. Split query from location and infer gl/hl. Routing is automatic \u2014 leave proxyMode unset. Set debug true when investigating location evidence, proxy behavior, CAPTCHA, or capture reliability.",
14508
- inputSchema: CaptureSerpSnapshotInputSchema,
14509
- outputSchema: CaptureSerpSnapshotOutputSchema,
14510
- annotations: liveWebToolAnnotations("SERP Intelligence Snapshot")
14511
- }, async (input) => formatCaptureSerpSnapshot(await executor.captureSerpSnapshot(input), input));
14512
- server.registerTool("capture_serp_page_snapshots", {
14513
- title: "SERP Intelligence Page Snapshots",
14514
- description: "Capture public ranking-page evidence through POST /serp-intelligence/page-snapshots, the same product page snapshot path used by Phoenix. Provide urls for simple captures or targets when preserving organic, AI citation, local-pack, configured target, or site-subject source metadata. Private IPs, localhost, file URLs, and internal URLs are rejected by the service. Use timeoutMs for slow pages and debug true for sanitized proxy/browser diagnostics.",
14515
- inputSchema: CaptureSerpPageSnapshotsInputSchema,
14516
- outputSchema: CaptureSerpPageSnapshotsOutputSchema,
14517
- annotations: liveWebToolAnnotations("SERP Intelligence Page Snapshots")
14518
- }, async (input) => formatCaptureSerpPageSnapshots(await executor.captureSerpPageSnapshots(input), input));
14519
- }
14862
+ var mcpApp = new Hono12();
14520
14863
  mcpApp.all("/", async (c) => {
14521
14864
  try {
14522
14865
  const keyOrError = await requireMcpCallerKey(c);
@@ -14540,7 +14883,7 @@ mcpApp.all("/", async (c) => {
14540
14883
  });
14541
14884
 
14542
14885
  // src/api/browser-agent-routes.ts
14543
- import { Hono as Hono12 } from "hono";
14886
+ import { Hono as Hono13 } from "hono";
14544
14887
 
14545
14888
  // src/api/browser-agent-db.ts
14546
14889
  import { randomUUID as randomUUID2 } from "crypto";
@@ -15974,7 +16317,7 @@ async function loadOpenSession(id, userId) {
15974
16317
  return row;
15975
16318
  }
15976
16319
  function buildBrowserAgentRoutes() {
15977
- const app2 = new Hono12();
16320
+ const app2 = new Hono13();
15978
16321
  app2.use("*", async (c, next) => {
15979
16322
  await migrateBrowserAgent();
15980
16323
  return next();
@@ -16680,7 +17023,7 @@ if (state.key) { refreshSessions(); if (state.current) selectSession(state.curre
16680
17023
 
16681
17024
  // src/api/stripe-routes.ts
16682
17025
  import Stripe from "stripe";
16683
- import { Hono as Hono13 } from "hono";
17026
+ import { Hono as Hono14 } from "hono";
16684
17027
 
16685
17028
  // src/api/memory.ts
16686
17029
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
@@ -16819,10 +17162,22 @@ async function syncMemoryKeyPlan(user, plan) {
16819
17162
  const res = await memoryCall("setScopeTool", { keyId: target.keyId, plan: effectivePlan }, key);
16820
17163
  return { ok: res.ok, error: res.error };
16821
17164
  }
17165
+ async function syncScheduleEntitlement(user, enabled, quotaPerPeriod) {
17166
+ const admin = ADMIN_KEY();
17167
+ if (!admin) return { ok: false, error: "memory not configured (MCP_MEMORY_ADMIN_KEY missing)" };
17168
+ const identity = resolveMemoryIdentity(user);
17169
+ const res = await memoryCall("setScheduleEntitlementTool", {
17170
+ granteeIdentity: identity,
17171
+ enabled,
17172
+ quotaPerPeriod,
17173
+ mcpScraperApiKey: user.api_key
17174
+ }, admin);
17175
+ return { ok: res.ok, error: res.error };
17176
+ }
16822
17177
 
16823
17178
  // src/api/stripe-routes.ts
16824
17179
  var stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
16825
- var stripeApp = new Hono13();
17180
+ var stripeApp = new Hono14();
16826
17181
  stripeApp.post("/webhooks", async (c) => {
16827
17182
  const sig = c.req.header("stripe-signature");
16828
17183
  const body = await c.req.text();
@@ -16861,6 +17216,15 @@ stripeApp.post("/webhooks", async (c) => {
16861
17216
  }
16862
17217
  return c.json({ received: true });
16863
17218
  }
17219
+ if (priceId && priceId in SCHEDULING_PLANS) {
17220
+ const sched = SCHEDULING_PLANS[priceId];
17221
+ const user2 = await resolveUser(sub.customer, sub.customer_email);
17222
+ if (user2) {
17223
+ const live = sub.status === "active" || sub.status === "trialing";
17224
+ await syncScheduleEntitlement(user2, live, sched.quota_per_period);
17225
+ }
17226
+ return c.json({ received: true });
17227
+ }
16864
17228
  const user = await getUserByStripeCustomerId(sub.customer);
16865
17229
  if (!user) return c.json({ received: true });
16866
17230
  if (priceId && priceId in SUBSCRIPTION_TIERS) {
@@ -16883,6 +17247,11 @@ stripeApp.post("/webhooks", async (c) => {
16883
17247
  }
16884
17248
  return c.json({ received: true });
16885
17249
  }
17250
+ if (priceId && priceId in SCHEDULING_PLANS) {
17251
+ const user2 = await getUserByStripeCustomerId(sub.customer);
17252
+ if (user2) await syncScheduleEntitlement(user2, false);
17253
+ return c.json({ received: true });
17254
+ }
16886
17255
  const user = await getUserByStripeCustomerId(sub.customer);
16887
17256
  if (!user) return c.json({ received: true });
16888
17257
  if (priceId && priceId in SUBSCRIPTION_TIERS) {
@@ -16908,7 +17277,7 @@ async function resolveUser(customerId, emailFallback) {
16908
17277
  }
16909
17278
 
16910
17279
  // src/api/oauth-routes.ts
16911
- import { Hono as Hono14 } from "hono";
17280
+ import { Hono as Hono15 } from "hono";
16912
17281
  import { getCookie, setCookie } from "hono/cookie";
16913
17282
  import { createHash as createHash3, randomBytes as randomBytes2, randomUUID as randomUUID3 } from "crypto";
16914
17283
  import { importPKCS8, exportJWK, SignJWT } from "jose";
@@ -16960,6 +17329,9 @@ function isAllowedRedirect(uri) {
16960
17329
  const h = u.hostname.toLowerCase();
16961
17330
  if (["claude.ai", "www.claude.ai", "claude.com", "www.claude.com"].includes(h)) return true;
16962
17331
  if (h.endsWith(".claude.ai") || h.endsWith(".claude.com")) return true;
17332
+ if (["chatgpt.com", "www.chatgpt.com", "chat.openai.com", "www.chat.openai.com"].includes(h)) return true;
17333
+ if (h.endsWith(".chatgpt.com") || h.endsWith(".openai.com")) return true;
17334
+ if (h === "viktor.com" || h.endsWith(".viktor.com")) return true;
16963
17335
  const extra = (process.env.OAUTH_EXTRA_REDIRECT_HOSTS ?? "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
16964
17336
  return extra.includes(h);
16965
17337
  }
@@ -17072,7 +17444,7 @@ function redirectWithError(redirectUri, state, error) {
17072
17444
  if (state) u.searchParams.set("state", state);
17073
17445
  return new Response(null, { status: 302, headers: { Location: u.toString() } });
17074
17446
  }
17075
- var oauthApp = new Hono14();
17447
+ var oauthApp = new Hono15();
17076
17448
  oauthApp.use("*", async (c, next) => {
17077
17449
  c.header("Access-Control-Allow-Origin", "*");
17078
17450
  c.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
@@ -17080,42 +17452,51 @@ oauthApp.use("*", async (c, next) => {
17080
17452
  if (c.req.method === "OPTIONS") return c.body(null, 204);
17081
17453
  return next();
17082
17454
  });
17083
- function authServerMetadata() {
17455
+ function authServerMetadata(scopes = SUPPORTED_SCOPES) {
17084
17456
  return {
17085
17457
  issuer: ISSUER,
17086
17458
  authorization_endpoint: `${ISSUER}/oauth/authorize`,
17087
17459
  token_endpoint: `${ISSUER}/oauth/token`,
17088
17460
  registration_endpoint: `${ISSUER}/oauth/register`,
17089
17461
  jwks_uri: `${ISSUER}/.well-known/jwks.json`,
17090
- scopes_supported: [...SUPPORTED_SCOPES],
17462
+ scopes_supported: [...scopes],
17091
17463
  response_types_supported: ["code"],
17092
17464
  grant_types_supported: ["authorization_code", "refresh_token"],
17093
17465
  code_challenge_methods_supported: ["S256"],
17094
17466
  token_endpoint_auth_methods_supported: ["none"]
17095
17467
  };
17096
17468
  }
17097
- function openidConfiguration() {
17469
+ function openidConfiguration(scopes = SUPPORTED_SCOPES) {
17098
17470
  return {
17099
- ...authServerMetadata(),
17471
+ ...authServerMetadata(scopes),
17100
17472
  subject_types_supported: ["public"],
17101
17473
  id_token_signing_alg_values_supported: ["RS256"]
17102
17474
  };
17103
17475
  }
17476
+ function scopesForRequest(c) {
17477
+ const host = (c.req.header("host") ?? "").toLowerCase();
17478
+ if (!host) return SUPPORTED_SCOPES;
17479
+ const suffix2 = c.req.path.replace(/^\/\.well-known\/(oauth-authorization-server|openid-configuration)/, "");
17480
+ const candidate = `https://${host}${suffix2}`.replace(/\/$/, "");
17481
+ if (candidate === SCRAPER_RESOURCE()) return SCRAPER_SCOPES;
17482
+ if (candidate === RESOURCE().replace(/\/$/, "")) return MEMORY_SCOPES;
17483
+ return SUPPORTED_SCOPES;
17484
+ }
17104
17485
  oauthApp.get("/.well-known/oauth-authorization-server", (c) => {
17105
17486
  console.log("[oauth-dcr] GET /.well-known/oauth-authorization-server ua=%s", c.req.header("user-agent") ?? "");
17106
- return c.json(authServerMetadata());
17487
+ return c.json(authServerMetadata(scopesForRequest(c)));
17107
17488
  });
17108
17489
  oauthApp.get("/.well-known/oauth-authorization-server/*", (c) => {
17109
17490
  console.log("[oauth-dcr] GET %s (path-aware AS) ua=%s", c.req.path, c.req.header("user-agent") ?? "");
17110
- return c.json(authServerMetadata());
17491
+ return c.json(authServerMetadata(scopesForRequest(c)));
17111
17492
  });
17112
17493
  oauthApp.get("/.well-known/openid-configuration", (c) => {
17113
17494
  console.log("[oauth-dcr] GET /.well-known/openid-configuration ua=%s", c.req.header("user-agent") ?? "");
17114
- return c.json(openidConfiguration());
17495
+ return c.json(openidConfiguration(scopesForRequest(c)));
17115
17496
  });
17116
17497
  oauthApp.get("/.well-known/openid-configuration/*", (c) => {
17117
17498
  console.log("[oauth-dcr] GET %s (path-aware OIDC) ua=%s", c.req.path, c.req.header("user-agent") ?? "");
17118
- return c.json(openidConfiguration());
17499
+ return c.json(openidConfiguration(scopesForRequest(c)));
17119
17500
  });
17120
17501
  oauthApp.get("/.well-known/jwks.json", async (c) => {
17121
17502
  const { publicJwk } = await getKeys();
@@ -17319,8 +17700,8 @@ oauthApp.post("/oauth/token", async (c) => {
17319
17700
  });
17320
17701
 
17321
17702
  // src/api/chat-routes.ts
17322
- import { Hono as Hono15 } from "hono";
17323
- var chatApp = new Hono15();
17703
+ import { Hono as Hono16 } from "hono";
17704
+ var chatApp = new Hono16();
17324
17705
  function tokenOk(token) {
17325
17706
  return !!token && token.startsWith("mk_") && token.length > 10;
17326
17707
  }
@@ -17989,6 +18370,331 @@ loadChannels();
17989
18370
  </html>`;
17990
18371
  }
17991
18372
 
18373
+ // src/api/schedule-routes.ts
18374
+ import { Hono as Hono17 } from "hono";
18375
+ var scheduleApp = new Hono17();
18376
+ function tokenOk2(token) {
18377
+ return !!token && token.startsWith("mk_") && token.length > 10;
18378
+ }
18379
+ function jsonError2(message, status = 400) {
18380
+ return new Response(JSON.stringify({ ok: false, error: message }), { status, headers: { "content-type": "application/json" } });
18381
+ }
18382
+ scheduleApp.get("/api/status", async (c) => {
18383
+ const token = c.req.query("token");
18384
+ if (!tokenOk2(token)) return jsonError2("missing or malformed token", 401);
18385
+ const r = await memoryCall("getScheduleStatusTool", {}, token);
18386
+ return c.json(r);
18387
+ });
18388
+ scheduleApp.get("/api/vaults", async (c) => {
18389
+ const token = c.req.query("token");
18390
+ if (!tokenOk2(token)) return jsonError2("missing or malformed token", 401);
18391
+ const r = await memoryCall("listVaultsTool", {}, token);
18392
+ if (!r.ok) return c.json(r);
18393
+ const vaults = (r.vaults ?? []).filter((v) => v.kind !== "channel");
18394
+ return c.json({ ok: true, vaults });
18395
+ });
18396
+ scheduleApp.get("/api/actions", async (c) => {
18397
+ const token = c.req.query("token");
18398
+ if (!tokenOk2(token)) return jsonError2("missing or malformed token", 401);
18399
+ const r = await memoryCall("listScheduledActionsTool", {}, token);
18400
+ return c.json(r);
18401
+ });
18402
+ scheduleApp.post("/api/propose", async (c) => {
18403
+ const body = await c.req.json().catch(() => ({}));
18404
+ if (!tokenOk2(body.token)) return jsonError2("missing or malformed token", 401);
18405
+ if (!body.request) return jsonError2("request is required");
18406
+ const r = await memoryCall("proposeScheduledActionTool", { request: body.request }, body.token);
18407
+ return c.json(r);
18408
+ });
18409
+ scheduleApp.post("/api/actions", async (c) => {
18410
+ const body = await c.req.json().catch(() => ({}));
18411
+ if (!tokenOk2(body.token)) return jsonError2("missing or malformed token", 401);
18412
+ if (!body.description || !body.vault || !body.cadence) return jsonError2("description, vault and cadence are required");
18413
+ const r = await memoryCall("createScheduledActionTool", { description: body.description, vault: body.vault, cadence: body.cadence }, body.token);
18414
+ return c.json(r);
18415
+ });
18416
+ scheduleApp.post("/api/actions/:id/pause", async (c) => {
18417
+ const body = await c.req.json().catch(() => ({}));
18418
+ if (!tokenOk2(body.token)) return jsonError2("missing or malformed token", 401);
18419
+ const r = await memoryCall("pauseScheduledActionTool", { id: c.req.param("id") }, body.token);
18420
+ return c.json(r);
18421
+ });
18422
+ scheduleApp.post("/api/actions/:id/resume", async (c) => {
18423
+ const body = await c.req.json().catch(() => ({}));
18424
+ if (!tokenOk2(body.token)) return jsonError2("missing or malformed token", 401);
18425
+ const r = await memoryCall("resumeScheduledActionTool", { id: c.req.param("id") }, body.token);
18426
+ return c.json(r);
18427
+ });
18428
+ scheduleApp.post("/api/actions/:id/delete", async (c) => {
18429
+ const body = await c.req.json().catch(() => ({}));
18430
+ if (!tokenOk2(body.token)) return jsonError2("missing or malformed token", 401);
18431
+ const r = await memoryCall("deleteScheduledActionTool", { id: c.req.param("id") }, body.token);
18432
+ return c.json(r);
18433
+ });
18434
+ scheduleApp.get("/:token", (c) => {
18435
+ const token = c.req.param("token");
18436
+ if (!tokenOk2(token)) {
18437
+ return c.html(`<!doctype html><html><body style="font-family:system-ui;padding:40px;color:#3D3548"><h2>This automations link isn't valid.</h2><p>Ask the AI to run get-schedule-link again, or revoke-schedule-link then get-schedule-link if you think it was compromised.</p></body></html>`, 401);
18438
+ }
18439
+ return c.html(renderSchedulePage(token));
18440
+ });
18441
+ function renderSchedulePage(token) {
18442
+ return `<!doctype html>
18443
+ <html>
18444
+ <head>
18445
+ <meta charset="utf-8">
18446
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
18447
+ <title>Scheduled Actions</title>
18448
+ <style>
18449
+ :root { color-scheme: light; --accent: #d06045; --warn: #b3261e; --bg: #faf8f6; --card: #fff; --line: #e8e1db; --ink: #211a14; --muted: #7a6f64; } html, body, div, span { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
18450
+ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; color: var(--ink); background: var(--bg); padding-bottom: 90px; }
18451
+ header { position: sticky; top: 0; background: var(--bg); padding: 18px 16px 12px; border-bottom: 1px solid var(--line); z-index: 10; }
18452
+ h1 { font-size: 21px; font-weight: 800; margin: 0 0 4px; letter-spacing: -.01em; }
18453
+ .quota { font-size: 13px; color: var(--muted); transition: color .2s; }
18454
+ .quota.warn { color: var(--warn); font-weight: 600; }
18455
+ .quota-bar { height: 6px; border-radius: 999px; background: var(--line); margin-top: 6px; overflow: hidden; }
18456
+ .quota-bar-fill { height: 100%; background: var(--accent); transition: width .3s ease, background .2s; }
18457
+ .quota-bar-fill.warn { background: var(--warn); }
18458
+ main { padding: 14px; max-width: 640px; margin: 0 auto; }
18459
+ .card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 16px; margin-bottom: 12px; animation: rise .25s ease both; }
18460
+ @keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
18461
+ .empty { text-align: center; color: var(--muted); padding: 56px 20px; font-size: 14px; }
18462
+ .empty-icon { font-size: 32px; margin-bottom: 10px; opacity: .7; }
18463
+ .action-desc { font-size: 15px; line-height: 1.45; margin-bottom: 6px; }
18464
+ .action-meta { font-size: 12.5px; color: var(--muted); display: flex; flex-wrap: wrap; gap: 6px 10px; }
18465
+ .pill { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11.5px; font-weight: 600; }
18466
+ .pill-active { background: #e3f3ea; color: #1a7a4c; }
18467
+ .pill-paused { background: #f3ecd8; color: #93701a; }
18468
+ .action-row { display: flex; gap: 8px; margin-top: 12px; }
18469
+ button { font: inherit; }
18470
+ .btn { flex: 1; padding: 10px 12px; border-radius: 9px; border: 1px solid var(--line); background: #fff; color: var(--ink); font-size: 13.5px; font-weight: 600; cursor: pointer; transition: transform .08s ease, background .15s, color .15s, border-color .15s; }
18471
+ .btn:active { transform: scale(.96); }
18472
+ .btn-accent { background: var(--accent); border-color: var(--accent); color: #fff; }
18473
+ .btn-danger { color: #b3261e; }
18474
+ .btn-danger.confirming { background: var(--warn); border-color: var(--warn); color: #fff; }
18475
+ .fab { position: fixed; bottom: 20px; left: 16px; right: 16px; max-width: 608px; margin: 0 auto; background: var(--accent); color: #fff; border: none; border-radius: 14px; padding: 16px; font-size: 15.5px; font-weight: 700; cursor: pointer; box-shadow: 0 8px 24px rgba(208,96,69,.35); transition: transform .1s ease, box-shadow .15s; }
18476
+ .fab:active { transform: scale(.98); box-shadow: 0 4px 14px rgba(208,96,69,.35); }
18477
+ .overlay { position: fixed; inset: 0; background: rgba(33,26,20,.45); display: flex; align-items: flex-end; justify-content: center; z-index: 50; opacity: 0; pointer-events: none; transition: opacity .2s ease; }
18478
+ .overlay.open { opacity: 1; pointer-events: auto; }
18479
+ .sheet { width: 100%; max-width: 640px; background: #fff; border-radius: 18px 18px 0 0; max-height: 88vh; overflow: auto; padding: 18px 16px 24px; transform: translateY(28px); transition: transform .28s cubic-bezier(.16,1,.3,1); }
18480
+ .overlay.open .sheet { transform: translateY(0); }
18481
+ .sheet h2 { font-size: 17px; margin: 0 0 14px; }
18482
+ textarea, input, select { width: 100%; box-sizing: border-box; border: 1px solid var(--line); border-radius: 10px; padding: 12px 13px; font-size: 15px; outline: none; margin-top: 6px; font-family: inherit; transition: border-color .15s; }
18483
+ textarea:focus, input:focus, select:focus { border-color: var(--accent); }
18484
+ textarea.autogrow { resize: none; overflow: hidden; line-height: 1.5; min-height: 90px; transition: height .12s ease; }
18485
+ label { font-size: 12.5px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .03em; display: block; margin-top: 14px; }
18486
+ .rationale { font-size: 13px; color: var(--muted); background: #f5f0ea; border-radius: 9px; padding: 10px 12px; margin-top: 10px; }
18487
+ .sheet-actions { display: flex; gap: 8px; margin-top: 18px; }
18488
+ .err { color: #b3261e; font-size: 13px; margin-top: 8px; display: none; }
18489
+ .spin { opacity: .55; pointer-events: none; }
18490
+ .toast { position: fixed; bottom: 96px; left: 50%; background: var(--ink); color: #fff; padding: 10px 18px; border-radius: 999px; font-size: 13.5px; font-weight: 600; z-index: 60; opacity: 0; pointer-events: none; transform: translate(-50%, 8px); transition: opacity .2s ease, transform .2s ease; }
18491
+ .toast.show { opacity: 1; transform: translate(-50%, 0); }
18492
+ </style>
18493
+ </head>
18494
+ <body>
18495
+ <header>
18496
+ <h1>Scheduled Actions</h1>
18497
+ <div class="quota" id="quota-text">Loading\u2026</div>
18498
+ <div class="quota-bar"><div class="quota-bar-fill" id="quota-fill" style="width:0%"></div></div>
18499
+ </header>
18500
+ <main id="list"></main>
18501
+ <button class="fab" id="new-btn">+ New scheduled action</button>
18502
+ <div class="toast" id="toast"></div>
18503
+
18504
+ <div class="overlay" id="propose-overlay">
18505
+ <div class="sheet">
18506
+ <h2>What should this do?</h2>
18507
+ <textarea id="propose-input" class="autogrow" rows="3" placeholder="e.g. check my competitor's pricing page every Monday and summarize what changed"></textarea>
18508
+ <div class="err" id="propose-err"></div>
18509
+ <div class="sheet-actions">
18510
+ <button class="btn" id="propose-cancel">Cancel</button>
18511
+ <button class="btn btn-accent" id="propose-submit">Propose</button>
18512
+ </div>
18513
+ </div>
18514
+ </div>
18515
+
18516
+ <div class="overlay" id="confirm-overlay">
18517
+ <div class="sheet">
18518
+ <h2>Review before confirming</h2>
18519
+ <label>Description</label>
18520
+ <textarea id="confirm-description" class="autogrow" rows="8"></textarea>
18521
+ <label>Vault</label>
18522
+ <select id="confirm-vault"></select>
18523
+ <label>Cadence</label>
18524
+ <select id="confirm-cadence">
18525
+ <option value="daily">Daily</option>
18526
+ <option value="weekly">Weekly</option>
18527
+ <option value="monthly">Monthly</option>
18528
+ </select>
18529
+ <div class="rationale" id="confirm-rationale"></div>
18530
+ <div class="err" id="confirm-err"></div>
18531
+ <div class="sheet-actions">
18532
+ <button class="btn" id="confirm-cancel">Cancel</button>
18533
+ <button class="btn btn-accent" id="confirm-submit">Confirm & create</button>
18534
+ </div>
18535
+ </div>
18536
+ </div>
18537
+
18538
+ <script>
18539
+ const TOKEN = ${JSON.stringify(token)};
18540
+ const state = { actions: [], vaults: [] };
18541
+
18542
+ function api(path, opts) { return fetch('/schedule' + path, opts).then(r => r.json()); }
18543
+ function get(path, params) { const qs = new URLSearchParams({ token: TOKEN, ...(params||{}) }).toString(); return api(path + '?' + qs); }
18544
+ function post(path, body) { return api(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ token: TOKEN, ...(body||{}) }) }); }
18545
+ function esc(s) { return (s || '').replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c])); }
18546
+ function fmt(iso) { try { return new Date(iso).toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } catch (e) { return iso || ''; } }
18547
+
18548
+ function autogrow(el) { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; }
18549
+ document.querySelectorAll('.autogrow').forEach(el => {
18550
+ el.addEventListener('input', () => autogrow(el));
18551
+ });
18552
+
18553
+ let toastTimer = null;
18554
+ function toast(message) {
18555
+ const el = document.getElementById('toast');
18556
+ el.textContent = message;
18557
+ el.classList.add('show');
18558
+ if (toastTimer) clearTimeout(toastTimer);
18559
+ toastTimer = setTimeout(() => el.classList.remove('show'), 2200);
18560
+ }
18561
+
18562
+ async function loadStatus() {
18563
+ const r = await get('/api/status');
18564
+ const text = document.getElementById('quota-text');
18565
+ const fill = document.getElementById('quota-fill');
18566
+ if (!r.ok || !r.enabled) {
18567
+ text.textContent = 'No active Scheduled Actions subscription.';
18568
+ text.classList.remove('warn');
18569
+ fill.style.width = '0%';
18570
+ fill.classList.remove('warn');
18571
+ return;
18572
+ }
18573
+ const pct = Math.min(100, Math.round((r.usedThisPeriod / Math.max(1, r.quotaPerPeriod)) * 100));
18574
+ const nearLimit = pct >= 90;
18575
+ text.textContent = r.usedThisPeriod + ' / ' + r.quotaPerPeriod + ' actions used this period' + (nearLimit ? ' \u2014 almost at your limit' : '');
18576
+ text.classList.toggle('warn', nearLimit);
18577
+ fill.style.width = pct + '%';
18578
+ fill.classList.toggle('warn', nearLimit);
18579
+ }
18580
+
18581
+ async function loadVaults() {
18582
+ const r = await get('/api/vaults');
18583
+ state.vaults = r.ok ? (r.vaults || []) : [];
18584
+ const sel = document.getElementById('confirm-vault');
18585
+ sel.innerHTML = state.vaults.map(v => '<option value="' + esc(v.handle) + '">' + esc(v.handle) + '</option>').join('');
18586
+ }
18587
+
18588
+ async function loadActions() {
18589
+ const r = await get('/api/actions');
18590
+ const box = document.getElementById('list');
18591
+ if (!r.ok) { box.innerHTML = '<div class="empty">' + esc(r.error || 'Could not load') + '</div>'; return; }
18592
+ state.actions = r.actions || [];
18593
+ if (!state.actions.length) {
18594
+ box.innerHTML = '<div class="empty"><div class="empty-icon">\u23F1\uFE0F</div>No scheduled actions yet.<br>Tap "New scheduled action" to create your first one.</div>';
18595
+ return;
18596
+ }
18597
+ box.innerHTML = state.actions.map(renderAction).join('');
18598
+ state.actions.forEach(a => {
18599
+ const pauseBtn = document.getElementById('pause-' + a.id);
18600
+ const resumeBtn = document.getElementById('resume-' + a.id);
18601
+ const delBtn = document.getElementById('delete-' + a.id);
18602
+ if (pauseBtn) pauseBtn.onclick = () => act('pause', a.id, 'Paused');
18603
+ if (resumeBtn) resumeBtn.onclick = () => act('resume', a.id, 'Resumed');
18604
+ if (delBtn) delBtn.onclick = () => confirmDelete(delBtn, a.id);
18605
+ });
18606
+ }
18607
+
18608
+ function confirmDelete(btn, id) {
18609
+ if (!btn.classList.contains('confirming')) {
18610
+ btn.classList.add('confirming');
18611
+ btn.textContent = 'Tap again to delete';
18612
+ btn.dataset.confirmTimer = setTimeout(() => {
18613
+ btn.classList.remove('confirming');
18614
+ btn.textContent = 'Delete';
18615
+ }, 3000);
18616
+ return;
18617
+ }
18618
+ clearTimeout(btn.dataset.confirmTimer);
18619
+ act('delete', id, 'Deleted');
18620
+ }
18621
+
18622
+ function renderAction(a) {
18623
+ const pill = a.status === 'active' ? '<span class="pill pill-active">active</span>' : '<span class="pill pill-paused">' + esc(a.status) + '</span>';
18624
+ return '<div class="card">' +
18625
+ '<div class="action-desc">' + esc(a.description) + '</div>' +
18626
+ '<div class="action-meta">' + pill + '<span>' + esc(a.cadence) + '</span><span>next ' + fmt(a.nextRunAt) + '</span>' +
18627
+ (a.lastRunStatus ? '<span>last: ' + esc(a.lastRunStatus) + '</span>' : '') + '</div>' +
18628
+ '<div class="action-row">' +
18629
+ (a.status === 'active'
18630
+ ? '<button class="btn" id="pause-' + a.id + '">Pause</button>'
18631
+ : '<button class="btn" id="resume-' + a.id + '">Resume</button>') +
18632
+ '<button class="btn btn-danger" id="delete-' + a.id + '">Delete</button>' +
18633
+ '</div></div>';
18634
+ }
18635
+
18636
+ async function act(kind, id, toastMessage) {
18637
+ await post('/api/actions/' + id + '/' + kind, {});
18638
+ await loadActions();
18639
+ await loadStatus();
18640
+ if (toastMessage) toast(toastMessage);
18641
+ }
18642
+
18643
+ document.getElementById('new-btn').onclick = () => {
18644
+ document.getElementById('propose-input').value = '';
18645
+ document.getElementById('propose-err').style.display = 'none';
18646
+ document.getElementById('propose-overlay').classList.add('open');
18647
+ };
18648
+ document.getElementById('propose-cancel').onclick = () => document.getElementById('propose-overlay').classList.remove('open');
18649
+
18650
+ document.getElementById('propose-submit').onclick = async () => {
18651
+ const request = document.getElementById('propose-input').value.trim();
18652
+ const errEl = document.getElementById('propose-err');
18653
+ errEl.style.display = 'none';
18654
+ if (!request) return;
18655
+ const btn = document.getElementById('propose-submit');
18656
+ btn.classList.add('spin'); btn.textContent = 'Thinking\u2026';
18657
+ const r = await post('/api/propose', { request });
18658
+ btn.classList.remove('spin'); btn.textContent = 'Propose';
18659
+ if (!r.ok) { errEl.textContent = r.error || 'Could not interpret that request'; errEl.style.display = 'block'; return; }
18660
+ document.getElementById('propose-overlay').classList.remove('open');
18661
+ document.getElementById('confirm-description').value = r.description || '';
18662
+ document.getElementById('confirm-vault').value = r.vault || '';
18663
+ document.getElementById('confirm-cadence').value = r.cadence || 'daily';
18664
+ document.getElementById('confirm-rationale').textContent = r.rationale || '';
18665
+ document.getElementById('confirm-err').style.display = 'none';
18666
+ document.getElementById('confirm-overlay').classList.add('open');
18667
+ autogrow(document.getElementById('confirm-description'));
18668
+ };
18669
+
18670
+ document.getElementById('confirm-cancel').onclick = () => document.getElementById('confirm-overlay').classList.remove('open');
18671
+
18672
+ document.getElementById('confirm-submit').onclick = async () => {
18673
+ const description = document.getElementById('confirm-description').value.trim();
18674
+ const vault = document.getElementById('confirm-vault').value;
18675
+ const cadence = document.getElementById('confirm-cadence').value;
18676
+ const errEl = document.getElementById('confirm-err');
18677
+ errEl.style.display = 'none';
18678
+ if (!description || !vault) { errEl.textContent = 'Description and vault are required'; errEl.style.display = 'block'; return; }
18679
+ const btn = document.getElementById('confirm-submit');
18680
+ btn.classList.add('spin'); btn.textContent = 'Creating\u2026';
18681
+ const r = await post('/api/actions', { description, vault, cadence });
18682
+ btn.classList.remove('spin'); btn.textContent = 'Confirm & create';
18683
+ if (!r.ok) { errEl.textContent = r.error || 'Could not create scheduled action'; errEl.style.display = 'block'; return; }
18684
+ document.getElementById('confirm-overlay').classList.remove('open');
18685
+ await loadActions();
18686
+ await loadStatus();
18687
+ toast('Scheduled action created');
18688
+ };
18689
+
18690
+ loadStatus();
18691
+ loadVaults();
18692
+ loadActions();
18693
+ </script>
18694
+ </body>
18695
+ </html>`;
18696
+ }
18697
+
17992
18698
  // src/api/site-audit-worker.ts
17993
18699
  var MAX_CONCURRENT_SITE_AUDIT = 1;
17994
18700
  async function drainSiteAuditQueue(budget) {
@@ -18194,13 +18900,14 @@ function toGb(bytes) {
18194
18900
  }
18195
18901
  async function entitledVaultRows(identity) {
18196
18902
  const rows = await sql().query(
18197
- `SELECT vault, owner_identity FROM mem_vault_registry WHERE grantee = $1 ORDER BY created_at ASC`,
18903
+ `SELECT vault, owner_identity, contract FROM mem_vault_registry WHERE grantee = $1 ORDER BY created_at ASC`,
18198
18904
  [identity]
18199
18905
  );
18200
18906
  const seen = /* @__PURE__ */ new Map();
18201
18907
  for (const r of rows) {
18202
18908
  if (seen.has(r.vault)) continue;
18203
- seen.set(r.vault, { vault: r.vault, owner: r.owner_identity, physical: physicalVault(r.owner_identity, r.vault) });
18909
+ const kind = r.contract?.kind === "channel" ? "channel" : "notes";
18910
+ seen.set(r.vault, { vault: r.vault, owner: r.owner_identity, physical: physicalVault(r.owner_identity, r.vault), kind });
18204
18911
  }
18205
18912
  return [...seen.values()];
18206
18913
  }
@@ -18226,7 +18933,8 @@ async function dbListVaults(identity) {
18226
18933
  role: isOwner ? "owner" : "shared",
18227
18934
  ...isOwner ? {} : { sharedBy: e.owner },
18228
18935
  notes: st?.notes ?? 0,
18229
- bytes: st?.bytes ?? 0
18936
+ bytes: st?.bytes ?? 0,
18937
+ kind: e.kind
18230
18938
  };
18231
18939
  });
18232
18940
  }
@@ -18403,7 +19111,7 @@ var sessionAuth = createMiddleware3(async (c, next) => {
18403
19111
  c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
18404
19112
  return next();
18405
19113
  });
18406
- var app = new Hono16();
19114
+ var app = new Hono18();
18407
19115
  var STRIPE_API_VERSION = "2026-02-25.clover";
18408
19116
  function requireStripeSecret() {
18409
19117
  const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
@@ -18594,6 +19302,7 @@ app.get("/me", async (c) => {
18594
19302
  const stats = await getUserStats(user.id);
18595
19303
  return c.json({
18596
19304
  authenticated: true,
19305
+ id: user.id,
18597
19306
  email: user.email,
18598
19307
  api_key: user.key_active ? user.api_key : null,
18599
19308
  created_at: user.created_at,
@@ -18668,6 +19377,28 @@ app.get("/memory/vaults", auth2, async (c) => {
18668
19377
  return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load vaults" }, 502);
18669
19378
  }
18670
19379
  });
19380
+ app.get("/memory/approved-senders", auth2, async (c) => {
19381
+ const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
19382
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
19383
+ const r = await memoryCall("listApprovedSendersTool", {}, key);
19384
+ return c.json(r);
19385
+ });
19386
+ app.post("/memory/approved-senders", auth2, async (c) => {
19387
+ const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
19388
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
19389
+ const body = await c.req.json().catch(() => ({}));
19390
+ const senderIdentity = body.senderIdentity?.trim();
19391
+ if (!senderIdentity) return c.json({ ok: false, error: "senderIdentity is required" }, 400);
19392
+ const r = await memoryCall("approveSenderTool", { senderIdentity }, key);
19393
+ return c.json(r);
19394
+ });
19395
+ app.delete("/memory/approved-senders/:identity", auth2, async (c) => {
19396
+ const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
19397
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
19398
+ const senderIdentity = c.req.param("identity");
19399
+ const r = await memoryCall("removeApprovedSenderTool", { senderIdentity }, key);
19400
+ return c.json(r);
19401
+ });
18671
19402
  app.get("/memory/connect", auth2, async (c) => {
18672
19403
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
18673
19404
  if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
@@ -18831,6 +19562,154 @@ app.post("/memory/portal", auth2, async (c) => {
18831
19562
  return c.json({ error: message }, 500);
18832
19563
  }
18833
19564
  });
19565
+ app.post("/schedule-checkout", auth2, async (c) => {
19566
+ try {
19567
+ const user = c.get("user");
19568
+ const body = await c.req.json().catch(() => ({}));
19569
+ const priceId = body.priceId?.trim();
19570
+ if (!priceId || !SCHEDULING_PLANS[priceId]) return c.json({ error: "Invalid priceId \u2014 must be a scheduling plan price." }, 400);
19571
+ const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
19572
+ if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
19573
+ const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
19574
+ let customerId = user.stripe_customer_id;
19575
+ if (!customerId) {
19576
+ const customer = await stripeClient.customers.create({ email: user.email });
19577
+ customerId = customer.id;
19578
+ await setStripeCustomerId(user.id, customerId);
19579
+ }
19580
+ const session = await stripeClient.checkout.sessions.create({
19581
+ mode: "subscription",
19582
+ customer: customerId,
19583
+ line_items: [{ price: priceId, quantity: 1 }],
19584
+ client_reference_id: String(user.id),
19585
+ metadata: { product: "mcp-scheduling", userId: String(user.id) },
19586
+ subscription_data: { metadata: { product: "mcp-scheduling", userId: String(user.id) } },
19587
+ automatic_tax: { enabled: true },
19588
+ billing_address_collection: "required",
19589
+ customer_update: { address: "auto", name: "auto" },
19590
+ tax_id_collection: { enabled: true },
19591
+ success_url: `${appOrigin()}/memory?billing=success`,
19592
+ cancel_url: `${appOrigin()}/memory?billing=cancel`
19593
+ });
19594
+ if (!session.url) return c.json({ error: "Stripe did not return a checkout URL." }, 502);
19595
+ return c.json({ url: session.url });
19596
+ } catch (err) {
19597
+ const message = err instanceof Error ? err.message : "Unable to start scheduling checkout.";
19598
+ console.error("[schedule-checkout]", message);
19599
+ return c.json({ error: message }, 500);
19600
+ }
19601
+ });
19602
+ app.get("/schedule-status", auth2, async (c) => {
19603
+ try {
19604
+ const user = c.get("user");
19605
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19606
+ if (!key) return c.json({ error: error ?? "memory unavailable" }, 503);
19607
+ const r = await memoryCall("getScheduleStatusTool", {}, key);
19608
+ return c.json(r);
19609
+ } catch (err) {
19610
+ const message = err instanceof Error ? err.message : "Unable to get schedule status.";
19611
+ console.error("[schedule-status]", message);
19612
+ return c.json({ error: message }, 500);
19613
+ }
19614
+ });
19615
+ app.post("/schedule-link", auth2, async (c) => {
19616
+ try {
19617
+ const user = c.get("user");
19618
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19619
+ if (!key) return c.json({ error: error ?? "memory unavailable" }, 503);
19620
+ const r = await memoryCall("getScheduleLinkTool", {}, key);
19621
+ return c.json(r);
19622
+ } catch (err) {
19623
+ const message = err instanceof Error ? err.message : "Unable to get schedule link.";
19624
+ console.error("[schedule-link]", message);
19625
+ return c.json({ error: message }, 500);
19626
+ }
19627
+ });
19628
+ app.get("/schedule-actions", auth2, async (c) => {
19629
+ const user = c.get("user");
19630
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19631
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
19632
+ const r = await memoryCall("listScheduledActionsTool", {}, key);
19633
+ return c.json(r);
19634
+ });
19635
+ app.post("/schedule-actions", auth2, async (c) => {
19636
+ const user = c.get("user");
19637
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19638
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
19639
+ const body = await c.req.json().catch(() => ({}));
19640
+ const r = await memoryCall("createScheduledActionTool", body, key);
19641
+ return c.json(r);
19642
+ });
19643
+ app.post("/schedule-actions/propose", auth2, async (c) => {
19644
+ const user = c.get("user");
19645
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19646
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
19647
+ const body = await c.req.json().catch(() => ({}));
19648
+ const r = await memoryCall("proposeScheduledActionTool", body, key);
19649
+ return c.json(r);
19650
+ });
19651
+ app.post("/schedule-actions/:id/pause", auth2, async (c) => {
19652
+ const user = c.get("user");
19653
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19654
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
19655
+ const r = await memoryCall("pauseScheduledActionTool", { id: c.req.param("id") }, key);
19656
+ return c.json(r);
19657
+ });
19658
+ app.post("/schedule-actions/:id/resume", auth2, async (c) => {
19659
+ const user = c.get("user");
19660
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19661
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
19662
+ const r = await memoryCall("resumeScheduledActionTool", { id: c.req.param("id") }, key);
19663
+ return c.json(r);
19664
+ });
19665
+ app.delete("/schedule-actions/:id", auth2, async (c) => {
19666
+ const user = c.get("user");
19667
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19668
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
19669
+ const r = await memoryCall("deleteScheduledActionTool", { id: c.req.param("id") }, key);
19670
+ return c.json(r);
19671
+ });
19672
+ app.post("/schedule-link/revoke", auth2, async (c) => {
19673
+ try {
19674
+ const user = c.get("user");
19675
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19676
+ if (!key) return c.json({ error: error ?? "memory unavailable" }, 503);
19677
+ const r = await memoryCall("revokeScheduleLinkTool", {}, key);
19678
+ return c.json(r);
19679
+ } catch (err) {
19680
+ const message = err instanceof Error ? err.message : "Unable to revoke schedule link.";
19681
+ console.error("[schedule-link/revoke]", message);
19682
+ return c.json({ error: message }, 500);
19683
+ }
19684
+ });
19685
+ app.post("/support/submit", auth2, async (c) => {
19686
+ try {
19687
+ const user = c.get("user");
19688
+ const body = await c.req.json().catch(() => ({}));
19689
+ const message = body.message?.trim();
19690
+ if (!message) return c.json({ error: "message is required" }, 400);
19691
+ if (message.length > 8e3) return c.json({ error: "message is too long (max 8000 characters)" }, 400);
19692
+ const inboxKey = process.env.SUPPORT_INBOX_MEMORY_KEY?.trim();
19693
+ if (!inboxKey) return c.json({ error: "support inbox is not configured" }, 503);
19694
+ const stamp = (/* @__PURE__ */ new Date()).toISOString();
19695
+ const path5 = `support/${stamp.replace(/[:.]/g, "-")}-user-${user.id}`;
19696
+ const title = `Support request from ${user.email} (user #${user.id})`;
19697
+ const content = [
19698
+ `**From:** ${user.email} (user #${user.id})`,
19699
+ `**Submitted:** ${stamp}`,
19700
+ "",
19701
+ "**Message:**",
19702
+ message
19703
+ ].join("\n");
19704
+ const r = await memoryCall("putTool", { vault: "Issues", path: path5, title, content }, inboxKey);
19705
+ if (!r.ok) return c.json({ error: r.error ?? "failed to submit support request" }, 502);
19706
+ return c.json({ ok: true });
19707
+ } catch (err) {
19708
+ const message = err instanceof Error ? err.message : "Unable to submit support request.";
19709
+ console.error("[support/submit]", message);
19710
+ return c.json({ error: message }, 500);
19711
+ }
19712
+ });
18834
19713
  app.get("/memory/usage", auth2, async (c) => {
18835
19714
  const user = c.get("user");
18836
19715
  try {
@@ -19544,7 +20423,7 @@ app.get("/cron/tick", async (c) => {
19544
20423
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
19545
20424
  return c.json({ error: "Unauthorized" }, 401);
19546
20425
  }
19547
- const { drainQueue } = await import("./worker-NV4GTIRG.js");
20426
+ const { drainQueue } = await import("./worker-TL4J65DE.js");
19548
20427
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
19549
20428
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
19550
20429
  const [results, sweepResult, reapResult, expiredResult, blobCleanup] = await Promise.all([
@@ -19561,6 +20440,7 @@ app.route("/api/internal/site-architecture-auditor", siteAuditApp);
19561
20440
  app.route("/youtube", youtubeApp);
19562
20441
  app.route("/screenshot", screenshotApp);
19563
20442
  app.route("/facebook", facebookAdApp);
20443
+ app.route("/google-ads", googleAdsApp);
19564
20444
  app.route("/instagram", instagramApp);
19565
20445
  app.route("/reddit", redditApp);
19566
20446
  app.route("/maps", mapsApp);
@@ -19570,6 +20450,7 @@ app.route("/serp-intelligence", serpIntelligenceApp);
19570
20450
  app.route("/mcp", mcpApp);
19571
20451
  app.route("/agent", buildBrowserAgentRoutes());
19572
20452
  app.route("/chat", chatApp);
20453
+ app.route("/schedule", scheduleApp);
19573
20454
  app.get("/console", (c) => c.html(renderConsoleHtml()));
19574
20455
  app.get("/console/auth/:id", async (c) => {
19575
20456
  const id = c.req.param("id");
@@ -19693,4 +20574,4 @@ app.get("/blog/:slug/", (c) => {
19693
20574
  export {
19694
20575
  app
19695
20576
  };
19696
- //# sourceMappingURL=server-KUNOK47B.js.map
20577
+ //# sourceMappingURL=server-HB7WJO3N.js.map