mcp-scraper 0.3.28 → 0.3.42

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 (44) hide show
  1. package/dist/bin/api-server.cjs +2464 -1134
  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 +237 -6
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +2 -2
  13. package/dist/bin/paa-harvest.cjs +109 -5
  14. package/dist/bin/paa-harvest.cjs.map +1 -1
  15. package/dist/bin/paa-harvest.js +2 -2
  16. package/dist/{chunk-TRNSXR46.js → chunk-DVRPXPYH.js} +22 -3
  17. package/dist/chunk-DVRPXPYH.js.map +1 -0
  18. package/dist/{chunk-QSSH4RCX.js → chunk-GMTS35L6.js} +111 -7
  19. package/dist/chunk-GMTS35L6.js.map +1 -0
  20. package/dist/{chunk-NGD4ITAO.js → chunk-KPF64WST.js} +238 -7
  21. package/dist/chunk-KPF64WST.js.map +1 -0
  22. package/dist/chunk-OUZZNW3I.js +7 -0
  23. package/dist/chunk-OUZZNW3I.js.map +1 -0
  24. package/dist/{chunk-H74L743B.js → chunk-RXJHXBGG.js} +40 -3
  25. package/dist/chunk-RXJHXBGG.js.map +1 -0
  26. package/dist/{db-FP2ABUU4.js → db-LYQENFPW.js} +2 -2
  27. package/dist/index.cjs +109 -5
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +8 -0
  30. package/dist/index.d.ts +8 -0
  31. package/dist/index.js +2 -2
  32. package/dist/{server-IDDX7KGT.js → server-CZT7EULY.js} +1157 -246
  33. package/dist/server-CZT7EULY.js.map +1 -0
  34. package/dist/{worker-NV4GTIRG.js → worker-FNSBPP7Y.js} +4 -4
  35. package/package.json +1 -1
  36. package/dist/chunk-7MY2NSQX.js +0 -7
  37. package/dist/chunk-7MY2NSQX.js.map +0 -1
  38. package/dist/chunk-H74L743B.js.map +0 -1
  39. package/dist/chunk-NGD4ITAO.js.map +0 -1
  40. package/dist/chunk-QSSH4RCX.js.map +0 -1
  41. package/dist/chunk-TRNSXR46.js.map +0 -1
  42. package/dist/server-IDDX7KGT.js.map +0 -1
  43. /package/dist/{db-FP2ABUU4.js.map → db-LYQENFPW.js.map} +0 -0
  44. /package/dist/{worker-NV4GTIRG.js.map → worker-FNSBPP7Y.js.map} +0 -0
@@ -16,7 +16,7 @@ import {
16
16
  renderLinkReport,
17
17
  sanitizeAttempts,
18
18
  sanitizeHarvestResult
19
- } from "./chunk-NGD4ITAO.js";
19
+ } from "./chunk-KPF64WST.js";
20
20
  import {
21
21
  csvRecords,
22
22
  listWorkflowDefinitions,
@@ -29,7 +29,7 @@ import {
29
29
  workflowStepCount,
30
30
  workflowSupportsSteps
31
31
  } from "./chunk-NNEIXK5L.js";
32
- import "./chunk-7MY2NSQX.js";
32
+ import "./chunk-OUZZNW3I.js";
33
33
  import {
34
34
  BROWSER_OPEN_MIN_BALANCE_MC,
35
35
  CONCURRENCY_PRICE_ID,
@@ -40,6 +40,7 @@ import {
40
40
  MC_PER_CREDIT,
41
41
  MEMORY_PLANS,
42
42
  MEMORY_PLAN_QUOTA,
43
+ SCHEDULING_PLANS,
43
44
  SUBSCRIPTION_TIERS,
44
45
  SUBSCRIPTION_TIER_BY_KEY,
45
46
  browserActiveCostMc,
@@ -49,7 +50,7 @@ import {
49
50
  harvestProblemResponse,
50
51
  insufficientBalanceResponse,
51
52
  serializeHarvestProblem
52
- } from "./chunk-H74L743B.js";
53
+ } from "./chunk-RXJHXBGG.js";
53
54
  import {
54
55
  BrowserDriver,
55
56
  MapsSelectors,
@@ -62,7 +63,7 @@ import {
62
63
  recordVendorUsage,
63
64
  resolveKernelProxyId,
64
65
  runWithCostContext
65
- } from "./chunk-QSSH4RCX.js";
66
+ } from "./chunk-GMTS35L6.js";
66
67
  import {
67
68
  CaptchaError,
68
69
  RECAPTCHA_INSTRUCTIONS,
@@ -158,7 +159,7 @@ import {
158
159
  setSubscriptionTier,
159
160
  setUserMemoryCreds,
160
161
  verifyPassword
161
- } from "./chunk-TRNSXR46.js";
162
+ } from "./chunk-DVRPXPYH.js";
162
163
 
163
164
  // src/blog/registry.ts
164
165
  var posts = [
@@ -3547,7 +3548,7 @@ async function fetchWithKernel(url) {
3547
3548
  });
3548
3549
  await client2.browsers.deleteByID(kb.session_id).catch(() => {
3549
3550
  });
3550
- void recordKernelSession({ kernelSessionId: kb.session_id, source: "kernel_fetch", stealth: typeof kb.stealth === "boolean" ? kb.stealth : null, headlessSent: useHeadless ? true : null, proxyUsed: typeof kb.proxy_id === "string" ? Boolean(kb.proxy_id) : null, fallback: true, openedAtMs, closedAtMs: Date.now() });
3551
+ void recordKernelSession({ kernelSessionId: kb.session_id, source: "kernel_fetch", stealth: typeof kb.stealth === "boolean" ? kb.stealth : null, headlessSent: useHeadless, proxyUsed: typeof kb.proxy_id === "string" ? Boolean(kb.proxy_id) : null, fallback: true, openedAtMs, closedAtMs: Date.now() });
3551
3552
  }
3552
3553
  }
3553
3554
 
@@ -5256,7 +5257,7 @@ async function extractSite(opts) {
5256
5257
  }
5257
5258
 
5258
5259
  // src/api/server.ts
5259
- import { Hono as Hono16 } from "hono";
5260
+ import { Hono as Hono18 } from "hono";
5260
5261
  import { serve as serveInngest } from "inngest/hono";
5261
5262
 
5262
5263
  // src/inngest/client.ts
@@ -7805,6 +7806,49 @@ var CATALOG = {
7805
7806
  }
7806
7807
  ]
7807
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
+ },
7808
7852
  {
7809
7853
  key: "instagram",
7810
7854
  label: "Instagram",
@@ -10790,9 +10834,330 @@ facebookAdApp.post("/media", createApiKeyAuth(), async (c) => {
10790
10834
  });
10791
10835
  });
10792
10836
 
10793
- // src/api/instagram-routes.ts
10837
+ // src/api/google-ads-routes.ts
10794
10838
  import { Hono as Hono5 } from "hono";
10795
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";
10796
11161
  import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
10797
11162
  import { homedir as homedir3 } from "os";
10798
11163
  import { join as join5 } from "path";
@@ -11156,33 +11521,33 @@ async function extractInstagramMediaFromPage(page, sourceUrl, capturedMediaUrls
11156
11521
  }
11157
11522
 
11158
11523
  // src/api/instagram-routes.ts
11159
- var InstagramProfileContentBodySchema = z14.object({
11160
- handle: z14.string().trim().optional(),
11161
- url: z14.string().trim().optional(),
11162
- profile: z14.string().trim().min(1).optional(),
11163
- saveProfileChanges: z14.boolean().optional(),
11164
- save_profile_changes: z14.boolean().optional(),
11165
- maxItems: z14.number().int().min(1).max(2e3).default(50),
11166
- maxScrolls: z14.number().int().min(0).max(250).default(10),
11167
- scrollDelayMs: z14.number().int().min(250).max(5e3).default(1200),
11168
- 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)
11169
11534
  }).refine((d) => !!d.handle || !!d.url, {
11170
11535
  message: "handle or url is required"
11171
11536
  });
11172
- var InstagramMediaTypeSchema = z14.enum(["image", "video", "audio"]);
11173
- var InstagramMediaDownloadBodySchema = z14.object({
11174
- url: z14.string().trim().min(1, "url is required"),
11175
- profile: z14.string().trim().min(1).optional(),
11176
- saveProfileChanges: z14.boolean().optional(),
11177
- save_profile_changes: z14.boolean().optional(),
11178
- mediaTypes: z14.array(InstagramMediaTypeSchema).default(["image", "video", "audio"]),
11179
- downloadMedia: z14.boolean().default(true),
11180
- downloadAllTracks: z14.boolean().default(false),
11181
- includeTranscript: z14.boolean().default(false),
11182
- 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)
11183
11548
  });
11184
- var instagramApp = new Hono5();
11185
- function invalidRequest2(message) {
11549
+ var instagramApp = new Hono6();
11550
+ function invalidRequest3(message) {
11186
11551
  return { error_code: "invalid_request", message };
11187
11552
  }
11188
11553
  function resolveSaveProfileChanges(body) {
@@ -11192,14 +11557,14 @@ function resolveProfileName(body) {
11192
11557
  const explicit = body.profile?.trim();
11193
11558
  return explicit || browserServiceProfileName();
11194
11559
  }
11195
- function configuredKernelProxy2() {
11560
+ function configuredKernelProxy3() {
11196
11561
  return (process.env.BROWSER_SERVICE_PROXY_ID ?? process.env.KERNEL_PROXY_ID)?.trim() || void 0;
11197
11562
  }
11198
11563
  async function kernelLaunchOptsDirect(profileName, saveProfileChanges) {
11199
11564
  return {
11200
11565
  headless: true,
11201
11566
  kernelApiKey: browserServiceApiKey(),
11202
- kernelProxyId: configuredKernelProxy2(),
11567
+ kernelProxyId: configuredKernelProxy3(),
11203
11568
  ...profileName ? { kernelProfileName: profileName } : {},
11204
11569
  ...typeof saveProfileChanges === "boolean" ? { kernelProfileSaveChanges: saveProfileChanges } : {},
11205
11570
  viewport: { width: 1280, height: 900 },
@@ -11285,13 +11650,13 @@ function trackFilename(track, index, shortcode) {
11285
11650
  instagramApp.post("/profile-content", createApiKeyAuth(), async (c) => {
11286
11651
  const raw = await c.req.json().catch(() => ({}));
11287
11652
  const parsed = InstagramProfileContentBodySchema.safeParse(raw);
11288
- 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);
11289
11654
  const body = parsed.data;
11290
11655
  let target;
11291
11656
  try {
11292
11657
  target = normalizeInstagramProfileInput(body.url ?? body.handle ?? "");
11293
11658
  } catch (err) {
11294
- 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);
11295
11660
  }
11296
11661
  const user = c.get("user");
11297
11662
  const gate = await acquireConcurrencyGate(user, "instagram_profile_content", {
@@ -11333,12 +11698,12 @@ instagramApp.post("/profile-content", createApiKeyAuth(), async (c) => {
11333
11698
  instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
11334
11699
  const raw = await c.req.json().catch(() => ({}));
11335
11700
  const parsed = InstagramMediaDownloadBodySchema.safeParse(raw);
11336
- 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);
11337
11702
  const body = parsed.data;
11338
11703
  const urlCheck = await validatePublicHttpUrl(body.url, { field: "url", requireHttps: false });
11339
- if (urlCheck.error) return c.json(invalidRequest2(urlCheck.error), 400);
11704
+ if (urlCheck.error) return c.json(invalidRequest3(urlCheck.error), 400);
11340
11705
  const sourceUrl = urlCheck.parsed;
11341
- 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);
11342
11707
  const user = c.get("user");
11343
11708
  const gate = await acquireConcurrencyGate(user, "instagram_media_download", {
11344
11709
  reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
@@ -11455,13 +11820,13 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
11455
11820
  });
11456
11821
 
11457
11822
  // src/api/reddit-routes.ts
11458
- import { Hono as Hono6 } from "hono";
11459
- import { z as z15 } from "zod";
11460
- var RedditThreadBodySchema = z15.object({
11461
- url: z15.string().trim().min(1, "url is required"),
11462
- 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()
11463
11828
  });
11464
- function invalidRequest3(message) {
11829
+ function invalidRequest4(message) {
11465
11830
  return { error_code: "invalid_request", message };
11466
11831
  }
11467
11832
  function toOldReddit(url) {
@@ -11515,14 +11880,14 @@ var PARSE_REDDIT = `(() => {
11515
11880
  });
11516
11881
  return { title: title, author: author, score: score, postBody: postBody, blocked: blocked, numComments: comments.length, comments: comments };
11517
11882
  })()`;
11518
- var redditApp = new Hono6();
11883
+ var redditApp = new Hono7();
11519
11884
  redditApp.post("/thread", createApiKeyAuth(), async (c) => {
11520
11885
  const raw = await c.req.json().catch(() => ({}));
11521
11886
  const parsed = RedditThreadBodySchema.safeParse(raw);
11522
- 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);
11523
11888
  const body = parsed.data;
11524
11889
  const oldUrl = toOldReddit(body.url);
11525
- 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);
11526
11891
  const user = c.get("user");
11527
11892
  const gate = await acquireConcurrencyGate(user, "reddit_thread", {
11528
11893
  reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
@@ -11583,7 +11948,7 @@ redditApp.post("/thread", createApiKeyAuth(), async (c) => {
11583
11948
  });
11584
11949
 
11585
11950
  // src/api/maps-routes.ts
11586
- import { Hono as Hono7 } from "hono";
11951
+ import { Hono as Hono8 } from "hono";
11587
11952
 
11588
11953
  // src/extractor/MapsNavigator.ts
11589
11954
  var MapsNavigator = class {
@@ -11758,7 +12123,7 @@ var MapsReviewCollector = class {
11758
12123
  };
11759
12124
 
11760
12125
  // src/extractor/MapsExtractor.ts
11761
- import { z as z16 } from "zod";
12126
+ import { z as z17 } from "zod";
11762
12127
  var MapsExtractor = class {
11763
12128
  constructor(driver) {
11764
12129
  this.driver = driver;
@@ -11944,7 +12309,7 @@ var MapsExtractor = class {
11944
12309
  });
11945
12310
  return rows;
11946
12311
  }, { hoursTable: MapsSelectors.hoursTable, hoursTableAlt: MapsSelectors.hoursTableAlt });
11947
- const result = z16.array(RawMapsHoursRowSchema).safeParse(raw);
12312
+ const result = z17.array(RawMapsHoursRowSchema).safeParse(raw);
11948
12313
  if (!result.success) {
11949
12314
  console.warn("[MapsExtractor] hours parse failed", result.error.flatten());
11950
12315
  return [];
@@ -12008,7 +12373,7 @@ var MapsExtractor = class {
12008
12373
  });
12009
12374
  return results;
12010
12375
  });
12011
- const result = z16.array(RawMapsAboutAttributeSchema).safeParse(raw);
12376
+ const result = z17.array(RawMapsAboutAttributeSchema).safeParse(raw);
12012
12377
  if (!result.success) {
12013
12378
  console.warn("[MapsExtractor] about parse failed", result.error.flatten());
12014
12379
  return [];
@@ -12384,7 +12749,7 @@ function mapsErrorResponse(c, err, errorCode) {
12384
12749
  attempts: rotationError?.attempts ?? void 0
12385
12750
  }, retryable ? 503 : 500);
12386
12751
  }
12387
- var mapsApp = new Hono7();
12752
+ var mapsApp = new Hono8();
12388
12753
  mapsApp.post("/search", createApiKeyAuth(), async (c) => {
12389
12754
  const user = c.get("user");
12390
12755
  const body = await c.req.json().catch(() => ({}));
@@ -12522,12 +12887,12 @@ mapsApp.post("/place", createApiKeyAuth(), async (c) => {
12522
12887
  });
12523
12888
 
12524
12889
  // src/api/directory-routes.ts
12525
- import { Hono as Hono8 } from "hono";
12890
+ import { Hono as Hono9 } from "hono";
12526
12891
 
12527
12892
  // src/directory/directory-workflow.ts
12528
12893
  import { mkdir as mkdir2, writeFile } from "fs/promises";
12529
12894
  import { join as join6 } from "path";
12530
- import { z as z17 } from "zod";
12895
+ import { z as z18 } from "zod";
12531
12896
 
12532
12897
  // src/directory/location-db.ts
12533
12898
  import { access, readFile } from "fs/promises";
@@ -12688,24 +13053,24 @@ async function resolveDirectoryMarkets(options) {
12688
13053
  }
12689
13054
 
12690
13055
  // src/directory/directory-workflow.ts
12691
- var DirectoryWorkflowOptionsSchema = z17.object({
12692
- query: z17.string().min(1),
12693
- state: z17.string().min(2).default("TN"),
12694
- minPopulation: z17.number().int().min(0).default(1e5),
12695
- populationYear: z17.union(POPULATION_YEARS.map((year) => z17.literal(year))).default(2025),
12696
- maxCities: z17.number().int().min(1).max(100).default(25),
12697
- maxResultsPerCity: z17.number().int().min(1).max(50).default(50),
12698
- concurrency: z17.number().int().min(1).max(5).default(5),
12699
- includeZipGroups: z17.boolean().default(true),
12700
- usZipsCsvPath: z17.string().optional(),
12701
- saveCsv: z17.boolean().default(true),
12702
- gl: z17.string().length(2).default("us"),
12703
- hl: z17.string().length(2).default("en"),
12704
- proxyMode: z17.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
12705
- proxyZip: z17.string().regex(/^\d{5}$/).optional(),
12706
- debug: z17.boolean().default(false),
12707
- headless: z17.boolean().default(true),
12708
- 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()
12709
13074
  });
12710
13075
  function errorMessage(err) {
12711
13076
  return err instanceof Error ? err.message : String(err);
@@ -12926,7 +13291,7 @@ async function runDirectoryWorkflowFromPlan(options, plan) {
12926
13291
  }
12927
13292
 
12928
13293
  // src/api/directory-routes.ts
12929
- var directoryApp = new Hono8();
13294
+ var directoryApp = new Hono9();
12930
13295
  directoryApp.post("/run", createApiKeyAuth(), async (c) => {
12931
13296
  const user = c.get("user");
12932
13297
  const body = await c.req.json().catch(() => ({}));
@@ -12999,35 +13364,35 @@ directoryApp.post("/run", createApiKeyAuth(), async (c) => {
12999
13364
  // src/api/workflow-routes.ts
13000
13365
  import { createHmac } from "crypto";
13001
13366
  import { readFile as readFile2 } from "fs/promises";
13002
- import { Hono as Hono9 } from "hono";
13003
- import { z as z18 } from "zod";
13004
- var workflowApp = new Hono9();
13005
- var WorkflowInputSchema = z18.record(z18.unknown()).default({});
13006
- var WorkflowIdSchema = z18.string().min(1);
13007
- var CadenceSchema = z18.enum(["daily", "weekly", "monthly"]);
13008
- var ScheduleStatusSchema = z18.enum(["active", "paused"]);
13009
- 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({
13010
13375
  workflowId: WorkflowIdSchema,
13011
13376
  input: WorkflowInputSchema,
13012
- webhookUrl: z18.string().url().optional()
13377
+ webhookUrl: z19.string().url().optional()
13013
13378
  });
13014
- var ScheduleCreateSchema = z18.object({
13379
+ var ScheduleCreateSchema = z19.object({
13015
13380
  workflowId: WorkflowIdSchema,
13016
- name: z18.string().min(1).max(120).optional(),
13381
+ name: z19.string().min(1).max(120).optional(),
13017
13382
  input: WorkflowInputSchema,
13018
13383
  cadence: CadenceSchema.default("weekly"),
13019
- timezone: z18.string().min(1).max(64).default("UTC"),
13020
- webhookUrl: z18.string().url().optional(),
13021
- 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()
13022
13387
  });
13023
- var SchedulePatchSchema = z18.object({
13024
- name: z18.string().min(1).max(120).optional(),
13388
+ var SchedulePatchSchema = z19.object({
13389
+ name: z19.string().min(1).max(120).optional(),
13025
13390
  status: ScheduleStatusSchema.optional(),
13026
13391
  input: WorkflowInputSchema.optional(),
13027
13392
  cadence: CadenceSchema.optional(),
13028
- timezone: z18.string().min(1).max(64).optional(),
13029
- webhookUrl: z18.string().url().nullable().optional(),
13030
- 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()
13031
13396
  });
13032
13397
  function hostedWorkflowOutputDir() {
13033
13398
  return workflowOutputBaseDir(process.env.MCP_SCRAPER_WORKFLOW_OUTPUT_DIR?.trim() || "/tmp/mcp-scraper-workflows");
@@ -13531,7 +13896,7 @@ workflowApp.post("/cron/dispatch", async (c) => {
13531
13896
  });
13532
13897
 
13533
13898
  // src/api/serp-intelligence-routes.ts
13534
- import { Hono as Hono10 } from "hono";
13899
+ import { Hono as Hono11 } from "hono";
13535
13900
 
13536
13901
  // src/serp-intelligence/page-snapshot-extractor.ts
13537
13902
  import { createHash as createHash2 } from "crypto";
@@ -13843,7 +14208,7 @@ async function capturePageSnapshots(targets, options = {}) {
13843
14208
  }
13844
14209
 
13845
14210
  // src/serp-intelligence/schemas.ts
13846
- import { z as z19 } from "zod";
14211
+ import { z as z20 } from "zod";
13847
14212
  var SerpIntelligenceDeviceValues = ["desktop", "mobile"];
13848
14213
  var SerpIntelligenceProxyModeValues = ["location", "configured", "none"];
13849
14214
  var SerpIntelligenceAttemptOutcomeValues = [
@@ -13905,171 +14270,171 @@ function isPublicHttpUrl(value) {
13905
14270
  return false;
13906
14271
  }
13907
14272
  }
13908
- var SerpIntelligencePublicHttpUrlSchema = z19.string().url().refine(isPublicHttpUrl, "url must be a public HTTP or HTTPS URL");
13909
- var SerpIntelligenceCaptureBodySchema = z19.object({
13910
- query: z19.string().trim().min(1, "query is required"),
13911
- location: z19.string().trim().min(1).optional(),
13912
- gl: z19.string().trim().length(2).default("us"),
13913
- hl: z19.string().trim().length(2).default("en"),
13914
- device: z19.enum(SerpIntelligenceDeviceValues).default("desktop"),
13915
- proxyMode: z19.enum(SerpIntelligenceProxyModeValues).default(DEFAULT_PROXY_MODE),
13916
- proxyZip: z19.string().regex(/^\d{5}$/).optional(),
13917
- pages: z19.number().int().min(1).max(2).default(1),
13918
- debug: z19.boolean().default(false),
13919
- includePageSnapshots: z19.boolean().default(false),
13920
- 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)
13921
14286
  }).strict();
13922
- var SerpIntelligencePageSnapshotRequestSchema = z19.object({
14287
+ var SerpIntelligencePageSnapshotRequestSchema = z20.object({
13923
14288
  url: SerpIntelligencePublicHttpUrlSchema,
13924
- sourceKind: z19.enum(SerpPageSnapshotSourceKindValues).default("configured_target"),
13925
- sourcePosition: z19.number().int().min(1).optional()
14289
+ sourceKind: z20.enum(SerpPageSnapshotSourceKindValues).default("configured_target"),
14290
+ sourcePosition: z20.number().int().min(1).optional()
13926
14291
  }).strict();
13927
- var SerpIntelligencePageSnapshotsBodySchema = z19.object({
13928
- urls: z19.array(SerpIntelligencePublicHttpUrlSchema).min(1).max(25),
13929
- targets: z19.array(SerpIntelligencePageSnapshotRequestSchema).min(1).max(25).optional(),
13930
- maxConcurrency: z19.number().int().min(1).max(5).default(2),
13931
- timeoutMs: z19.number().int().min(1e3).max(6e4).default(15e3),
13932
- 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)
13933
14298
  }).strict();
13934
- var SerpIntelligenceAICitationSchema = z19.object({
13935
- text: z19.string(),
13936
- href: z19.string()
14299
+ var SerpIntelligenceAICitationSchema = z20.object({
14300
+ text: z20.string(),
14301
+ href: z20.string()
13937
14302
  }).strict();
13938
- var SerpIntelligenceOrganicResultSchema = z19.object({
13939
- position: z19.number().int().min(1),
13940
- title: z19.string(),
13941
- url: z19.string(),
13942
- domain: z19.string(),
13943
- cite: z19.string().nullable(),
13944
- snippet: z19.string().nullable(),
13945
- isRedditStyle: z19.boolean(),
13946
- inlineRating: z19.object({
13947
- value: z19.string(),
13948
- 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()
13949
14314
  }).strict().nullable()
13950
14315
  }).strict();
13951
- var SerpIntelligenceLocationEvidenceSchema = z19.object({
13952
- status: z19.enum(SerpIntelligenceLocalizationStatusValues),
13953
- expected: z19.object({
13954
- city: z19.string(),
13955
- regionCode: z19.string().nullable(),
13956
- 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()
13957
14322
  }).strict().nullable(),
13958
- candidates: z19.array(z19.object({
13959
- city: z19.string(),
13960
- regionCode: z19.string(),
13961
- count: z19.number().int().min(0),
13962
- 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())
13963
14328
  }).strict())
13964
14329
  }).strict();
13965
- var SerpIntelligenceHarvestResultSchema = z19.object({
13966
- seed: z19.string(),
13967
- location: z19.string().nullable(),
13968
- extractedAt: z19.string(),
13969
- totalQuestions: z19.number().int().min(0),
13970
- surface: z19.enum(["web", "aim", "unknown"]),
13971
- aiOverview: z19.object({
13972
- detected: z19.boolean(),
13973
- text: z19.string().nullable(),
13974
- citations: z19.array(SerpIntelligenceAICitationSchema),
13975
- expanded: z19.boolean().optional(),
13976
- fullyExpanded: z19.boolean().optional(),
13977
- 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()
13978
14343
  }).strict(),
13979
- aiMode: z19.object({
13980
- detected: z19.boolean(),
13981
- text: z19.string().nullable(),
13982
- citations: z19.array(SerpIntelligenceAICitationSchema)
14344
+ aiMode: z20.object({
14345
+ detected: z20.boolean(),
14346
+ text: z20.string().nullable(),
14347
+ citations: z20.array(SerpIntelligenceAICitationSchema)
13983
14348
  }).strict(),
13984
- tree: z19.array(z19.unknown()),
13985
- flat: z19.array(z19.unknown()),
13986
- videos: z19.array(z19.unknown()),
13987
- forums: z19.array(z19.unknown()),
13988
- organicResults: z19.array(SerpIntelligenceOrganicResultSchema),
13989
- localPack: z19.array(z19.unknown()),
13990
- entityIds: z19.object({
13991
- entities: z19.array(z19.object({
13992
- name: z19.string(),
13993
- kgId: z19.string().nullable(),
13994
- cid: z19.string().nullable(),
13995
- 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()
13996
14361
  }).strict()),
13997
- kgIds: z19.array(z19.string()),
13998
- cids: z19.array(z19.string()),
13999
- gcids: z19.array(z19.string())
14362
+ kgIds: z20.array(z20.string()),
14363
+ cids: z20.array(z20.string()),
14364
+ gcids: z20.array(z20.string())
14000
14365
  }).strict(),
14001
- stats: z19.object({
14002
- seed: z19.string(),
14003
- totalQuestions: z19.number().int().min(0),
14004
- maxDepthReached: z19.number().int().min(0),
14005
- durationMs: z19.number().min(0),
14006
- 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)
14007
14372
  }).strict(),
14008
- diagnostics: z19.object({
14009
- completionStatus: z19.enum(["paa_found", "no_paa", "serp_only"]),
14010
- problem: z19.null(),
14011
- warnings: z19.array(z19.unknown()).optional(),
14012
- 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({
14013
14378
  locationEvidence: SerpIntelligenceLocationEvidenceSchema.optional()
14014
14379
  }).passthrough().optional()
14015
14380
  }).passthrough(),
14016
- whatPeopleSaying: z19.array(z19.unknown())
14381
+ whatPeopleSaying: z20.array(z20.unknown())
14017
14382
  }).strict();
14018
- var SerpIntelligenceCaptureAttemptSchema = z19.object({
14019
- attemptNumber: z19.number().int().min(1),
14020
- outcome: z19.enum(SerpIntelligenceAttemptOutcomeValues),
14021
- startedAt: z19.string().optional(),
14022
- completedAt: z19.string().optional(),
14023
- durationMs: z19.number().min(0).optional(),
14024
- problemCode: z19.string().optional(),
14025
- message: z19.string().optional(),
14026
- kernelSessionId: z19.string().nullable().optional(),
14027
- 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()
14028
14393
  }).strict();
14029
- var SerpPageSnapshotCaptureSchema = z19.object({
14394
+ var SerpPageSnapshotCaptureSchema = z20.object({
14030
14395
  url: SerpIntelligencePublicHttpUrlSchema,
14031
14396
  requestedUrl: SerpIntelligencePublicHttpUrlSchema,
14032
14397
  finalUrl: SerpIntelligencePublicHttpUrlSchema.nullable(),
14033
- sourceKind: z19.enum(SerpPageSnapshotSourceKindValues),
14034
- sourcePosition: z19.number().int().min(1).nullable(),
14035
- status: z19.enum(SerpPageFetchStatusValues),
14036
- fetchedVia: z19.enum(SerpPageFetchedViaValues).nullable(),
14037
- httpStatus: z19.number().int().min(100).max(599).nullable(),
14038
- contentType: z19.string().nullable(),
14039
- 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(),
14040
14405
  canonicalUrl: SerpIntelligencePublicHttpUrlSchema.nullable(),
14041
- metaDescription: z19.string().nullable(),
14042
- headings: z19.array(z19.object({
14043
- level: z19.number().int().min(1).max(6),
14044
- 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()
14045
14410
  }).strict()).default([]),
14046
- artifact: z19.object({
14047
- htmlBlobUrl: z19.string().url().nullable(),
14048
- textBlobUrl: z19.string().url().nullable(),
14049
- markdownBlobUrl: z19.string().url().nullable(),
14050
- screenshotBlobUrl: z19.string().url().nullable(),
14051
- contentSha256: z19.string().nullable(),
14052
- 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()
14053
14418
  }).strict(),
14054
- error: z19.object({
14055
- code: z19.string(),
14056
- message: z19.string()
14419
+ error: z20.object({
14420
+ code: z20.string(),
14421
+ message: z20.string()
14057
14422
  }).strict().nullable()
14058
14423
  }).strict();
14059
- var SerpIntelligenceCaptureResponseSchema = z19.object({
14424
+ var SerpIntelligenceCaptureResponseSchema = z20.object({
14060
14425
  harvestResult: SerpIntelligenceHarvestResultSchema,
14061
- attempts: z19.array(SerpIntelligenceCaptureAttemptSchema),
14426
+ attempts: z20.array(SerpIntelligenceCaptureAttemptSchema),
14062
14427
  locationEvidence: SerpIntelligenceLocationEvidenceSchema.nullable(),
14063
- pageSnapshotArtifacts: z19.array(SerpPageSnapshotCaptureSchema),
14064
- billing: z19.object({
14065
- creditsUsed: z19.number().min(0).optional(),
14066
- requestId: z19.string().optional(),
14067
- 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()
14068
14433
  }).strict().optional()
14069
14434
  }).strict();
14070
- var SerpIntelligencePageSnapshotsResponseSchema = z19.object({
14071
- pageSnapshotArtifacts: z19.array(SerpPageSnapshotCaptureSchema),
14072
- attempts: z19.array(SerpIntelligenceCaptureAttemptSchema).default([])
14435
+ var SerpIntelligencePageSnapshotsResponseSchema = z20.object({
14436
+ pageSnapshotArtifacts: z20.array(SerpPageSnapshotCaptureSchema),
14437
+ attempts: z20.array(SerpIntelligenceCaptureAttemptSchema).default([])
14073
14438
  }).strict();
14074
14439
 
14075
14440
  // src/serp-intelligence/serp-capture-service.ts
@@ -14241,7 +14606,7 @@ var SERP_INTELLIGENCE_RATE_LIMIT = 60;
14241
14606
  var SERP_INTELLIGENCE_RATE_WINDOW_SECONDS = 60;
14242
14607
  var POST_CAPTURE_ROUTE_LABEL = "POST /capture";
14243
14608
  var POST_PAGE_SNAPSHOTS_ROUTE_LABEL = "POST /page-snapshots";
14244
- var serpIntelligenceApp = new Hono10();
14609
+ var serpIntelligenceApp = new Hono11();
14245
14610
  serpIntelligenceApp.use("*", createApiKeyAuth());
14246
14611
  function structuredError(input) {
14247
14612
  return {
@@ -14410,7 +14775,7 @@ serpIntelligenceApp.post("/page-snapshots", async (c) => {
14410
14775
  });
14411
14776
 
14412
14777
  // src/mcp/mcp-routes.ts
14413
- import { Hono as Hono11 } from "hono";
14778
+ import { Hono as Hono12 } from "hono";
14414
14779
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
14415
14780
 
14416
14781
  // src/mcp/mcp-oauth.ts
@@ -14494,7 +14859,7 @@ async function requireMcpCallerKey(c) {
14494
14859
  if (!user) return mcpAuthError();
14495
14860
  return callerKey;
14496
14861
  }
14497
- var mcpApp = new Hono11();
14862
+ var mcpApp = new Hono12();
14498
14863
  mcpApp.all("/", async (c) => {
14499
14864
  try {
14500
14865
  const keyOrError = await requireMcpCallerKey(c);
@@ -14518,7 +14883,7 @@ mcpApp.all("/", async (c) => {
14518
14883
  });
14519
14884
 
14520
14885
  // src/api/browser-agent-routes.ts
14521
- import { Hono as Hono12 } from "hono";
14886
+ import { Hono as Hono13 } from "hono";
14522
14887
 
14523
14888
  // src/api/browser-agent-db.ts
14524
14889
  import { randomUUID as randomUUID2 } from "crypto";
@@ -15952,7 +16317,7 @@ async function loadOpenSession(id, userId) {
15952
16317
  return row;
15953
16318
  }
15954
16319
  function buildBrowserAgentRoutes() {
15955
- const app2 = new Hono12();
16320
+ const app2 = new Hono13();
15956
16321
  app2.use("*", async (c, next) => {
15957
16322
  await migrateBrowserAgent();
15958
16323
  return next();
@@ -16658,7 +17023,7 @@ if (state.key) { refreshSessions(); if (state.current) selectSession(state.curre
16658
17023
 
16659
17024
  // src/api/stripe-routes.ts
16660
17025
  import Stripe from "stripe";
16661
- import { Hono as Hono13 } from "hono";
17026
+ import { Hono as Hono14 } from "hono";
16662
17027
 
16663
17028
  // src/api/memory.ts
16664
17029
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
@@ -16797,10 +17162,22 @@ async function syncMemoryKeyPlan(user, plan) {
16797
17162
  const res = await memoryCall("setScopeTool", { keyId: target.keyId, plan: effectivePlan }, key);
16798
17163
  return { ok: res.ok, error: res.error };
16799
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
+ }
16800
17177
 
16801
17178
  // src/api/stripe-routes.ts
16802
17179
  var stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
16803
- var stripeApp = new Hono13();
17180
+ var stripeApp = new Hono14();
16804
17181
  stripeApp.post("/webhooks", async (c) => {
16805
17182
  const sig = c.req.header("stripe-signature");
16806
17183
  const body = await c.req.text();
@@ -16839,6 +17216,15 @@ stripeApp.post("/webhooks", async (c) => {
16839
17216
  }
16840
17217
  return c.json({ received: true });
16841
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
+ }
16842
17228
  const user = await getUserByStripeCustomerId(sub.customer);
16843
17229
  if (!user) return c.json({ received: true });
16844
17230
  if (priceId && priceId in SUBSCRIPTION_TIERS) {
@@ -16861,6 +17247,11 @@ stripeApp.post("/webhooks", async (c) => {
16861
17247
  }
16862
17248
  return c.json({ received: true });
16863
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
+ }
16864
17255
  const user = await getUserByStripeCustomerId(sub.customer);
16865
17256
  if (!user) return c.json({ received: true });
16866
17257
  if (priceId && priceId in SUBSCRIPTION_TIERS) {
@@ -16886,7 +17277,7 @@ async function resolveUser(customerId, emailFallback) {
16886
17277
  }
16887
17278
 
16888
17279
  // src/api/oauth-routes.ts
16889
- import { Hono as Hono14 } from "hono";
17280
+ import { Hono as Hono15 } from "hono";
16890
17281
  import { getCookie, setCookie } from "hono/cookie";
16891
17282
  import { createHash as createHash3, randomBytes as randomBytes2, randomUUID as randomUUID3 } from "crypto";
16892
17283
  import { importPKCS8, exportJWK, SignJWT } from "jose";
@@ -16934,10 +17325,14 @@ function isAllowedRedirect(uri) {
16934
17325
  } catch {
16935
17326
  return false;
16936
17327
  }
16937
- if (u.protocol !== "https:") return false;
16938
17328
  const h = u.hostname.toLowerCase();
17329
+ if (u.protocol === "http:" && (h === "localhost" || h === "127.0.0.1" || h === "::1")) return true;
17330
+ if (u.protocol !== "https:") return false;
16939
17331
  if (["claude.ai", "www.claude.ai", "claude.com", "www.claude.com"].includes(h)) return true;
16940
17332
  if (h.endsWith(".claude.ai") || h.endsWith(".claude.com")) return true;
17333
+ if (["chatgpt.com", "www.chatgpt.com", "chat.openai.com", "www.chat.openai.com"].includes(h)) return true;
17334
+ if (h.endsWith(".chatgpt.com") || h.endsWith(".openai.com")) return true;
17335
+ if (h === "viktor.com" || h.endsWith(".viktor.com")) return true;
16941
17336
  const extra = (process.env.OAUTH_EXTRA_REDIRECT_HOSTS ?? "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
16942
17337
  return extra.includes(h);
16943
17338
  }
@@ -17050,7 +17445,7 @@ function redirectWithError(redirectUri, state, error) {
17050
17445
  if (state) u.searchParams.set("state", state);
17051
17446
  return new Response(null, { status: 302, headers: { Location: u.toString() } });
17052
17447
  }
17053
- var oauthApp = new Hono14();
17448
+ var oauthApp = new Hono15();
17054
17449
  oauthApp.use("*", async (c, next) => {
17055
17450
  c.header("Access-Control-Allow-Origin", "*");
17056
17451
  c.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
@@ -17297,8 +17692,8 @@ oauthApp.post("/oauth/token", async (c) => {
17297
17692
  });
17298
17693
 
17299
17694
  // src/api/chat-routes.ts
17300
- import { Hono as Hono15 } from "hono";
17301
- var chatApp = new Hono15();
17695
+ import { Hono as Hono16 } from "hono";
17696
+ var chatApp = new Hono16();
17302
17697
  function tokenOk(token) {
17303
17698
  return !!token && token.startsWith("mk_") && token.length > 10;
17304
17699
  }
@@ -17967,6 +18362,331 @@ loadChannels();
17967
18362
  </html>`;
17968
18363
  }
17969
18364
 
18365
+ // src/api/schedule-routes.ts
18366
+ import { Hono as Hono17 } from "hono";
18367
+ var scheduleApp = new Hono17();
18368
+ function tokenOk2(token) {
18369
+ return !!token && token.startsWith("mk_") && token.length > 10;
18370
+ }
18371
+ function jsonError2(message, status = 400) {
18372
+ return new Response(JSON.stringify({ ok: false, error: message }), { status, headers: { "content-type": "application/json" } });
18373
+ }
18374
+ scheduleApp.get("/api/status", async (c) => {
18375
+ const token = c.req.query("token");
18376
+ if (!tokenOk2(token)) return jsonError2("missing or malformed token", 401);
18377
+ const r = await memoryCall("getScheduleStatusTool", {}, token);
18378
+ return c.json(r);
18379
+ });
18380
+ scheduleApp.get("/api/vaults", async (c) => {
18381
+ const token = c.req.query("token");
18382
+ if (!tokenOk2(token)) return jsonError2("missing or malformed token", 401);
18383
+ const r = await memoryCall("listVaultsTool", {}, token);
18384
+ if (!r.ok) return c.json(r);
18385
+ const vaults = (r.vaults ?? []).filter((v) => v.kind !== "channel");
18386
+ return c.json({ ok: true, vaults });
18387
+ });
18388
+ scheduleApp.get("/api/actions", 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("listScheduledActionsTool", {}, token);
18392
+ return c.json(r);
18393
+ });
18394
+ scheduleApp.post("/api/propose", async (c) => {
18395
+ const body = await c.req.json().catch(() => ({}));
18396
+ if (!tokenOk2(body.token)) return jsonError2("missing or malformed token", 401);
18397
+ if (!body.request) return jsonError2("request is required");
18398
+ const r = await memoryCall("proposeScheduledActionTool", { request: body.request }, body.token);
18399
+ return c.json(r);
18400
+ });
18401
+ scheduleApp.post("/api/actions", async (c) => {
18402
+ const body = await c.req.json().catch(() => ({}));
18403
+ if (!tokenOk2(body.token)) return jsonError2("missing or malformed token", 401);
18404
+ if (!body.description || !body.vault || !body.cadence) return jsonError2("description, vault and cadence are required");
18405
+ const r = await memoryCall("createScheduledActionTool", { description: body.description, vault: body.vault, cadence: body.cadence }, body.token);
18406
+ return c.json(r);
18407
+ });
18408
+ scheduleApp.post("/api/actions/:id/pause", async (c) => {
18409
+ const body = await c.req.json().catch(() => ({}));
18410
+ if (!tokenOk2(body.token)) return jsonError2("missing or malformed token", 401);
18411
+ const r = await memoryCall("pauseScheduledActionTool", { id: c.req.param("id") }, body.token);
18412
+ return c.json(r);
18413
+ });
18414
+ scheduleApp.post("/api/actions/:id/resume", async (c) => {
18415
+ const body = await c.req.json().catch(() => ({}));
18416
+ if (!tokenOk2(body.token)) return jsonError2("missing or malformed token", 401);
18417
+ const r = await memoryCall("resumeScheduledActionTool", { id: c.req.param("id") }, body.token);
18418
+ return c.json(r);
18419
+ });
18420
+ scheduleApp.post("/api/actions/:id/delete", async (c) => {
18421
+ const body = await c.req.json().catch(() => ({}));
18422
+ if (!tokenOk2(body.token)) return jsonError2("missing or malformed token", 401);
18423
+ const r = await memoryCall("deleteScheduledActionTool", { id: c.req.param("id") }, body.token);
18424
+ return c.json(r);
18425
+ });
18426
+ scheduleApp.get("/:token", (c) => {
18427
+ const token = c.req.param("token");
18428
+ if (!tokenOk2(token)) {
18429
+ 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);
18430
+ }
18431
+ return c.html(renderSchedulePage(token));
18432
+ });
18433
+ function renderSchedulePage(token) {
18434
+ return `<!doctype html>
18435
+ <html>
18436
+ <head>
18437
+ <meta charset="utf-8">
18438
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
18439
+ <title>Scheduled Actions</title>
18440
+ <style>
18441
+ :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; }
18442
+ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; color: var(--ink); background: var(--bg); padding-bottom: 90px; }
18443
+ header { position: sticky; top: 0; background: var(--bg); padding: 18px 16px 12px; border-bottom: 1px solid var(--line); z-index: 10; }
18444
+ h1 { font-size: 21px; font-weight: 800; margin: 0 0 4px; letter-spacing: -.01em; }
18445
+ .quota { font-size: 13px; color: var(--muted); transition: color .2s; }
18446
+ .quota.warn { color: var(--warn); font-weight: 600; }
18447
+ .quota-bar { height: 6px; border-radius: 999px; background: var(--line); margin-top: 6px; overflow: hidden; }
18448
+ .quota-bar-fill { height: 100%; background: var(--accent); transition: width .3s ease, background .2s; }
18449
+ .quota-bar-fill.warn { background: var(--warn); }
18450
+ main { padding: 14px; max-width: 640px; margin: 0 auto; }
18451
+ .card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 16px; margin-bottom: 12px; animation: rise .25s ease both; }
18452
+ @keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
18453
+ .empty { text-align: center; color: var(--muted); padding: 56px 20px; font-size: 14px; }
18454
+ .empty-icon { font-size: 32px; margin-bottom: 10px; opacity: .7; }
18455
+ .action-desc { font-size: 15px; line-height: 1.45; margin-bottom: 6px; }
18456
+ .action-meta { font-size: 12.5px; color: var(--muted); display: flex; flex-wrap: wrap; gap: 6px 10px; }
18457
+ .pill { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11.5px; font-weight: 600; }
18458
+ .pill-active { background: #e3f3ea; color: #1a7a4c; }
18459
+ .pill-paused { background: #f3ecd8; color: #93701a; }
18460
+ .action-row { display: flex; gap: 8px; margin-top: 12px; }
18461
+ button { font: inherit; }
18462
+ .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; }
18463
+ .btn:active { transform: scale(.96); }
18464
+ .btn-accent { background: var(--accent); border-color: var(--accent); color: #fff; }
18465
+ .btn-danger { color: #b3261e; }
18466
+ .btn-danger.confirming { background: var(--warn); border-color: var(--warn); color: #fff; }
18467
+ .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; }
18468
+ .fab:active { transform: scale(.98); box-shadow: 0 4px 14px rgba(208,96,69,.35); }
18469
+ .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; }
18470
+ .overlay.open { opacity: 1; pointer-events: auto; }
18471
+ .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); }
18472
+ .overlay.open .sheet { transform: translateY(0); }
18473
+ .sheet h2 { font-size: 17px; margin: 0 0 14px; }
18474
+ 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; }
18475
+ textarea:focus, input:focus, select:focus { border-color: var(--accent); }
18476
+ textarea.autogrow { resize: none; overflow: hidden; line-height: 1.5; min-height: 90px; transition: height .12s ease; }
18477
+ label { font-size: 12.5px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .03em; display: block; margin-top: 14px; }
18478
+ .rationale { font-size: 13px; color: var(--muted); background: #f5f0ea; border-radius: 9px; padding: 10px 12px; margin-top: 10px; }
18479
+ .sheet-actions { display: flex; gap: 8px; margin-top: 18px; }
18480
+ .err { color: #b3261e; font-size: 13px; margin-top: 8px; display: none; }
18481
+ .spin { opacity: .55; pointer-events: none; }
18482
+ .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; }
18483
+ .toast.show { opacity: 1; transform: translate(-50%, 0); }
18484
+ </style>
18485
+ </head>
18486
+ <body>
18487
+ <header>
18488
+ <h1>Scheduled Actions</h1>
18489
+ <div class="quota" id="quota-text">Loading\u2026</div>
18490
+ <div class="quota-bar"><div class="quota-bar-fill" id="quota-fill" style="width:0%"></div></div>
18491
+ </header>
18492
+ <main id="list"></main>
18493
+ <button class="fab" id="new-btn">+ New scheduled action</button>
18494
+ <div class="toast" id="toast"></div>
18495
+
18496
+ <div class="overlay" id="propose-overlay">
18497
+ <div class="sheet">
18498
+ <h2>What should this do?</h2>
18499
+ <textarea id="propose-input" class="autogrow" rows="3" placeholder="e.g. check my competitor's pricing page every Monday and summarize what changed"></textarea>
18500
+ <div class="err" id="propose-err"></div>
18501
+ <div class="sheet-actions">
18502
+ <button class="btn" id="propose-cancel">Cancel</button>
18503
+ <button class="btn btn-accent" id="propose-submit">Propose</button>
18504
+ </div>
18505
+ </div>
18506
+ </div>
18507
+
18508
+ <div class="overlay" id="confirm-overlay">
18509
+ <div class="sheet">
18510
+ <h2>Review before confirming</h2>
18511
+ <label>Description</label>
18512
+ <textarea id="confirm-description" class="autogrow" rows="8"></textarea>
18513
+ <label>Vault</label>
18514
+ <select id="confirm-vault"></select>
18515
+ <label>Cadence</label>
18516
+ <select id="confirm-cadence">
18517
+ <option value="daily">Daily</option>
18518
+ <option value="weekly">Weekly</option>
18519
+ <option value="monthly">Monthly</option>
18520
+ </select>
18521
+ <div class="rationale" id="confirm-rationale"></div>
18522
+ <div class="err" id="confirm-err"></div>
18523
+ <div class="sheet-actions">
18524
+ <button class="btn" id="confirm-cancel">Cancel</button>
18525
+ <button class="btn btn-accent" id="confirm-submit">Confirm & create</button>
18526
+ </div>
18527
+ </div>
18528
+ </div>
18529
+
18530
+ <script>
18531
+ const TOKEN = ${JSON.stringify(token)};
18532
+ const state = { actions: [], vaults: [] };
18533
+
18534
+ function api(path, opts) { return fetch('/schedule' + path, opts).then(r => r.json()); }
18535
+ function get(path, params) { const qs = new URLSearchParams({ token: TOKEN, ...(params||{}) }).toString(); return api(path + '?' + qs); }
18536
+ function post(path, body) { return api(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ token: TOKEN, ...(body||{}) }) }); }
18537
+ function esc(s) { return (s || '').replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c])); }
18538
+ function fmt(iso) { try { return new Date(iso).toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } catch (e) { return iso || ''; } }
18539
+
18540
+ function autogrow(el) { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; }
18541
+ document.querySelectorAll('.autogrow').forEach(el => {
18542
+ el.addEventListener('input', () => autogrow(el));
18543
+ });
18544
+
18545
+ let toastTimer = null;
18546
+ function toast(message) {
18547
+ const el = document.getElementById('toast');
18548
+ el.textContent = message;
18549
+ el.classList.add('show');
18550
+ if (toastTimer) clearTimeout(toastTimer);
18551
+ toastTimer = setTimeout(() => el.classList.remove('show'), 2200);
18552
+ }
18553
+
18554
+ async function loadStatus() {
18555
+ const r = await get('/api/status');
18556
+ const text = document.getElementById('quota-text');
18557
+ const fill = document.getElementById('quota-fill');
18558
+ if (!r.ok || !r.enabled) {
18559
+ text.textContent = 'No active Scheduled Actions subscription.';
18560
+ text.classList.remove('warn');
18561
+ fill.style.width = '0%';
18562
+ fill.classList.remove('warn');
18563
+ return;
18564
+ }
18565
+ const pct = Math.min(100, Math.round((r.usedThisPeriod / Math.max(1, r.quotaPerPeriod)) * 100));
18566
+ const nearLimit = pct >= 90;
18567
+ text.textContent = r.usedThisPeriod + ' / ' + r.quotaPerPeriod + ' actions used this period' + (nearLimit ? ' \u2014 almost at your limit' : '');
18568
+ text.classList.toggle('warn', nearLimit);
18569
+ fill.style.width = pct + '%';
18570
+ fill.classList.toggle('warn', nearLimit);
18571
+ }
18572
+
18573
+ async function loadVaults() {
18574
+ const r = await get('/api/vaults');
18575
+ state.vaults = r.ok ? (r.vaults || []) : [];
18576
+ const sel = document.getElementById('confirm-vault');
18577
+ sel.innerHTML = state.vaults.map(v => '<option value="' + esc(v.handle) + '">' + esc(v.handle) + '</option>').join('');
18578
+ }
18579
+
18580
+ async function loadActions() {
18581
+ const r = await get('/api/actions');
18582
+ const box = document.getElementById('list');
18583
+ if (!r.ok) { box.innerHTML = '<div class="empty">' + esc(r.error || 'Could not load') + '</div>'; return; }
18584
+ state.actions = r.actions || [];
18585
+ if (!state.actions.length) {
18586
+ 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>';
18587
+ return;
18588
+ }
18589
+ box.innerHTML = state.actions.map(renderAction).join('');
18590
+ state.actions.forEach(a => {
18591
+ const pauseBtn = document.getElementById('pause-' + a.id);
18592
+ const resumeBtn = document.getElementById('resume-' + a.id);
18593
+ const delBtn = document.getElementById('delete-' + a.id);
18594
+ if (pauseBtn) pauseBtn.onclick = () => act('pause', a.id, 'Paused');
18595
+ if (resumeBtn) resumeBtn.onclick = () => act('resume', a.id, 'Resumed');
18596
+ if (delBtn) delBtn.onclick = () => confirmDelete(delBtn, a.id);
18597
+ });
18598
+ }
18599
+
18600
+ function confirmDelete(btn, id) {
18601
+ if (!btn.classList.contains('confirming')) {
18602
+ btn.classList.add('confirming');
18603
+ btn.textContent = 'Tap again to delete';
18604
+ btn.dataset.confirmTimer = setTimeout(() => {
18605
+ btn.classList.remove('confirming');
18606
+ btn.textContent = 'Delete';
18607
+ }, 3000);
18608
+ return;
18609
+ }
18610
+ clearTimeout(btn.dataset.confirmTimer);
18611
+ act('delete', id, 'Deleted');
18612
+ }
18613
+
18614
+ function renderAction(a) {
18615
+ const pill = a.status === 'active' ? '<span class="pill pill-active">active</span>' : '<span class="pill pill-paused">' + esc(a.status) + '</span>';
18616
+ return '<div class="card">' +
18617
+ '<div class="action-desc">' + esc(a.description) + '</div>' +
18618
+ '<div class="action-meta">' + pill + '<span>' + esc(a.cadence) + '</span><span>next ' + fmt(a.nextRunAt) + '</span>' +
18619
+ (a.lastRunStatus ? '<span>last: ' + esc(a.lastRunStatus) + '</span>' : '') + '</div>' +
18620
+ '<div class="action-row">' +
18621
+ (a.status === 'active'
18622
+ ? '<button class="btn" id="pause-' + a.id + '">Pause</button>'
18623
+ : '<button class="btn" id="resume-' + a.id + '">Resume</button>') +
18624
+ '<button class="btn btn-danger" id="delete-' + a.id + '">Delete</button>' +
18625
+ '</div></div>';
18626
+ }
18627
+
18628
+ async function act(kind, id, toastMessage) {
18629
+ await post('/api/actions/' + id + '/' + kind, {});
18630
+ await loadActions();
18631
+ await loadStatus();
18632
+ if (toastMessage) toast(toastMessage);
18633
+ }
18634
+
18635
+ document.getElementById('new-btn').onclick = () => {
18636
+ document.getElementById('propose-input').value = '';
18637
+ document.getElementById('propose-err').style.display = 'none';
18638
+ document.getElementById('propose-overlay').classList.add('open');
18639
+ };
18640
+ document.getElementById('propose-cancel').onclick = () => document.getElementById('propose-overlay').classList.remove('open');
18641
+
18642
+ document.getElementById('propose-submit').onclick = async () => {
18643
+ const request = document.getElementById('propose-input').value.trim();
18644
+ const errEl = document.getElementById('propose-err');
18645
+ errEl.style.display = 'none';
18646
+ if (!request) return;
18647
+ const btn = document.getElementById('propose-submit');
18648
+ btn.classList.add('spin'); btn.textContent = 'Thinking\u2026';
18649
+ const r = await post('/api/propose', { request });
18650
+ btn.classList.remove('spin'); btn.textContent = 'Propose';
18651
+ if (!r.ok) { errEl.textContent = r.error || 'Could not interpret that request'; errEl.style.display = 'block'; return; }
18652
+ document.getElementById('propose-overlay').classList.remove('open');
18653
+ document.getElementById('confirm-description').value = r.description || '';
18654
+ document.getElementById('confirm-vault').value = r.vault || '';
18655
+ document.getElementById('confirm-cadence').value = r.cadence || 'daily';
18656
+ document.getElementById('confirm-rationale').textContent = r.rationale || '';
18657
+ document.getElementById('confirm-err').style.display = 'none';
18658
+ document.getElementById('confirm-overlay').classList.add('open');
18659
+ autogrow(document.getElementById('confirm-description'));
18660
+ };
18661
+
18662
+ document.getElementById('confirm-cancel').onclick = () => document.getElementById('confirm-overlay').classList.remove('open');
18663
+
18664
+ document.getElementById('confirm-submit').onclick = async () => {
18665
+ const description = document.getElementById('confirm-description').value.trim();
18666
+ const vault = document.getElementById('confirm-vault').value;
18667
+ const cadence = document.getElementById('confirm-cadence').value;
18668
+ const errEl = document.getElementById('confirm-err');
18669
+ errEl.style.display = 'none';
18670
+ if (!description || !vault) { errEl.textContent = 'Description and vault are required'; errEl.style.display = 'block'; return; }
18671
+ const btn = document.getElementById('confirm-submit');
18672
+ btn.classList.add('spin'); btn.textContent = 'Creating\u2026';
18673
+ const r = await post('/api/actions', { description, vault, cadence });
18674
+ btn.classList.remove('spin'); btn.textContent = 'Confirm & create';
18675
+ if (!r.ok) { errEl.textContent = r.error || 'Could not create scheduled action'; errEl.style.display = 'block'; return; }
18676
+ document.getElementById('confirm-overlay').classList.remove('open');
18677
+ await loadActions();
18678
+ await loadStatus();
18679
+ toast('Scheduled action created');
18680
+ };
18681
+
18682
+ loadStatus();
18683
+ loadVaults();
18684
+ loadActions();
18685
+ </script>
18686
+ </body>
18687
+ </html>`;
18688
+ }
18689
+
17970
18690
  // src/api/site-audit-worker.ts
17971
18691
  var MAX_CONCURRENT_SITE_AUDIT = 1;
17972
18692
  async function drainSiteAuditQueue(budget) {
@@ -18172,16 +18892,32 @@ function toGb(bytes) {
18172
18892
  }
18173
18893
  async function entitledVaultRows(identity) {
18174
18894
  const rows = await sql().query(
18175
- `SELECT vault, owner_identity FROM mem_vault_registry WHERE grantee = $1 ORDER BY created_at ASC`,
18895
+ `SELECT vault, owner_identity, contract FROM mem_vault_registry WHERE grantee = $1 ORDER BY created_at ASC`,
18176
18896
  [identity]
18177
18897
  );
18178
18898
  const seen = /* @__PURE__ */ new Map();
18179
18899
  for (const r of rows) {
18180
18900
  if (seen.has(r.vault)) continue;
18181
- seen.set(r.vault, { vault: r.vault, owner: r.owner_identity, physical: physicalVault(r.owner_identity, r.vault) });
18901
+ const kind = r.contract?.kind === "channel" ? "channel" : "notes";
18902
+ seen.set(r.vault, { vault: r.vault, owner: r.owner_identity, physical: physicalVault(r.owner_identity, r.vault), kind });
18182
18903
  }
18183
18904
  return [...seen.values()];
18184
18905
  }
18906
+ var EMBED_BYTES_PER_CHUNK = Number(process.env.JINA_EMBED_DIM ?? 1024) * 4;
18907
+ async function embedBytesByPhysical(physicals) {
18908
+ const m = /* @__PURE__ */ new Map();
18909
+ try {
18910
+ const rows = await sql().query(
18911
+ `SELECT metadata->>'vault' AS vault, COUNT(*)::bigint AS chunks
18912
+ FROM "memory" WHERE metadata->>'vault' = ANY($1) GROUP BY 1`,
18913
+ [physicals]
18914
+ );
18915
+ for (const r of rows) if (r.vault) m.set(r.vault, Number(r.chunks) * EMBED_BYTES_PER_CHUNK);
18916
+ } catch {
18917
+ return m;
18918
+ }
18919
+ return m;
18920
+ }
18185
18921
  async function storageByPhysical(physicals) {
18186
18922
  if (physicals.length === 0) return /* @__PURE__ */ new Map();
18187
18923
  const rows = await sql().query(
@@ -18189,8 +18925,9 @@ async function storageByPhysical(physicals) {
18189
18925
  FROM mem_notes WHERE vault = ANY($1) GROUP BY vault`,
18190
18926
  [physicals]
18191
18927
  );
18928
+ const embed = await embedBytesByPhysical(physicals);
18192
18929
  const m = /* @__PURE__ */ new Map();
18193
- for (const r of rows) m.set(r.vault, { notes: Number(r.notes), bytes: Number(r.bytes) });
18930
+ for (const r of rows) m.set(r.vault, { notes: Number(r.notes), bytes: Number(r.bytes) + (embed.get(r.vault) ?? 0) });
18194
18931
  return m;
18195
18932
  }
18196
18933
  async function dbListVaults(identity) {
@@ -18204,7 +18941,8 @@ async function dbListVaults(identity) {
18204
18941
  role: isOwner ? "owner" : "shared",
18205
18942
  ...isOwner ? {} : { sharedBy: e.owner },
18206
18943
  notes: st?.notes ?? 0,
18207
- bytes: st?.bytes ?? 0
18944
+ bytes: st?.bytes ?? 0,
18945
+ kind: e.kind
18208
18946
  };
18209
18947
  });
18210
18948
  }
@@ -18381,7 +19119,7 @@ var sessionAuth = createMiddleware3(async (c, next) => {
18381
19119
  c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
18382
19120
  return next();
18383
19121
  });
18384
- var app = new Hono16();
19122
+ var app = new Hono18();
18385
19123
  var STRIPE_API_VERSION = "2026-02-25.clover";
18386
19124
  function requireStripeSecret() {
18387
19125
  const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
@@ -18572,6 +19310,7 @@ app.get("/me", async (c) => {
18572
19310
  const stats = await getUserStats(user.id);
18573
19311
  return c.json({
18574
19312
  authenticated: true,
19313
+ id: user.id,
18575
19314
  email: user.email,
18576
19315
  api_key: user.key_active ? user.api_key : null,
18577
19316
  created_at: user.created_at,
@@ -18646,6 +19385,28 @@ app.get("/memory/vaults", auth2, async (c) => {
18646
19385
  return c.json({ ok: false, error: err instanceof Error ? err.message : "could not load vaults" }, 502);
18647
19386
  }
18648
19387
  });
19388
+ app.get("/memory/approved-senders", auth2, async (c) => {
19389
+ const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
19390
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
19391
+ const r = await memoryCall("listApprovedSendersTool", {}, key);
19392
+ return c.json(r);
19393
+ });
19394
+ app.post("/memory/approved-senders", auth2, async (c) => {
19395
+ const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
19396
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
19397
+ const body = await c.req.json().catch(() => ({}));
19398
+ const senderIdentity = body.senderIdentity?.trim();
19399
+ if (!senderIdentity) return c.json({ ok: false, error: "senderIdentity is required" }, 400);
19400
+ const r = await memoryCall("approveSenderTool", { senderIdentity }, key);
19401
+ return c.json(r);
19402
+ });
19403
+ app.delete("/memory/approved-senders/:identity", auth2, async (c) => {
19404
+ const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
19405
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 502);
19406
+ const senderIdentity = c.req.param("identity");
19407
+ const r = await memoryCall("removeApprovedSenderTool", { senderIdentity }, key);
19408
+ return c.json(r);
19409
+ });
18649
19410
  app.get("/memory/connect", auth2, async (c) => {
18650
19411
  const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
18651
19412
  if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
@@ -18809,6 +19570,154 @@ app.post("/memory/portal", auth2, async (c) => {
18809
19570
  return c.json({ error: message }, 500);
18810
19571
  }
18811
19572
  });
19573
+ app.post("/schedule-checkout", auth2, async (c) => {
19574
+ try {
19575
+ const user = c.get("user");
19576
+ const body = await c.req.json().catch(() => ({}));
19577
+ const priceId = body.priceId?.trim();
19578
+ if (!priceId || !SCHEDULING_PLANS[priceId]) return c.json({ error: "Invalid priceId \u2014 must be a scheduling plan price." }, 400);
19579
+ const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
19580
+ if (!secret2) return c.json({ error: "Stripe is not configured." }, 503);
19581
+ const stripeClient = new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
19582
+ let customerId = user.stripe_customer_id;
19583
+ if (!customerId) {
19584
+ const customer = await stripeClient.customers.create({ email: user.email });
19585
+ customerId = customer.id;
19586
+ await setStripeCustomerId(user.id, customerId);
19587
+ }
19588
+ const session = await stripeClient.checkout.sessions.create({
19589
+ mode: "subscription",
19590
+ customer: customerId,
19591
+ line_items: [{ price: priceId, quantity: 1 }],
19592
+ client_reference_id: String(user.id),
19593
+ metadata: { product: "mcp-scheduling", userId: String(user.id) },
19594
+ subscription_data: { metadata: { product: "mcp-scheduling", userId: String(user.id) } },
19595
+ automatic_tax: { enabled: true },
19596
+ billing_address_collection: "required",
19597
+ customer_update: { address: "auto", name: "auto" },
19598
+ tax_id_collection: { enabled: true },
19599
+ success_url: `${appOrigin()}/memory?billing=success`,
19600
+ cancel_url: `${appOrigin()}/memory?billing=cancel`
19601
+ });
19602
+ if (!session.url) return c.json({ error: "Stripe did not return a checkout URL." }, 502);
19603
+ return c.json({ url: session.url });
19604
+ } catch (err) {
19605
+ const message = err instanceof Error ? err.message : "Unable to start scheduling checkout.";
19606
+ console.error("[schedule-checkout]", message);
19607
+ return c.json({ error: message }, 500);
19608
+ }
19609
+ });
19610
+ app.get("/schedule-status", auth2, async (c) => {
19611
+ try {
19612
+ const user = c.get("user");
19613
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19614
+ if (!key) return c.json({ error: error ?? "memory unavailable" }, 503);
19615
+ const r = await memoryCall("getScheduleStatusTool", {}, key);
19616
+ return c.json(r);
19617
+ } catch (err) {
19618
+ const message = err instanceof Error ? err.message : "Unable to get schedule status.";
19619
+ console.error("[schedule-status]", message);
19620
+ return c.json({ error: message }, 500);
19621
+ }
19622
+ });
19623
+ app.post("/schedule-link", auth2, async (c) => {
19624
+ try {
19625
+ const user = c.get("user");
19626
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19627
+ if (!key) return c.json({ error: error ?? "memory unavailable" }, 503);
19628
+ const r = await memoryCall("getScheduleLinkTool", {}, key);
19629
+ return c.json(r);
19630
+ } catch (err) {
19631
+ const message = err instanceof Error ? err.message : "Unable to get schedule link.";
19632
+ console.error("[schedule-link]", message);
19633
+ return c.json({ error: message }, 500);
19634
+ }
19635
+ });
19636
+ app.get("/schedule-actions", auth2, async (c) => {
19637
+ const user = c.get("user");
19638
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19639
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
19640
+ const r = await memoryCall("listScheduledActionsTool", {}, key);
19641
+ return c.json(r);
19642
+ });
19643
+ app.post("/schedule-actions", 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("createScheduledActionTool", body, key);
19649
+ return c.json(r);
19650
+ });
19651
+ app.post("/schedule-actions/propose", 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 body = await c.req.json().catch(() => ({}));
19656
+ const r = await memoryCall("proposeScheduledActionTool", body, key);
19657
+ return c.json(r);
19658
+ });
19659
+ app.post("/schedule-actions/:id/pause", auth2, async (c) => {
19660
+ const user = c.get("user");
19661
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19662
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
19663
+ const r = await memoryCall("pauseScheduledActionTool", { id: c.req.param("id") }, key);
19664
+ return c.json(r);
19665
+ });
19666
+ app.post("/schedule-actions/:id/resume", auth2, async (c) => {
19667
+ const user = c.get("user");
19668
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19669
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
19670
+ const r = await memoryCall("resumeScheduledActionTool", { id: c.req.param("id") }, key);
19671
+ return c.json(r);
19672
+ });
19673
+ app.delete("/schedule-actions/:id", auth2, async (c) => {
19674
+ const user = c.get("user");
19675
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19676
+ if (!key) return c.json({ ok: false, error: error ?? "memory unavailable" }, 503);
19677
+ const r = await memoryCall("deleteScheduledActionTool", { id: c.req.param("id") }, key);
19678
+ return c.json(r);
19679
+ });
19680
+ app.post("/schedule-link/revoke", auth2, async (c) => {
19681
+ try {
19682
+ const user = c.get("user");
19683
+ const { key, error } = await getOrCreateUserMemoryKey(user);
19684
+ if (!key) return c.json({ error: error ?? "memory unavailable" }, 503);
19685
+ const r = await memoryCall("revokeScheduleLinkTool", {}, key);
19686
+ return c.json(r);
19687
+ } catch (err) {
19688
+ const message = err instanceof Error ? err.message : "Unable to revoke schedule link.";
19689
+ console.error("[schedule-link/revoke]", message);
19690
+ return c.json({ error: message }, 500);
19691
+ }
19692
+ });
19693
+ app.post("/support/submit", auth2, async (c) => {
19694
+ try {
19695
+ const user = c.get("user");
19696
+ const body = await c.req.json().catch(() => ({}));
19697
+ const message = body.message?.trim();
19698
+ if (!message) return c.json({ error: "message is required" }, 400);
19699
+ if (message.length > 8e3) return c.json({ error: "message is too long (max 8000 characters)" }, 400);
19700
+ const inboxKey = process.env.SUPPORT_INBOX_MEMORY_KEY?.trim();
19701
+ if (!inboxKey) return c.json({ error: "support inbox is not configured" }, 503);
19702
+ const stamp = (/* @__PURE__ */ new Date()).toISOString();
19703
+ const path5 = `support/${stamp.replace(/[:.]/g, "-")}-user-${user.id}`;
19704
+ const title = `Support request from ${user.email} (user #${user.id})`;
19705
+ const content = [
19706
+ `**From:** ${user.email} (user #${user.id})`,
19707
+ `**Submitted:** ${stamp}`,
19708
+ "",
19709
+ "**Message:**",
19710
+ message
19711
+ ].join("\n");
19712
+ const r = await memoryCall("putTool", { vault: "Issues", path: path5, title, content }, inboxKey);
19713
+ if (!r.ok) return c.json({ error: r.error ?? "failed to submit support request" }, 502);
19714
+ return c.json({ ok: true });
19715
+ } catch (err) {
19716
+ const message = err instanceof Error ? err.message : "Unable to submit support request.";
19717
+ console.error("[support/submit]", message);
19718
+ return c.json({ error: message }, 500);
19719
+ }
19720
+ });
18812
19721
  app.get("/memory/usage", auth2, async (c) => {
18813
19722
  const user = c.get("user");
18814
19723
  try {
@@ -19522,7 +20431,7 @@ app.get("/cron/tick", async (c) => {
19522
20431
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
19523
20432
  return c.json({ error: "Unauthorized" }, 401);
19524
20433
  }
19525
- const { drainQueue } = await import("./worker-NV4GTIRG.js");
20434
+ const { drainQueue } = await import("./worker-FNSBPP7Y.js");
19526
20435
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
19527
20436
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
19528
20437
  const [results, sweepResult, reapResult, expiredResult, blobCleanup] = await Promise.all([
@@ -19539,6 +20448,7 @@ app.route("/api/internal/site-architecture-auditor", siteAuditApp);
19539
20448
  app.route("/youtube", youtubeApp);
19540
20449
  app.route("/screenshot", screenshotApp);
19541
20450
  app.route("/facebook", facebookAdApp);
20451
+ app.route("/google-ads", googleAdsApp);
19542
20452
  app.route("/instagram", instagramApp);
19543
20453
  app.route("/reddit", redditApp);
19544
20454
  app.route("/maps", mapsApp);
@@ -19548,6 +20458,7 @@ app.route("/serp-intelligence", serpIntelligenceApp);
19548
20458
  app.route("/mcp", mcpApp);
19549
20459
  app.route("/agent", buildBrowserAgentRoutes());
19550
20460
  app.route("/chat", chatApp);
20461
+ app.route("/schedule", scheduleApp);
19551
20462
  app.get("/console", (c) => c.html(renderConsoleHtml()));
19552
20463
  app.get("/console/auth/:id", async (c) => {
19553
20464
  const id = c.req.param("id");
@@ -19671,4 +20582,4 @@ app.get("/blog/:slug/", (c) => {
19671
20582
  export {
19672
20583
  app
19673
20584
  };
19674
- //# sourceMappingURL=server-IDDX7KGT.js.map
20585
+ //# sourceMappingURL=server-CZT7EULY.js.map