adaria-ai 0.1.0

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 (337) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -0
  3. package/apps.example.yaml +65 -0
  4. package/dist/agent/audit.d.ts +16 -0
  5. package/dist/agent/audit.d.ts.map +1 -0
  6. package/dist/agent/audit.js +42 -0
  7. package/dist/agent/audit.js.map +1 -0
  8. package/dist/agent/claude.d.ts +62 -0
  9. package/dist/agent/claude.d.ts.map +1 -0
  10. package/dist/agent/claude.js +297 -0
  11. package/dist/agent/claude.js.map +1 -0
  12. package/dist/agent/conversation-summary.d.ts +29 -0
  13. package/dist/agent/conversation-summary.d.ts.map +1 -0
  14. package/dist/agent/conversation-summary.js +221 -0
  15. package/dist/agent/conversation-summary.js.map +1 -0
  16. package/dist/agent/core.d.ts +81 -0
  17. package/dist/agent/core.d.ts.map +1 -0
  18. package/dist/agent/core.js +527 -0
  19. package/dist/agent/core.js.map +1 -0
  20. package/dist/agent/mcp-launcher.d.ts +42 -0
  21. package/dist/agent/mcp-launcher.d.ts.map +1 -0
  22. package/dist/agent/mcp-launcher.js +38 -0
  23. package/dist/agent/mcp-launcher.js.map +1 -0
  24. package/dist/agent/mcp-manager.d.ts +81 -0
  25. package/dist/agent/mcp-manager.d.ts.map +1 -0
  26. package/dist/agent/mcp-manager.js +136 -0
  27. package/dist/agent/mcp-manager.js.map +1 -0
  28. package/dist/agent/memory.d.ts +10 -0
  29. package/dist/agent/memory.d.ts.map +1 -0
  30. package/dist/agent/memory.js +95 -0
  31. package/dist/agent/memory.js.map +1 -0
  32. package/dist/agent/safety.d.ts +45 -0
  33. package/dist/agent/safety.d.ts.map +1 -0
  34. package/dist/agent/safety.js +71 -0
  35. package/dist/agent/safety.js.map +1 -0
  36. package/dist/agent/session.d.ts +27 -0
  37. package/dist/agent/session.d.ts.map +1 -0
  38. package/dist/agent/session.js +124 -0
  39. package/dist/agent/session.js.map +1 -0
  40. package/dist/agent/tool-descriptions.d.ts +8 -0
  41. package/dist/agent/tool-descriptions.d.ts.map +1 -0
  42. package/dist/agent/tool-descriptions.js +26 -0
  43. package/dist/agent/tool-descriptions.js.map +1 -0
  44. package/dist/cli/analyze.d.ts +8 -0
  45. package/dist/cli/analyze.d.ts.map +1 -0
  46. package/dist/cli/analyze.js +114 -0
  47. package/dist/cli/analyze.js.map +1 -0
  48. package/dist/cli/daemon.d.ts +2 -0
  49. package/dist/cli/daemon.d.ts.map +1 -0
  50. package/dist/cli/daemon.js +91 -0
  51. package/dist/cli/daemon.js.map +1 -0
  52. package/dist/cli/doctor.d.ts +2 -0
  53. package/dist/cli/doctor.d.ts.map +1 -0
  54. package/dist/cli/doctor.js +198 -0
  55. package/dist/cli/doctor.js.map +1 -0
  56. package/dist/cli/init.d.ts +3 -0
  57. package/dist/cli/init.d.ts.map +1 -0
  58. package/dist/cli/init.js +459 -0
  59. package/dist/cli/init.js.map +1 -0
  60. package/dist/cli/logs.d.ts +4 -0
  61. package/dist/cli/logs.d.ts.map +1 -0
  62. package/dist/cli/logs.js +50 -0
  63. package/dist/cli/logs.js.map +1 -0
  64. package/dist/cli/monitor-cmd.d.ts +11 -0
  65. package/dist/cli/monitor-cmd.d.ts.map +1 -0
  66. package/dist/cli/monitor-cmd.js +59 -0
  67. package/dist/cli/monitor-cmd.js.map +1 -0
  68. package/dist/cli/start.d.ts +11 -0
  69. package/dist/cli/start.d.ts.map +1 -0
  70. package/dist/cli/start.js +103 -0
  71. package/dist/cli/start.js.map +1 -0
  72. package/dist/cli/status.d.ts +9 -0
  73. package/dist/cli/status.d.ts.map +1 -0
  74. package/dist/cli/status.js +49 -0
  75. package/dist/cli/status.js.map +1 -0
  76. package/dist/cli/stop.d.ts +2 -0
  77. package/dist/cli/stop.d.ts.map +1 -0
  78. package/dist/cli/stop.js +34 -0
  79. package/dist/cli/stop.js.map +1 -0
  80. package/dist/collectors/appstore.d.ts +51 -0
  81. package/dist/collectors/appstore.d.ts.map +1 -0
  82. package/dist/collectors/appstore.js +166 -0
  83. package/dist/collectors/appstore.js.map +1 -0
  84. package/dist/collectors/arden-tts.d.ts +60 -0
  85. package/dist/collectors/arden-tts.d.ts.map +1 -0
  86. package/dist/collectors/arden-tts.js +83 -0
  87. package/dist/collectors/arden-tts.js.map +1 -0
  88. package/dist/collectors/asomobile.d.ts +37 -0
  89. package/dist/collectors/asomobile.d.ts.map +1 -0
  90. package/dist/collectors/asomobile.js +88 -0
  91. package/dist/collectors/asomobile.js.map +1 -0
  92. package/dist/collectors/eodin-blog.d.ts +90 -0
  93. package/dist/collectors/eodin-blog.d.ts.map +1 -0
  94. package/dist/collectors/eodin-blog.js +238 -0
  95. package/dist/collectors/eodin-blog.js.map +1 -0
  96. package/dist/collectors/eodin-sdk.d.ts +60 -0
  97. package/dist/collectors/eodin-sdk.d.ts.map +1 -0
  98. package/dist/collectors/eodin-sdk.js +112 -0
  99. package/dist/collectors/eodin-sdk.js.map +1 -0
  100. package/dist/collectors/fridgify-recipes.d.ts +65 -0
  101. package/dist/collectors/fridgify-recipes.d.ts.map +1 -0
  102. package/dist/collectors/fridgify-recipes.js +111 -0
  103. package/dist/collectors/fridgify-recipes.js.map +1 -0
  104. package/dist/collectors/playstore.d.ts +46 -0
  105. package/dist/collectors/playstore.d.ts.map +1 -0
  106. package/dist/collectors/playstore.js +140 -0
  107. package/dist/collectors/playstore.js.map +1 -0
  108. package/dist/collectors/youtube.d.ts +44 -0
  109. package/dist/collectors/youtube.d.ts.map +1 -0
  110. package/dist/collectors/youtube.js +107 -0
  111. package/dist/collectors/youtube.js.map +1 -0
  112. package/dist/config/apps-schema.d.ts +94 -0
  113. package/dist/config/apps-schema.d.ts.map +1 -0
  114. package/dist/config/apps-schema.js +66 -0
  115. package/dist/config/apps-schema.js.map +1 -0
  116. package/dist/config/keychain.d.ts +14 -0
  117. package/dist/config/keychain.d.ts.map +1 -0
  118. package/dist/config/keychain.js +89 -0
  119. package/dist/config/keychain.js.map +1 -0
  120. package/dist/config/load-apps.d.ts +16 -0
  121. package/dist/config/load-apps.d.ts.map +1 -0
  122. package/dist/config/load-apps.js +38 -0
  123. package/dist/config/load-apps.js.map +1 -0
  124. package/dist/config/schema.d.ts +306 -0
  125. package/dist/config/schema.d.ts.map +1 -0
  126. package/dist/config/schema.js +220 -0
  127. package/dist/config/schema.js.map +1 -0
  128. package/dist/config/store.d.ts +38 -0
  129. package/dist/config/store.d.ts.map +1 -0
  130. package/dist/config/store.js +180 -0
  131. package/dist/config/store.js.map +1 -0
  132. package/dist/db/queries.d.ts +304 -0
  133. package/dist/db/queries.d.ts.map +1 -0
  134. package/dist/db/queries.js +327 -0
  135. package/dist/db/queries.js.map +1 -0
  136. package/dist/db/schema.d.ts +15 -0
  137. package/dist/db/schema.d.ts.map +1 -0
  138. package/dist/db/schema.js +252 -0
  139. package/dist/db/schema.js.map +1 -0
  140. package/dist/index.d.ts +3 -0
  141. package/dist/index.d.ts.map +1 -0
  142. package/dist/index.js +86 -0
  143. package/dist/index.js.map +1 -0
  144. package/dist/messenger/adapter.d.ts +63 -0
  145. package/dist/messenger/adapter.d.ts.map +1 -0
  146. package/dist/messenger/adapter.js +7 -0
  147. package/dist/messenger/adapter.js.map +1 -0
  148. package/dist/messenger/factory.d.ts +12 -0
  149. package/dist/messenger/factory.d.ts.map +1 -0
  150. package/dist/messenger/factory.js +9 -0
  151. package/dist/messenger/factory.js.map +1 -0
  152. package/dist/messenger/slack.d.ts +30 -0
  153. package/dist/messenger/slack.d.ts.map +1 -0
  154. package/dist/messenger/slack.js +309 -0
  155. package/dist/messenger/slack.js.map +1 -0
  156. package/dist/messenger/split.d.ts +17 -0
  157. package/dist/messenger/split.d.ts.map +1 -0
  158. package/dist/messenger/split.js +56 -0
  159. package/dist/messenger/split.js.map +1 -0
  160. package/dist/orchestrator/dashboard.d.ts +67 -0
  161. package/dist/orchestrator/dashboard.d.ts.map +1 -0
  162. package/dist/orchestrator/dashboard.js +113 -0
  163. package/dist/orchestrator/dashboard.js.map +1 -0
  164. package/dist/orchestrator/monitor.d.ts +37 -0
  165. package/dist/orchestrator/monitor.d.ts.map +1 -0
  166. package/dist/orchestrator/monitor.js +236 -0
  167. package/dist/orchestrator/monitor.js.map +1 -0
  168. package/dist/orchestrator/types.d.ts +82 -0
  169. package/dist/orchestrator/types.d.ts.map +1 -0
  170. package/dist/orchestrator/types.js +12 -0
  171. package/dist/orchestrator/types.js.map +1 -0
  172. package/dist/orchestrator/weekly.d.ts +66 -0
  173. package/dist/orchestrator/weekly.d.ts.map +1 -0
  174. package/dist/orchestrator/weekly.js +376 -0
  175. package/dist/orchestrator/weekly.js.map +1 -0
  176. package/dist/prompts/loader.d.ts +18 -0
  177. package/dist/prompts/loader.d.ts.map +1 -0
  178. package/dist/prompts/loader.js +28 -0
  179. package/dist/prompts/loader.js.map +1 -0
  180. package/dist/security/auth.d.ts +14 -0
  181. package/dist/security/auth.d.ts.map +1 -0
  182. package/dist/security/auth.js +14 -0
  183. package/dist/security/auth.js.map +1 -0
  184. package/dist/security/prompt-guard.d.ts +21 -0
  185. package/dist/security/prompt-guard.d.ts.map +1 -0
  186. package/dist/security/prompt-guard.js +54 -0
  187. package/dist/security/prompt-guard.js.map +1 -0
  188. package/dist/skills/aso.d.ts +60 -0
  189. package/dist/skills/aso.d.ts.map +1 -0
  190. package/dist/skills/aso.js +322 -0
  191. package/dist/skills/aso.js.map +1 -0
  192. package/dist/skills/content.d.ts +25 -0
  193. package/dist/skills/content.d.ts.map +1 -0
  194. package/dist/skills/content.js +90 -0
  195. package/dist/skills/content.js.map +1 -0
  196. package/dist/skills/index.d.ts +65 -0
  197. package/dist/skills/index.d.ts.map +1 -0
  198. package/dist/skills/index.js +90 -0
  199. package/dist/skills/index.js.map +1 -0
  200. package/dist/skills/onboarding.d.ts +58 -0
  201. package/dist/skills/onboarding.d.ts.map +1 -0
  202. package/dist/skills/onboarding.js +274 -0
  203. package/dist/skills/onboarding.js.map +1 -0
  204. package/dist/skills/registry.d.ts +24 -0
  205. package/dist/skills/registry.d.ts.map +1 -0
  206. package/dist/skills/registry.js +66 -0
  207. package/dist/skills/registry.js.map +1 -0
  208. package/dist/skills/review.d.ts +33 -0
  209. package/dist/skills/review.d.ts.map +1 -0
  210. package/dist/skills/review.js +236 -0
  211. package/dist/skills/review.js.map +1 -0
  212. package/dist/skills/sdk-request.d.ts +30 -0
  213. package/dist/skills/sdk-request.d.ts.map +1 -0
  214. package/dist/skills/sdk-request.js +72 -0
  215. package/dist/skills/sdk-request.js.map +1 -0
  216. package/dist/skills/seo-blog.d.ts +64 -0
  217. package/dist/skills/seo-blog.d.ts.map +1 -0
  218. package/dist/skills/seo-blog.js +268 -0
  219. package/dist/skills/seo-blog.js.map +1 -0
  220. package/dist/skills/short-form.d.ts +28 -0
  221. package/dist/skills/short-form.d.ts.map +1 -0
  222. package/dist/skills/short-form.js +121 -0
  223. package/dist/skills/short-form.js.map +1 -0
  224. package/dist/skills/social-publish.d.ts +32 -0
  225. package/dist/skills/social-publish.d.ts.map +1 -0
  226. package/dist/skills/social-publish.js +133 -0
  227. package/dist/skills/social-publish.js.map +1 -0
  228. package/dist/social/base.d.ts +47 -0
  229. package/dist/social/base.d.ts.map +1 -0
  230. package/dist/social/base.js +26 -0
  231. package/dist/social/base.js.map +1 -0
  232. package/dist/social/facebook.d.ts +27 -0
  233. package/dist/social/facebook.d.ts.map +1 -0
  234. package/dist/social/facebook.js +166 -0
  235. package/dist/social/facebook.js.map +1 -0
  236. package/dist/social/factory.d.ts +26 -0
  237. package/dist/social/factory.d.ts.map +1 -0
  238. package/dist/social/factory.js +32 -0
  239. package/dist/social/factory.js.map +1 -0
  240. package/dist/social/linkedin.d.ts +26 -0
  241. package/dist/social/linkedin.d.ts.map +1 -0
  242. package/dist/social/linkedin.js +190 -0
  243. package/dist/social/linkedin.js.map +1 -0
  244. package/dist/social/threads.d.ts +21 -0
  245. package/dist/social/threads.d.ts.map +1 -0
  246. package/dist/social/threads.js +122 -0
  247. package/dist/social/threads.js.map +1 -0
  248. package/dist/social/tiktok.d.ts +23 -0
  249. package/dist/social/tiktok.d.ts.map +1 -0
  250. package/dist/social/tiktok.js +110 -0
  251. package/dist/social/tiktok.js.map +1 -0
  252. package/dist/social/twitter.d.ts +30 -0
  253. package/dist/social/twitter.d.ts.map +1 -0
  254. package/dist/social/twitter.js +189 -0
  255. package/dist/social/twitter.js.map +1 -0
  256. package/dist/social/youtube.d.ts +21 -0
  257. package/dist/social/youtube.d.ts.map +1 -0
  258. package/dist/social/youtube.js +108 -0
  259. package/dist/social/youtube.js.map +1 -0
  260. package/dist/tools/app-info.d.ts +7 -0
  261. package/dist/tools/app-info.d.ts.map +1 -0
  262. package/dist/tools/app-info.js +53 -0
  263. package/dist/tools/app-info.js.map +1 -0
  264. package/dist/tools/collector-fetch.d.ts +11 -0
  265. package/dist/tools/collector-fetch.d.ts.map +1 -0
  266. package/dist/tools/collector-fetch.js +101 -0
  267. package/dist/tools/collector-fetch.js.map +1 -0
  268. package/dist/tools/db-query.d.ts +29 -0
  269. package/dist/tools/db-query.d.ts.map +1 -0
  270. package/dist/tools/db-query.js +159 -0
  271. package/dist/tools/db-query.js.map +1 -0
  272. package/dist/tools/skill-result.d.ts +8 -0
  273. package/dist/tools/skill-result.d.ts.map +1 -0
  274. package/dist/tools/skill-result.js +63 -0
  275. package/dist/tools/skill-result.js.map +1 -0
  276. package/dist/tools/tool-host.d.ts +12 -0
  277. package/dist/tools/tool-host.d.ts.map +1 -0
  278. package/dist/tools/tool-host.js +124 -0
  279. package/dist/tools/tool-host.js.map +1 -0
  280. package/dist/types/collectors.d.ts +198 -0
  281. package/dist/types/collectors.d.ts.map +1 -0
  282. package/dist/types/collectors.js +28 -0
  283. package/dist/types/collectors.js.map +1 -0
  284. package/dist/types/skill.d.ts +60 -0
  285. package/dist/types/skill.d.ts.map +1 -0
  286. package/dist/types/skill.js +9 -0
  287. package/dist/types/skill.js.map +1 -0
  288. package/dist/utils/circuit-breaker.d.ts +26 -0
  289. package/dist/utils/circuit-breaker.d.ts.map +1 -0
  290. package/dist/utils/circuit-breaker.js +67 -0
  291. package/dist/utils/circuit-breaker.js.map +1 -0
  292. package/dist/utils/errors.d.ts +44 -0
  293. package/dist/utils/errors.d.ts.map +1 -0
  294. package/dist/utils/errors.js +75 -0
  295. package/dist/utils/errors.js.map +1 -0
  296. package/dist/utils/escape.d.ts +11 -0
  297. package/dist/utils/escape.d.ts.map +1 -0
  298. package/dist/utils/escape.js +19 -0
  299. package/dist/utils/escape.js.map +1 -0
  300. package/dist/utils/logger.d.ts +19 -0
  301. package/dist/utils/logger.d.ts.map +1 -0
  302. package/dist/utils/logger.js +93 -0
  303. package/dist/utils/logger.js.map +1 -0
  304. package/dist/utils/parse-json.d.ts +13 -0
  305. package/dist/utils/parse-json.d.ts.map +1 -0
  306. package/dist/utils/parse-json.js +61 -0
  307. package/dist/utils/parse-json.js.map +1 -0
  308. package/dist/utils/paths.d.ts +14 -0
  309. package/dist/utils/paths.d.ts.map +1 -0
  310. package/dist/utils/paths.js +19 -0
  311. package/dist/utils/paths.js.map +1 -0
  312. package/dist/utils/rate-limiter.d.ts +20 -0
  313. package/dist/utils/rate-limiter.d.ts.map +1 -0
  314. package/dist/utils/rate-limiter.js +47 -0
  315. package/dist/utils/rate-limiter.js.map +1 -0
  316. package/dist/utils/retry.d.ts +26 -0
  317. package/dist/utils/retry.d.ts.map +1 -0
  318. package/dist/utils/retry.js +61 -0
  319. package/dist/utils/retry.js.map +1 -0
  320. package/launchd/.gitkeep +0 -0
  321. package/launchd/com.adaria-ai.daemon.plist.template +62 -0
  322. package/launchd/com.adaria-ai.monitor.plist.template +41 -0
  323. package/launchd/com.adaria-ai.weekly.plist.template +43 -0
  324. package/package.json +72 -0
  325. package/prompts/aso-description.md +44 -0
  326. package/prompts/aso-inapp-events.md +20 -0
  327. package/prompts/aso-metadata.md +34 -0
  328. package/prompts/aso-screenshots.md +20 -0
  329. package/prompts/onboarding-hypotheses.md +38 -0
  330. package/prompts/onboarding-review-timing.md +24 -0
  331. package/prompts/review-clustering.md +19 -0
  332. package/prompts/review-replies.md +18 -0
  333. package/prompts/review-sentiment.md +16 -0
  334. package/prompts/seo-blog-fridgify-recipe.md +116 -0
  335. package/prompts/seo-blog.md +69 -0
  336. package/prompts/short-form-ideas.md +50 -0
  337. package/prompts/social-publish.md +46 -0
@@ -0,0 +1,112 @@
1
+ import { ExternalApiError } from "../utils/errors.js";
2
+ const ALLOWED_HOSTS = new Set(["api.eodin.app"]);
3
+ const DEFAULT_BASE_URL = "https://api.eodin.app/api/v1/events";
4
+ const ERROR_BODY_MAX_CHARS = 512;
5
+ export class EodinSdkCollector {
6
+ apiKey;
7
+ baseUrl;
8
+ loggedPercentCohort = false;
9
+ constructor(options, testHooks) {
10
+ if (!options.apiKey) {
11
+ throw new Error("EodinSdkCollector requires apiKey");
12
+ }
13
+ this.apiKey = options.apiKey;
14
+ this.baseUrl = testHooks?.baseUrl ?? DEFAULT_BASE_URL;
15
+ }
16
+ async request(path, params = {}) {
17
+ const url = new URL(`${this.baseUrl}${path}`);
18
+ if (!ALLOWED_HOSTS.has(url.hostname)) {
19
+ throw new Error(`Untrusted SDK host: ${url.hostname}. Allowed: ${[...ALLOWED_HOSTS].join(", ")}`);
20
+ }
21
+ for (const [key, value] of Object.entries(params)) {
22
+ // `null` is a defensive check for loose callers even though QueryParams
23
+ // doesn't declare it — leave both branches in.
24
+ if (value === undefined || value === null || value === "")
25
+ continue;
26
+ url.searchParams.set(key, String(value));
27
+ }
28
+ const response = await fetch(url.toString(), {
29
+ headers: {
30
+ "X-API-Key": this.apiKey,
31
+ "Content-Type": "application/json",
32
+ },
33
+ });
34
+ if (!response.ok) {
35
+ const rawBody = await response.text();
36
+ // The Eodin server has historically echoed the submitted API key back
37
+ // in error bodies (`{"error":"Invalid X-API-Key: <key>"}`). Redact
38
+ // before the message reaches audit logs or Slack error cards.
39
+ const redacted = rawBody
40
+ .replaceAll(this.apiKey, "[REDACTED]")
41
+ .slice(0, ERROR_BODY_MAX_CHARS);
42
+ throw new ExternalApiError(`Eodin SDK API ${String(response.status)}: ${redacted}`, { statusCode: response.status });
43
+ }
44
+ return (await response.json());
45
+ }
46
+ /**
47
+ * Daily/weekly/monthly aggregate rows.
48
+ */
49
+ async getSummary(appId, startDate, endDate, options = {}) {
50
+ const res = await this.request("/summary", {
51
+ app_id: appId,
52
+ start: startDate,
53
+ end: endDate,
54
+ granularity: options.granularity ?? "daily",
55
+ os: options.os ?? "all",
56
+ });
57
+ return res.data ?? [];
58
+ }
59
+ /**
60
+ * Aggregate funnel for the period. Step order is fixed by the Eodin API:
61
+ * `app_install → app_open → core_action → paywall_view → subscribe_start`.
62
+ */
63
+ async getFunnel(appId, startDate, endDate, options = {}) {
64
+ const res = await this.request("/funnel", {
65
+ app_id: appId,
66
+ start: startDate,
67
+ end: endDate,
68
+ source: options.source,
69
+ os: options.os ?? "all",
70
+ });
71
+ return res.data ?? { funnel: [], overall_conversion: 0 };
72
+ }
73
+ /**
74
+ * Cohort retention. `retention[0]` is always the cohort anchor (100% of
75
+ * the cohort by definition), so any value above 1.5 signals the server
76
+ * returned percents instead of fractions; we detect and normalize so
77
+ * every downstream consumer sees fractions in [0, 1].
78
+ */
79
+ async getCohort(appId, startDate, endDate, options = {}) {
80
+ const res = await this.request("/cohort", {
81
+ app_id: appId,
82
+ start: startDate,
83
+ end: endDate,
84
+ granularity: options.granularity ?? "weekly",
85
+ os: options.os ?? "all",
86
+ });
87
+ const cohorts = res.data?.cohorts ?? [];
88
+ return cohorts.map((c) => ({
89
+ ...c,
90
+ retention: this.normalizeRetention(c.retention),
91
+ }));
92
+ }
93
+ normalizeRetention(retention) {
94
+ // Wire is typed as number[] but the Eodin server has historically
95
+ // returned nulls/strings inside the array, so the typeof guards below
96
+ // are intentional runtime defense — not dead code the strict types
97
+ // would otherwise suggest.
98
+ if (!Array.isArray(retention) || retention.length === 0) {
99
+ return retention;
100
+ }
101
+ const first = retention[0];
102
+ if (typeof first !== "number" || first <= 1.5) {
103
+ return retention;
104
+ }
105
+ if (!this.loggedPercentCohort) {
106
+ console.warn("[eodin-sdk] Detected percent-encoded cohort retention; normalizing to fractions.");
107
+ this.loggedPercentCohort = true;
108
+ }
109
+ return retention.map((r) => (typeof r === "number" ? r / 100 : r));
110
+ }
111
+ }
112
+ //# sourceMappingURL=eodin-sdk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eodin-sdk.js","sourceRoot":"","sources":["../../src/collectors/eodin-sdk.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ import type { FridgifyCascadeResult, FridgifyPeriod, FridgifyPopularMetric, FridgifyRecipe } from "../types/collectors.js";
2
+ /**
3
+ * Fridgify Recipes API collector.
4
+ *
5
+ * Thin wrapper around the Fridgify backend's public recipe endpoints that
6
+ * power the growth agent's recipe-aware blog posts.
7
+ *
8
+ * - Base URL: `https://fridgify-api.eodin.app`
9
+ * - Auth: none (public endpoints, IP rate-limited 20 req/min)
10
+ */
11
+ export interface FridgifyRecipesCollectorOptions {
12
+ /** Override the rate-limit backoff window. Defaults to 60 s. */
13
+ retryDelayMs?: number;
14
+ }
15
+ /**
16
+ * Test-only overrides. `baseUrl` stays off the production options type so
17
+ * config loaders cannot introduce a user-controlled URL into the SSRF
18
+ * surface; the allowlist still applies to test-hook values.
19
+ */
20
+ export interface FridgifyRecipesCollectorTestHooks {
21
+ baseUrl?: string;
22
+ }
23
+ export interface GetPopularOptions {
24
+ period?: FridgifyPeriod;
25
+ metric?: FridgifyPopularMetric;
26
+ /** Server clamps to 1–50. */
27
+ limit?: number;
28
+ }
29
+ export interface CascadeOptions {
30
+ metric?: FridgifyPopularMetric;
31
+ limit?: number;
32
+ /** Narrowest window is accepted once `rows.length >= minResults`. */
33
+ minResults?: number;
34
+ }
35
+ export declare class FridgifyRecipesCollector {
36
+ private readonly baseUrl;
37
+ private readonly retryDelayMs;
38
+ constructor(options?: FridgifyRecipesCollectorOptions, testHooks?: FridgifyRecipesCollectorTestHooks);
39
+ private fetchOnce;
40
+ private request;
41
+ /**
42
+ * Top recipes in a time window, ranked by engagement.
43
+ */
44
+ getPopular(options?: GetPopularOptions): Promise<FridgifyRecipe[]>;
45
+ /**
46
+ * Period-cascade variant of {@link getPopular}.
47
+ *
48
+ * Fridgify's `week` window is frequently empty under current traffic.
49
+ * Walk week → month → quarter → year and stop at the narrowest window
50
+ * that yields at least `minResults` rows, so blog copy naturally stays
51
+ * fresh ("Top recipes this week") without the skill giving up when the
52
+ * week is quiet.
53
+ *
54
+ * Callers that need a roundup-worthy result should branch on
55
+ * {@link FridgifyCascadeResult.satisfied}, not on `rows.length > 0`, to
56
+ * avoid building a "top recipes this year" post from a single stray row.
57
+ */
58
+ getPopularWithCascade(options?: CascadeOptions): Promise<FridgifyCascadeResult>;
59
+ /**
60
+ * Fetch a single recipe by id. Returns the same shape as items in
61
+ * {@link getPopular} minus `periodScore`.
62
+ */
63
+ getRecipe(id: string): Promise<FridgifyRecipe>;
64
+ }
65
+ //# sourceMappingURL=fridgify-recipes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fridgify-recipes.d.ts","sourceRoot":"","sources":["../../src/collectors/fridgify-recipes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,qBAAqB,EACrB,cAAc,EACd,qBAAqB,EACrB,cAAc,EACf,MAAM,wBAAwB,CAAC;AAEhC;;;;;;;;GAQG;AACH,MAAM,WAAW,+BAA+B;IAC9C,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,iCAAiC;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAcD,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,6BAA6B;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID,qBAAa,wBAAwB;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAGpC,OAAO,GAAE,+BAAoC,EAC7C,SAAS,CAAC,EAAE,iCAAiC;YAMjC,SAAS;YAMT,OAAO;IAmDrB;;OAEG;IACG,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAa5E;;;;;;;;;;;;OAYG;IACG,qBAAqB,CACzB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,qBAAqB,CAAC;IA0BjC;;;OAGG;IACG,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;CAUrD"}
@@ -0,0 +1,111 @@
1
+ import { ExternalApiError, RateLimitError } from "../utils/errors.js";
2
+ import { info as logInfo, warn as logWarn } from "../utils/logger.js";
3
+ const DEFAULT_BASE_URL = "https://fridgify-api.eodin.app";
4
+ const DEFAULT_RETRY_DELAY_MS = 60_000;
5
+ const ALLOWED_HOSTS = new Set([
6
+ "fridgify-api.eodin.app",
7
+ ]);
8
+ const CASCADE_PERIODS = [
9
+ "week",
10
+ "month",
11
+ "quarter",
12
+ "year",
13
+ ];
14
+ export class FridgifyRecipesCollector {
15
+ baseUrl;
16
+ retryDelayMs;
17
+ constructor(options = {}, testHooks) {
18
+ this.baseUrl = testHooks?.baseUrl ?? DEFAULT_BASE_URL;
19
+ this.retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
20
+ }
21
+ async fetchOnce(url) {
22
+ return fetch(url, {
23
+ headers: { "Content-Type": "application/json" },
24
+ });
25
+ }
26
+ async request(path, params = {}) {
27
+ const url = new URL(`${this.baseUrl}${path}`);
28
+ if (!ALLOWED_HOSTS.has(url.hostname)) {
29
+ throw new Error(`Untrusted Fridgify host: ${url.hostname}. Allowed: ${[...ALLOWED_HOSTS].join(", ")}`);
30
+ }
31
+ for (const [key, value] of Object.entries(params)) {
32
+ if (value === undefined || value === "")
33
+ continue;
34
+ url.searchParams.set(key, String(value));
35
+ }
36
+ const target = url.toString();
37
+ let response = await this.fetchOnce(target);
38
+ // The endpoint is capped at 20 req/min per IP. Retry exactly once after
39
+ // the configured backoff so scheduled weekly runs can ride through a
40
+ // bursty neighbor on the shared IP. Retrying more aggressively would
41
+ // just waste the budget.
42
+ if (response.status === 429) {
43
+ logWarn(`[fridgify-recipes] 429 on ${path}; waiting ${String(this.retryDelayMs)}ms before one retry`);
44
+ await new Promise((resolve) => setTimeout(resolve, this.retryDelayMs));
45
+ response = await this.fetchOnce(target);
46
+ }
47
+ if (!response.ok) {
48
+ const body = await response.text();
49
+ if (response.status === 429) {
50
+ throw new RateLimitError(`Fridgify API still rate limited after 1 retry: ${body.slice(0, 512)}`, { retryAfterSeconds: Math.ceil(this.retryDelayMs / 1000) });
51
+ }
52
+ throw new ExternalApiError(`Fridgify API ${String(response.status)}: ${body.slice(0, 512)}`, { statusCode: response.status });
53
+ }
54
+ return (await response.json());
55
+ }
56
+ /**
57
+ * Top recipes in a time window, ranked by engagement.
58
+ */
59
+ async getPopular(options = {}) {
60
+ const period = options.period ?? "week";
61
+ const metric = options.metric ?? "combined";
62
+ const limit = options.limit ?? 10;
63
+ const data = await this.request("/recipes/popular", {
64
+ period,
65
+ metric,
66
+ limit,
67
+ });
68
+ return Array.isArray(data) ? data : [];
69
+ }
70
+ /**
71
+ * Period-cascade variant of {@link getPopular}.
72
+ *
73
+ * Fridgify's `week` window is frequently empty under current traffic.
74
+ * Walk week → month → quarter → year and stop at the narrowest window
75
+ * that yields at least `minResults` rows, so blog copy naturally stays
76
+ * fresh ("Top recipes this week") without the skill giving up when the
77
+ * week is quiet.
78
+ *
79
+ * Callers that need a roundup-worthy result should branch on
80
+ * {@link FridgifyCascadeResult.satisfied}, not on `rows.length > 0`, to
81
+ * avoid building a "top recipes this year" post from a single stray row.
82
+ */
83
+ async getPopularWithCascade(options = {}) {
84
+ const metric = options.metric ?? "combined";
85
+ const limit = options.limit ?? 10;
86
+ const minResults = options.minResults ?? 5;
87
+ let lastRows = [];
88
+ for (const period of CASCADE_PERIODS) {
89
+ const rows = await this.getPopular({ period, metric, limit });
90
+ lastRows = rows;
91
+ if (rows.length >= minResults) {
92
+ logInfo(`[fridgify-recipes] cascade stopped at period=${period} (${String(rows.length)} rows)`);
93
+ return { period, rows, satisfied: true };
94
+ }
95
+ }
96
+ const finalPeriod = CASCADE_PERIODS[CASCADE_PERIODS.length - 1] ?? "year";
97
+ logWarn(`[fridgify-recipes] cascade exhausted — no window had >=${String(minResults)} rows (last=${String(lastRows.length)})`);
98
+ return { period: finalPeriod, rows: lastRows, satisfied: false };
99
+ }
100
+ /**
101
+ * Fetch a single recipe by id. Returns the same shape as items in
102
+ * {@link getPopular} minus `periodScore`.
103
+ */
104
+ async getRecipe(id) {
105
+ if (id.length === 0) {
106
+ throw new Error("FridgifyRecipesCollector.getRecipe requires a non-empty string id");
107
+ }
108
+ return this.request(`/recipes/${encodeURIComponent(id)}`);
109
+ }
110
+ }
111
+ //# sourceMappingURL=fridgify-recipes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fridgify-recipes.js","sourceRoot":"","sources":["../../src/collectors/fridgify-recipes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { StoreReview } from "../types/collectors.js";
2
+ /**
3
+ * Google Play Developer API collector.
4
+ * Uses a Service Account JSON for authentication via JWT → OAuth token exchange.
5
+ *
6
+ * @see https://developers.google.com/android-publisher
7
+ */
8
+ export interface PlayStoreServiceAccount {
9
+ client_email: string;
10
+ private_key: string;
11
+ }
12
+ export interface PlayStoreCollectorOptions {
13
+ serviceAccountJson: PlayStoreServiceAccount | string;
14
+ }
15
+ /**
16
+ * Test-only overrides. Kept off {@link PlayStoreCollectorOptions} so
17
+ * production config loaders cannot feed a user-controlled URL into the
18
+ * SSRF surface.
19
+ */
20
+ export interface PlayStoreCollectorTestHooks {
21
+ baseUrl?: string;
22
+ tokenUrl?: string;
23
+ }
24
+ export declare class PlayStoreCollector {
25
+ private readonly serviceAccount;
26
+ private readonly baseUrl;
27
+ private readonly tokenUrl;
28
+ private accessToken;
29
+ private tokenExpiresAt;
30
+ constructor(options: PlayStoreCollectorOptions, testHooks?: PlayStoreCollectorTestHooks);
31
+ private getAccessToken;
32
+ private request;
33
+ /**
34
+ * Fetch reviews for a package.
35
+ */
36
+ getReviews(packageName: string): Promise<StoreReview[]>;
37
+ /**
38
+ * Fetch app listing details (title, short/full description) for a locale.
39
+ */
40
+ getAppDetails(packageName: string, locale?: string): Promise<unknown>;
41
+ /**
42
+ * Reply to a review on Google Play. Approval-gated write path.
43
+ */
44
+ replyToReview(packageName: string, reviewId: string, replyText: string): Promise<unknown>;
45
+ }
46
+ //# sourceMappingURL=playstore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playstore.d.ts","sourceRoot":"","sources":["../../src/collectors/playstore.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IACxC,kBAAkB,EAAE,uBAAuB,GAAG,MAAM,CAAC;CACtD;AAED;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA+BD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA0B;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAK;gBAGzB,OAAO,EAAE,yBAAyB,EAClC,SAAS,CAAC,EAAE,2BAA2B;YA+B3B,cAAc;YAsCd,OAAO;IAmCrB;;OAEG;IACG,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IA0B7D;;OAEG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,SAAU,GAAG,OAAO,CAAC,OAAO,CAAC;IAM5E;;OAEG;IACG,aAAa,CACjB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC;CAiBpB"}
@@ -0,0 +1,140 @@
1
+ import { SignJWT, importPKCS8 } from "jose";
2
+ import { AuthError, ExternalApiError, RateLimitError } from "../utils/errors.js";
3
+ import { parseRetryAfter } from "../utils/retry.js";
4
+ const DEFAULT_BASE_URL = "https://androidpublisher.googleapis.com/androidpublisher/v3";
5
+ const DEFAULT_TOKEN_URL = "https://oauth2.googleapis.com/token";
6
+ const REPLY_LIMIT = 350;
7
+ export class PlayStoreCollector {
8
+ serviceAccount;
9
+ baseUrl;
10
+ tokenUrl;
11
+ accessToken = null;
12
+ tokenExpiresAt = 0;
13
+ constructor(options, testHooks) {
14
+ if (!options.serviceAccountJson) {
15
+ throw new Error("PlayStoreCollector requires serviceAccountJson");
16
+ }
17
+ if (typeof options.serviceAccountJson === "string") {
18
+ try {
19
+ this.serviceAccount = JSON.parse(options.serviceAccountJson);
20
+ }
21
+ catch {
22
+ // Never surface parser errors verbatim — the raw text may contain
23
+ // a leading fragment of the private key.
24
+ throw new AuthError("PlayStoreCollector: serviceAccountJson is not valid JSON");
25
+ }
26
+ }
27
+ else {
28
+ this.serviceAccount = options.serviceAccountJson;
29
+ }
30
+ if (!this.serviceAccount.client_email || !this.serviceAccount.private_key) {
31
+ throw new Error("PlayStoreCollector: serviceAccountJson must include client_email and private_key");
32
+ }
33
+ this.baseUrl = testHooks?.baseUrl ?? DEFAULT_BASE_URL;
34
+ this.tokenUrl = testHooks?.tokenUrl ?? DEFAULT_TOKEN_URL;
35
+ }
36
+ async getAccessToken() {
37
+ if (this.accessToken && Date.now() < this.tokenExpiresAt) {
38
+ return this.accessToken;
39
+ }
40
+ const key = await importPKCS8(this.serviceAccount.private_key, "RS256");
41
+ const jwt = await new SignJWT({
42
+ scope: "https://www.googleapis.com/auth/androidpublisher",
43
+ })
44
+ .setProtectedHeader({ alg: "RS256", typ: "JWT" })
45
+ .setIssuer(this.serviceAccount.client_email)
46
+ .setAudience(this.tokenUrl)
47
+ .setIssuedAt()
48
+ .setExpirationTime("1h")
49
+ .sign(key);
50
+ const response = await fetch(this.tokenUrl, {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
53
+ body: new URLSearchParams({
54
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
55
+ assertion: jwt,
56
+ }),
57
+ });
58
+ if (!response.ok) {
59
+ const body = await response.text();
60
+ throw new ExternalApiError(`Google OAuth failed: ${body}`, {
61
+ statusCode: response.status,
62
+ });
63
+ }
64
+ const data = (await response.json());
65
+ this.accessToken = data.access_token;
66
+ this.tokenExpiresAt = Date.now() + (data.expires_in - 60) * 1000;
67
+ return this.accessToken;
68
+ }
69
+ async request(path, init) {
70
+ const token = await this.getAccessToken();
71
+ const url = `${this.baseUrl}${path}`;
72
+ const headers = {
73
+ Authorization: `Bearer ${token}`,
74
+ ...(init?.body !== undefined ? { "Content-Type": "application/json" } : {}),
75
+ ...(init?.headers ?? {}),
76
+ };
77
+ const fetchInit = { headers };
78
+ if (init?.method !== undefined)
79
+ fetchInit.method = init.method;
80
+ if (init?.body !== undefined)
81
+ fetchInit.body = init.body;
82
+ const response = await fetch(url, fetchInit);
83
+ if (response.status === 429) {
84
+ throw new RateLimitError("Google Play API rate limited", {
85
+ retryAfterSeconds: parseRetryAfter(response.headers.get("Retry-After")),
86
+ });
87
+ }
88
+ if (!response.ok) {
89
+ const body = await response.text();
90
+ throw new ExternalApiError(`Google Play API ${String(response.status)}: ${body}`, { statusCode: response.status });
91
+ }
92
+ return (await response.json());
93
+ }
94
+ /**
95
+ * Fetch reviews for a package.
96
+ */
97
+ async getReviews(packageName) {
98
+ const data = await this.request(`/applications/${packageName}/reviews`);
99
+ return (data.reviews ?? []).map((r) => {
100
+ const comment = r.comments?.[0]?.userComment;
101
+ const rawSeconds = comment?.lastModified?.seconds;
102
+ const seconds = typeof rawSeconds === "string"
103
+ ? Number.parseInt(rawSeconds, 10)
104
+ : typeof rawSeconds === "number"
105
+ ? rawSeconds
106
+ : null;
107
+ const createdAt = seconds !== null && Number.isFinite(seconds)
108
+ ? new Date(seconds * 1000).toISOString()
109
+ : null;
110
+ return {
111
+ reviewId: r.reviewId,
112
+ rating: comment?.starRating ?? 0,
113
+ body: comment?.text ?? "",
114
+ createdAt,
115
+ };
116
+ });
117
+ }
118
+ /**
119
+ * Fetch app listing details (title, short/full description) for a locale.
120
+ */
121
+ async getAppDetails(packageName, locale = "ko-KR") {
122
+ return this.request(`/applications/${packageName}/edits/-/listings/${locale}`);
123
+ }
124
+ /**
125
+ * Reply to a review on Google Play. Approval-gated write path.
126
+ */
127
+ async replyToReview(packageName, reviewId, replyText) {
128
+ if (typeof replyText !== "string" || replyText.trim().length === 0) {
129
+ throw new Error("replyToReview: replyText must be a non-empty string");
130
+ }
131
+ if (replyText.length > REPLY_LIMIT) {
132
+ throw new Error(`replyToReview: replyText exceeds Google Play ${String(REPLY_LIMIT)} char limit (${String(replyText.length)})`);
133
+ }
134
+ return this.request(`/applications/${packageName}/reviews/${reviewId}:reply`, {
135
+ method: "POST",
136
+ body: JSON.stringify({ replyText }),
137
+ });
138
+ }
139
+ }
140
+ //# sourceMappingURL=playstore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playstore.js","sourceRoot":"","sources":["../../src/collectors/playstore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import type { YouTubeVideoStats } from "../types/collectors.js";
2
+ /**
3
+ * YouTube Data API v3 client for collecting Shorts performance metrics.
4
+ *
5
+ * Used by `ShortFormSkill` (M5) to pull recent Shorts from a channel and
6
+ * fetch their view / like / comment counts so the weekly briefing can
7
+ * show week-over-week changes.
8
+ *
9
+ * @see https://developers.google.com/youtube/v3
10
+ */
11
+ export interface YouTubeCollectorOptions {
12
+ apiKey: string;
13
+ }
14
+ /**
15
+ * Test-only overrides. `baseUrl` is intentionally kept off
16
+ * {@link YouTubeCollectorOptions} so production config loaders cannot
17
+ * override Google's API host; the allowlist still gates any test-hook
18
+ * value as defense-in-depth.
19
+ */
20
+ export interface YouTubeCollectorTestHooks {
21
+ baseUrl?: string;
22
+ }
23
+ export declare class YouTubeCollector {
24
+ private readonly apiKey;
25
+ private readonly baseUrl;
26
+ constructor(options: YouTubeCollectorOptions, testHooks?: YouTubeCollectorTestHooks);
27
+ private redact;
28
+ private fetchJson;
29
+ /**
30
+ * Get recent Shorts videos from a channel.
31
+ *
32
+ * YouTube's `videoDuration=short` search filter returns clips under
33
+ * **4 minutes** (not 60 seconds). We cross-check the content-details
34
+ * duration against `maxDurationSeconds` (default 60) so callers get a
35
+ * correct "Shorts" bucket by default. Pass a larger value (e.g. 180)
36
+ * if you want YouTube's looser "short-form" definition.
37
+ */
38
+ getRecentShorts(channelId: string, maxResults?: number, maxDurationSeconds?: number): Promise<YouTubeVideoStats[]>;
39
+ /**
40
+ * Get statistics for specific video IDs.
41
+ */
42
+ getVideoStats(videoIds: string[]): Promise<YouTubeVideoStats[]>;
43
+ }
44
+ //# sourceMappingURL=youtube.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"youtube.d.ts","sourceRoot":"","sources":["../../src/collectors/youtube.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE;;;;;;;;GAQG;AACH,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAKD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAG/B,OAAO,EAAE,uBAAuB,EAChC,SAAS,CAAC,EAAE,yBAAyB;IASvC,OAAO,CAAC,MAAM;YAIA,SAAS;IAgCvB;;;;;;;;OAQG;IACG,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,UAAU,SAAK,EACf,kBAAkB,SAAK,GACtB,OAAO,CAAC,iBAAiB,EAAE,CAAC;IA2B/B;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;CAkCtE"}