thumbgate 1.4.2 → 1.4.4

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 (279) hide show
  1. package/.claude-plugin/README.md +45 -34
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/.claude-plugin/plugin.json +3 -3
  4. package/.well-known/llms.txt +1 -1
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +26 -2
  7. package/adapters/README.md +4 -1
  8. package/adapters/claude/.mcp.json +2 -2
  9. package/adapters/codex/config.toml +2 -2
  10. package/adapters/mcp/server-stdio.js +10 -4
  11. package/adapters/opencode/opencode.json +1 -1
  12. package/bin/cli.js +246 -90
  13. package/config/mcp-allowlists.json +11 -3
  14. package/package.json +184 -21
  15. package/scripts/audit-trail.js +25 -15
  16. package/scripts/auto-wire-hooks.js +127 -0
  17. package/scripts/cli-demo.js +102 -0
  18. package/scripts/cli-schema.js +285 -0
  19. package/scripts/cli-status.js +166 -0
  20. package/scripts/cross-encoder-reranker.js +235 -0
  21. package/scripts/explore-subcommands.js +277 -0
  22. package/scripts/explore.js +569 -0
  23. package/scripts/feedback-loop.js +20 -6
  24. package/scripts/lesson-inference.js +7 -1
  25. package/scripts/lesson-reranker.js +263 -0
  26. package/scripts/lesson-retrieval.js +34 -17
  27. package/scripts/lesson-search.js +69 -0
  28. package/scripts/perplexity-client.js +210 -0
  29. package/scripts/reflector-agent.js +2 -2
  30. package/scripts/statusline-local-stats.js +3 -1
  31. package/scripts/statusline.sh +12 -11
  32. package/src/api/server.js +178 -17
  33. package/src/index.js +3 -0
  34. package/.claude-plugin/bundle/icon.png +0 -0
  35. package/.claude-plugin/bundle/icon.svg +0 -18
  36. package/.claude-plugin/bundle/server/index.js +0 -24
  37. package/adapters/chatgpt/INSTALL.md +0 -138
  38. package/bin/memory.sh +0 -64
  39. package/bin/obsidian-sync.sh +0 -20
  40. package/plugins/amp-skill/INSTALL.md +0 -52
  41. package/plugins/amp-skill/SKILL.md +0 -64
  42. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +0 -22
  43. package/plugins/claude-codex-bridge/.mcp.json +0 -14
  44. package/plugins/claude-codex-bridge/INSTALL.md +0 -43
  45. package/plugins/claude-codex-bridge/README.md +0 -46
  46. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +0 -286
  47. package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +0 -24
  48. package/plugins/claude-codex-bridge/skills/result/SKILL.md +0 -22
  49. package/plugins/claude-codex-bridge/skills/review/SKILL.md +0 -28
  50. package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +0 -27
  51. package/plugins/claude-codex-bridge/skills/setup/SKILL.md +0 -21
  52. package/plugins/claude-codex-bridge/skills/status/SKILL.md +0 -19
  53. package/plugins/claude-skill/INSTALL.md +0 -55
  54. package/plugins/claude-skill/SKILL.md +0 -46
  55. package/plugins/codex-profile/.codex-plugin/plugin.json +0 -43
  56. package/plugins/codex-profile/.mcp.json +0 -14
  57. package/plugins/codex-profile/AGENTS.md +0 -20
  58. package/plugins/codex-profile/INSTALL.md +0 -89
  59. package/plugins/codex-profile/README.md +0 -61
  60. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +0 -23
  61. package/plugins/cursor-marketplace/CHANGELOG.md +0 -30
  62. package/plugins/cursor-marketplace/LICENSE +0 -21
  63. package/plugins/cursor-marketplace/README.md +0 -124
  64. package/plugins/cursor-marketplace/agents/reliability-reviewer.md +0 -31
  65. package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
  66. package/plugins/cursor-marketplace/commands/capture-feedback.md +0 -33
  67. package/plugins/cursor-marketplace/commands/check-gates.md +0 -25
  68. package/plugins/cursor-marketplace/commands/show-lessons.md +0 -27
  69. package/plugins/cursor-marketplace/hooks/hooks.json +0 -10
  70. package/plugins/cursor-marketplace/mcp.json +0 -14
  71. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +0 -34
  72. package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +0 -30
  73. package/plugins/cursor-marketplace/rules/session-continuity.mdc +0 -28
  74. package/plugins/cursor-marketplace/scripts/gate-check.sh +0 -21
  75. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +0 -48
  76. package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +0 -31
  77. package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +0 -30
  78. package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +0 -33
  79. package/plugins/gemini-extension/INSTALL.md +0 -92
  80. package/plugins/gemini-extension/gemini_prompt.txt +0 -14
  81. package/plugins/gemini-extension/tool_contract.json +0 -45
  82. package/plugins/opencode-profile/INSTALL.md +0 -57
  83. package/public/assets/instagram-card.png +0 -0
  84. package/public/assets/tiktok-agent-memory.mp4 +0 -0
  85. package/public/blog.html +0 -474
  86. package/public/compare/mem0.html +0 -189
  87. package/public/compare/speclock.html +0 -180
  88. package/public/compare.html +0 -310
  89. package/public/dashboard.html +0 -1100
  90. package/public/guide.html +0 -317
  91. package/public/guides/claude-code-prevent-repeated-mistakes.html +0 -161
  92. package/public/guides/codex-cli-guardrails.html +0 -158
  93. package/public/guides/cursor-prevent-repeated-mistakes.html +0 -161
  94. package/public/guides/pre-action-gates.html +0 -162
  95. package/public/guides/stop-repeated-ai-agent-mistakes.html +0 -159
  96. package/public/index.html +0 -1128
  97. package/public/js/buyer-intent.js +0 -252
  98. package/public/learn/agent-harness-pattern.html +0 -180
  99. package/public/learn/ai-agent-persistent-memory.html +0 -203
  100. package/public/learn/learn.css +0 -45
  101. package/public/learn/mcp-pre-action-gates-explained.html +0 -172
  102. package/public/learn/stop-ai-agent-force-push.html +0 -134
  103. package/public/learn/vibe-coding-safety-net.html +0 -142
  104. package/public/learn.html +0 -274
  105. package/public/lessons.html +0 -967
  106. package/public/llm-context.md +0 -140
  107. package/public/pro.html +0 -1087
  108. package/public/vercel.json +0 -8
  109. package/scripts/a2ui-engine.js +0 -73
  110. package/scripts/adk-consolidator.js +0 -274
  111. package/scripts/agent-security-hardening.js +0 -225
  112. package/scripts/ai-search-visibility.js +0 -142
  113. package/scripts/autonomous-sales-agent.js +0 -39
  114. package/scripts/autoresearch-runner.js +0 -216
  115. package/scripts/background-agent-governance.js +0 -229
  116. package/scripts/behavioral-extraction.js +0 -93
  117. package/scripts/budget-enforcer.js +0 -173
  118. package/scripts/budget-guard.js +0 -173
  119. package/scripts/build-claude-mcpb.js +0 -255
  120. package/scripts/build-codex-plugin.js +0 -152
  121. package/scripts/capture-railway-diagnostics.sh +0 -97
  122. package/scripts/changeset-check.js +0 -372
  123. package/scripts/check-congruence.js +0 -443
  124. package/scripts/computer-use-firewall.js +0 -280
  125. package/scripts/content-engine/linkedin-content-generator.js +0 -154
  126. package/scripts/content-engine/output/linkedin-memento-validation.md +0 -17
  127. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +0 -175
  128. package/scripts/content-engine/reddit-thread-finder.js +0 -154
  129. package/scripts/context-engine.js +0 -710
  130. package/scripts/daily-digest.js +0 -11
  131. package/scripts/data-governance.js +0 -173
  132. package/scripts/deploy-gcp.sh +0 -44
  133. package/scripts/deploy-policy.js +0 -249
  134. package/scripts/disagreement-mining.js +0 -315
  135. package/scripts/dpo-optimizer.js +0 -206
  136. package/scripts/ensure-repo-bootstrap.js +0 -130
  137. package/scripts/ephemeral-agent-store.js +0 -212
  138. package/scripts/eval-harness.js +0 -56
  139. package/scripts/export-kto-pairs.js +0 -309
  140. package/scripts/export-training.js +0 -446
  141. package/scripts/feedback-fallback.js +0 -111
  142. package/scripts/feedback-inbox-read.js +0 -162
  143. package/scripts/feedback-root-consolidator.js +0 -233
  144. package/scripts/feedback-to-memory.js +0 -185
  145. package/scripts/gate-satisfy.js +0 -42
  146. package/scripts/generate-paperbanana-diagrams.sh +0 -99
  147. package/scripts/generate-pretool-hook.sh +0 -40
  148. package/scripts/github-about.js +0 -430
  149. package/scripts/github-outreach.js +0 -65
  150. package/scripts/gtm-revenue-loop.js +0 -535
  151. package/scripts/hallucination-detector.js +0 -226
  152. package/scripts/hf-papers.js +0 -317
  153. package/scripts/hook-auto-capture.sh +0 -100
  154. package/scripts/hook-stop-pr-thread-check.sh +0 -68
  155. package/scripts/hook-stop-self-score.sh +0 -51
  156. package/scripts/hook-stop-verify-deploy.sh +0 -31
  157. package/scripts/hook-verify-before-done.sh +0 -20
  158. package/scripts/managed-dpo-export.js +0 -91
  159. package/scripts/markdown-escape.js +0 -12
  160. package/scripts/marketing-experiment.js +0 -657
  161. package/scripts/memalign-recall.js +0 -111
  162. package/scripts/memory-migration.js +0 -296
  163. package/scripts/meta-policy.js +0 -190
  164. package/scripts/metered-billing.js +0 -16
  165. package/scripts/model-tier-router.js +0 -310
  166. package/scripts/money-watcher.js +0 -218
  167. package/scripts/multi-hop-recall.js +0 -240
  168. package/scripts/per-step-scoring.js +0 -163
  169. package/scripts/perplexity-marketing.js +0 -466
  170. package/scripts/pii-scanner.js +0 -153
  171. package/scripts/plan-gate.js +0 -154
  172. package/scripts/post-everywhere.js +0 -341
  173. package/scripts/post-to-x-retry.sh +0 -22
  174. package/scripts/post-to-x.js +0 -369
  175. package/scripts/pr-manager.js +0 -421
  176. package/scripts/principle-extractor.js +0 -162
  177. package/scripts/pro-features.js +0 -41
  178. package/scripts/prompt-dlp.js +0 -222
  179. package/scripts/prove-adapters.js +0 -860
  180. package/scripts/prove-attribution.js +0 -361
  181. package/scripts/prove-automation.js +0 -651
  182. package/scripts/prove-autoresearch.js +0 -304
  183. package/scripts/prove-claim-verification.js +0 -277
  184. package/scripts/prove-cloudflare-sandbox.js +0 -161
  185. package/scripts/prove-data-pipeline.js +0 -408
  186. package/scripts/prove-data-quality.js +0 -227
  187. package/scripts/prove-evolution.js +0 -352
  188. package/scripts/prove-harnesses.js +0 -287
  189. package/scripts/prove-intelligence.js +0 -257
  190. package/scripts/prove-lancedb.js +0 -425
  191. package/scripts/prove-local-intelligence.js +0 -340
  192. package/scripts/prove-loop-closure.js +0 -263
  193. package/scripts/prove-packaged-runtime.js +0 -326
  194. package/scripts/prove-predictive-insights.js +0 -355
  195. package/scripts/prove-runtime.js +0 -363
  196. package/scripts/prove-seo-gsd.js +0 -234
  197. package/scripts/prove-settings.js +0 -279
  198. package/scripts/prove-subway-upgrades.js +0 -277
  199. package/scripts/prove-tessl.js +0 -229
  200. package/scripts/prove-training-export.js +0 -325
  201. package/scripts/prove-workflow-contract.js +0 -112
  202. package/scripts/prove-xmemory.js +0 -332
  203. package/scripts/publish-decision.js +0 -159
  204. package/scripts/ralph-loop.js +0 -376
  205. package/scripts/ralph-mode-ci.js +0 -331
  206. package/scripts/reddit-dm-outreach.js +0 -192
  207. package/scripts/reddit-monitor-cron.sh +0 -26
  208. package/scripts/reminder-engine.js +0 -132
  209. package/scripts/revenue-status.js +0 -472
  210. package/scripts/rotate-stripe-webhook-secret.js +0 -314
  211. package/scripts/schedule-manager.js +0 -249
  212. package/scripts/self-healing-check.js +0 -193
  213. package/scripts/shieldcortex-memory-firewall-runner.mjs +0 -53
  214. package/scripts/skill-exporter.js +0 -260
  215. package/scripts/skill-materializer.js +0 -134
  216. package/scripts/skill-packs.js +0 -136
  217. package/scripts/skill-proposer.js +0 -99
  218. package/scripts/skill-quality-tracker.js +0 -282
  219. package/scripts/slow-loop.js +0 -72
  220. package/scripts/social-analytics/db/analytics.sqlite +0 -0
  221. package/scripts/social-analytics/db/schema.sql +0 -32
  222. package/scripts/social-analytics/digest.js +0 -256
  223. package/scripts/social-analytics/engagement-audit.js +0 -185
  224. package/scripts/social-analytics/generate-instagram-card.js +0 -97
  225. package/scripts/social-analytics/instagram-thumbgate-post.js +0 -111
  226. package/scripts/social-analytics/install-growth-automation.js +0 -114
  227. package/scripts/social-analytics/load-env.js +0 -77
  228. package/scripts/social-analytics/mcp-server.js +0 -289
  229. package/scripts/social-analytics/normalizer.js +0 -580
  230. package/scripts/social-analytics/notify.js +0 -162
  231. package/scripts/social-analytics/poll-all.js +0 -107
  232. package/scripts/social-analytics/pollers/github.js +0 -195
  233. package/scripts/social-analytics/pollers/instagram.js +0 -253
  234. package/scripts/social-analytics/pollers/linkedin.js +0 -340
  235. package/scripts/social-analytics/pollers/plausible.js +0 -245
  236. package/scripts/social-analytics/pollers/reddit.js +0 -306
  237. package/scripts/social-analytics/pollers/threads.js +0 -233
  238. package/scripts/social-analytics/pollers/tiktok.js +0 -203
  239. package/scripts/social-analytics/pollers/x.js +0 -227
  240. package/scripts/social-analytics/pollers/youtube.js +0 -304
  241. package/scripts/social-analytics/pollers/zernio.js +0 -183
  242. package/scripts/social-analytics/publish-instagram-thumbgate.js +0 -104
  243. package/scripts/social-analytics/publish-thumbgate-launch.js +0 -322
  244. package/scripts/social-analytics/publishers/devto.js +0 -122
  245. package/scripts/social-analytics/publishers/instagram.js +0 -317
  246. package/scripts/social-analytics/publishers/linkedin.js +0 -294
  247. package/scripts/social-analytics/publishers/reddit.js +0 -385
  248. package/scripts/social-analytics/publishers/threads.js +0 -275
  249. package/scripts/social-analytics/publishers/tiktok.js +0 -217
  250. package/scripts/social-analytics/publishers/x.js +0 -259
  251. package/scripts/social-analytics/publishers/youtube.js +0 -223
  252. package/scripts/social-analytics/publishers/zernio.js +0 -539
  253. package/scripts/social-analytics/reconcile-thumbgate-campaign.js +0 -165
  254. package/scripts/social-analytics/run-digest.js +0 -34
  255. package/scripts/social-analytics/schedule-thumbgate-campaign.js +0 -275
  256. package/scripts/social-analytics/store.js +0 -455
  257. package/scripts/social-analytics/sync-launch-assets.js +0 -185
  258. package/scripts/social-analytics/utm.js +0 -143
  259. package/scripts/social-pipeline.js +0 -2626
  260. package/scripts/social-post-hourly.js +0 -228
  261. package/scripts/social-quality-gate.js +0 -134
  262. package/scripts/social-reply-monitor.js +0 -592
  263. package/scripts/status-dashboard.js +0 -155
  264. package/scripts/stripe-live-status.js +0 -115
  265. package/scripts/subagent-profiles.js +0 -79
  266. package/scripts/sync-branch-protection.js +0 -340
  267. package/scripts/sync-gh-secrets-from-env.sh +0 -70
  268. package/scripts/sync-github-about.js +0 -55
  269. package/scripts/sync-version.js +0 -479
  270. package/scripts/synthetic-dpo.js +0 -234
  271. package/scripts/tessl-export.js +0 -369
  272. package/scripts/test-coverage.js +0 -128
  273. package/scripts/thumbgate_session_start.sh +0 -32
  274. package/scripts/train_from_feedback.py +0 -929
  275. package/scripts/validate-feedback.js +0 -581
  276. package/scripts/verify-obsidian-setup.sh +0 -269
  277. package/scripts/verify-run.js +0 -269
  278. package/scripts/weekly-auto-post.js +0 -124
  279. package/scripts/x-autonomous-marketing.js +0 -139
@@ -1,304 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * youtube.js
5
- * Polls the YouTube Data API v3 for channel and video engagement data.
6
- * Focused on YouTube Shorts (@IgorGanapolsky123).
7
- *
8
- * Required env vars:
9
- * YOUTUBE_API_KEY — Data API v3 key (required)
10
- * YOUTUBE_CHANNEL_ID — Channel ID, e.g. UCxxxxxxxxxxxxxxxxxxxxxxxx (required)
11
- *
12
- * API reference:
13
- * GET /channels?part=statistics&id={channelId}&key={apiKey}
14
- * GET /search?part=snippet&channelId={channelId}&order=date&type=video&maxResults={n}&key={apiKey}
15
- * GET /videos?part=statistics,contentDetails,snippet&id={ids}&key={apiKey}
16
- */
17
-
18
- const YOUTUBE_API_BASE = 'https://www.googleapis.com/youtube/v3';
19
-
20
- /**
21
- * Parses an ISO 8601 duration string (e.g. PT1M30S) into total seconds.
22
- *
23
- * @param {string} duration - ISO 8601 duration string
24
- * @returns {number} Total seconds
25
- */
26
- function parseDurationSeconds(duration) {
27
- if (!duration) return 0;
28
- // Matches optional hours (H), minutes (M), seconds (S)
29
- const match = duration.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/);
30
- if (!match) return 0;
31
- const hours = parseInt(match[1] || '0', 10);
32
- const minutes = parseInt(match[2] || '0', 10);
33
- const seconds = parseInt(match[3] || '0', 10);
34
- return hours * 3600 + minutes * 60 + seconds;
35
- }
36
-
37
- /**
38
- * Fetch channel-level statistics.
39
- *
40
- * @param {string} apiKey - YouTube Data API v3 key
41
- * @param {string} channelId - YouTube channel ID
42
- * @returns {Promise<{ subscriberCount: number, viewCount: number, videoCount: number }>}
43
- */
44
- async function fetchChannelStats(apiKey, channelId) {
45
- if (!apiKey) throw new Error('apiKey is required');
46
- if (!channelId) throw new Error('channelId is required');
47
-
48
- const url =
49
- `${YOUTUBE_API_BASE}/channels` +
50
- `?part=statistics&id=${encodeURIComponent(channelId)}&key=${encodeURIComponent(apiKey)}`;
51
-
52
- console.log(`[youtube] Fetching channel stats for channelId=${channelId}`);
53
-
54
- const res = await fetch(url);
55
- if (!res.ok) {
56
- const text = await res.text().catch(() => '');
57
- throw new Error(`YouTube API ${res.status} for channels: ${text}`);
58
- }
59
-
60
- const json = await res.json();
61
- if (json.error) {
62
- throw new Error(`YouTube API error: ${json.error.code} — ${json.error.message}`);
63
- }
64
-
65
- const item = json.items?.[0];
66
- if (!item) {
67
- throw new Error(`YouTube channels API returned no items for channelId=${channelId}`);
68
- }
69
-
70
- const stats = item.statistics || {};
71
- const result = {
72
- subscriberCount: parseInt(stats.subscriberCount || '0', 10),
73
- viewCount: parseInt(stats.viewCount || '0', 10),
74
- videoCount: parseInt(stats.videoCount || '0', 10),
75
- };
76
-
77
- console.log(
78
- `[youtube] Channel stats: subscribers=${result.subscriberCount} ` +
79
- `views=${result.viewCount} videos=${result.videoCount}`
80
- );
81
-
82
- return result;
83
- }
84
-
85
- /**
86
- * Fetch the most recent videos for a channel via the Search API.
87
- *
88
- * @param {string} apiKey - YouTube Data API v3 key
89
- * @param {string} channelId - YouTube channel ID
90
- * @param {number} [maxResults=20] - Maximum number of results (1–50)
91
- * @returns {Promise<Array<{ videoId: string, snippet: object }>>}
92
- */
93
- async function fetchRecentVideos(apiKey, channelId, maxResults) {
94
- if (!apiKey) throw new Error('apiKey is required');
95
- if (!channelId) throw new Error('channelId is required');
96
-
97
- const limit = maxResults || 20;
98
- const url =
99
- `${YOUTUBE_API_BASE}/search` +
100
- `?part=snippet` +
101
- `&channelId=${encodeURIComponent(channelId)}` +
102
- `&order=date` +
103
- `&type=video` +
104
- `&maxResults=${limit}` +
105
- `&key=${encodeURIComponent(apiKey)}`;
106
-
107
- console.log(`[youtube] Fetching recent videos: channelId=${channelId} maxResults=${limit}`);
108
-
109
- const res = await fetch(url);
110
- if (!res.ok) {
111
- const text = await res.text().catch(() => '');
112
- throw new Error(`YouTube API ${res.status} for search: ${text}`);
113
- }
114
-
115
- const json = await res.json();
116
- if (json.error) {
117
- throw new Error(`YouTube API error: ${json.error.code} — ${json.error.message}`);
118
- }
119
-
120
- const items = json.items ?? [];
121
- const videos = items.map((item) => ({
122
- videoId: item.id?.videoId || '',
123
- snippet: item.snippet || {},
124
- })).filter((v) => v.videoId);
125
-
126
- console.log(`[youtube] Retrieved ${videos.length} video IDs from search`);
127
- return videos;
128
- }
129
-
130
- /**
131
- * Fetch per-video statistics, content details, and snippet for a batch of video IDs.
132
- *
133
- * @param {string} apiKey - YouTube Data API v3 key
134
- * @param {string[]} videoIds - Array of YouTube video IDs (max 50 per request)
135
- * @returns {Promise<object[]>} Array of video resource objects with statistics, contentDetails, snippet
136
- */
137
- async function fetchVideoStats(apiKey, videoIds) {
138
- if (!apiKey) throw new Error('apiKey is required');
139
- if (!Array.isArray(videoIds) || videoIds.length === 0) return [];
140
-
141
- const ids = videoIds.join(',');
142
- const url =
143
- `${YOUTUBE_API_BASE}/videos` +
144
- `?part=statistics,contentDetails,snippet` +
145
- `&id=${encodeURIComponent(ids)}` +
146
- `&key=${encodeURIComponent(apiKey)}`;
147
-
148
- console.log(`[youtube] Fetching video stats for ${videoIds.length} video(s)`);
149
-
150
- const res = await fetch(url);
151
- if (!res.ok) {
152
- const text = await res.text().catch(() => '');
153
- throw new Error(`YouTube API ${res.status} for videos: ${text}`);
154
- }
155
-
156
- const json = await res.json();
157
- if (json.error) {
158
- throw new Error(`YouTube API error: ${json.error.code} — ${json.error.message}`);
159
- }
160
-
161
- const items = json.items ?? [];
162
-
163
- return items.map((item) => {
164
- const stats = item.statistics || {};
165
- const details = item.contentDetails || {};
166
- const snippet = item.snippet || {};
167
- const durationSeconds = parseDurationSeconds(details.duration || '');
168
- const isShort = durationSeconds > 0 && durationSeconds <= 60;
169
-
170
- return {
171
- videoId: item.id,
172
- duration: details.duration || null,
173
- durationSeconds,
174
- isShort,
175
- viewCount: parseInt(stats.viewCount || '0', 10),
176
- likeCount: parseInt(stats.likeCount || '0', 10),
177
- commentCount: parseInt(stats.commentCount || '0', 10),
178
- favoriteCount: parseInt(stats.favoriteCount || '0', 10),
179
- title: snippet.title || '',
180
- publishedAt: snippet.publishedAt || null,
181
- channelId: snippet.channelId || '',
182
- description: snippet.description || '',
183
- tags: snippet.tags || [],
184
- };
185
- });
186
- }
187
-
188
- /**
189
- * Main polling entry point.
190
- *
191
- * Fetches recent videos for the configured channel, retrieves per-video stats,
192
- * identifies Shorts by duration (<= 60s), normalizes each record, and upserts
193
- * into the analytics database. Also records a channel subscriber snapshot.
194
- *
195
- * @param {import('better-sqlite3').Database} db
196
- * @returns {Promise<void>}
197
- */
198
- async function pollYouTube(db) {
199
- const apiKey = process.env.YOUTUBE_API_KEY;
200
- if (!apiKey) {
201
- throw new Error('YOUTUBE_API_KEY environment variable is required');
202
- }
203
-
204
- const channelId = process.env.YOUTUBE_CHANNEL_ID;
205
- if (!channelId) {
206
- throw new Error('YOUTUBE_CHANNEL_ID environment variable is required');
207
- }
208
-
209
- // Lazy-require sibling modules so they can be built/tested independently.
210
- const { normalizeYouTubeMetric } = require('../normalizer');
211
- const { upsertMetric, upsertFollowerSnapshot } = require('../store');
212
-
213
- // Fetch channel stats and recent video list in parallel.
214
- const [channelStats, recentVideos] = await Promise.all([
215
- fetchChannelStats(apiKey, channelId),
216
- fetchRecentVideos(apiKey, channelId, 20),
217
- ]);
218
-
219
- const fetchedAt = new Date().toISOString();
220
- const today = fetchedAt.slice(0, 10);
221
-
222
- if (recentVideos.length > 0) {
223
- const videoIds = recentVideos.map((v) => v.videoId);
224
- const videoStats = await fetchVideoStats(apiKey, videoIds);
225
-
226
- // Build a lookup by videoId for O(1) access.
227
- const statsById = {};
228
- for (const stat of videoStats) {
229
- statsById[stat.videoId] = stat;
230
- }
231
-
232
- for (const { videoId, snippet } of recentVideos) {
233
- const stat = statsById[videoId] || {};
234
- const isShort = stat.isShort || false;
235
- const publishedAt = stat.publishedAt || snippet.publishedAt || null;
236
- const metricDate = publishedAt ? publishedAt.slice(0, 10) : today;
237
-
238
- const raw = {
239
- id: videoId,
240
- isShort,
241
- content_type: isShort ? 'short' : 'video',
242
- publishedAt,
243
- metric_date: metricDate,
244
- channelId,
245
- title: stat.title || snippet.title || '',
246
- duration: stat.duration || null,
247
- statistics: {
248
- viewCount: stat.viewCount ?? 0,
249
- likeCount: stat.likeCount ?? 0,
250
- commentCount: stat.commentCount ?? 0,
251
- favoriteCount: stat.favoriteCount ?? 0,
252
- },
253
- };
254
-
255
- const normalized = normalizeYouTubeMetric(raw);
256
- upsertMetric(db, normalized);
257
-
258
- console.log(
259
- `[youtube] Upserted ${isShort ? 'Short' : 'video'} id=${videoId} ` +
260
- `views=${stat.viewCount ?? 0} likes=${stat.likeCount ?? 0} ` +
261
- `comments=${stat.commentCount ?? 0} date=${metricDate}`
262
- );
263
- }
264
-
265
- console.log(`[youtube] Upserted ${recentVideos.length} video metric records`);
266
- } else {
267
- console.log('[youtube] No recent videos found');
268
- }
269
-
270
- // Record channel subscriber snapshot.
271
- const subscriberCount = channelStats.subscriberCount ?? 0;
272
- upsertFollowerSnapshot(db, {
273
- platform: 'youtube',
274
- follower_count: subscriberCount,
275
- snapshot_date: today,
276
- });
277
-
278
- console.log(
279
- `[youtube] Follower snapshot upserted: platform=youtube ` +
280
- `subscriber_count=${subscriberCount} date=${today}`
281
- );
282
- }
283
-
284
- module.exports = { fetchChannelStats, fetchRecentVideos, fetchVideoStats, pollYouTube };
285
-
286
- // ---------------------------------------------------------------------------
287
- // Stand-alone execution
288
- // ---------------------------------------------------------------------------
289
- if (require.main === module) {
290
- const { initDb } = require('../store');
291
-
292
- const db = initDb();
293
-
294
- pollYouTube(db)
295
- .then(() => {
296
- console.log('[youtube] Poll complete.');
297
- db.close();
298
- })
299
- .catch((err) => {
300
- console.error('[youtube] Poll failed:', err.message);
301
- db.close();
302
- process.exit(1);
303
- });
304
- }
@@ -1,183 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Zernio analytics poller.
5
- *
6
- * Fetches daily metrics and per-post analytics from the Zernio API,
7
- * normalizes them, and upserts into the local SQLite database.
8
- *
9
- * Required environment variables:
10
- * ZERNIO_API_KEY — Bearer token for https://zernio.com/api/v1
11
- */
12
-
13
- const { normalizeZernioMetric } = require('../normalizer');
14
- const { loadLocalEnv } = require('../load-env');
15
- const { upsertMetric, initDb } = require('../store');
16
- const { getConnectedAccounts } = require('../publishers/zernio');
17
-
18
- const ZERNIO_BASE = 'https://zernio.com/api/v1';
19
-
20
- loadLocalEnv();
21
-
22
- function requireApiKey() {
23
- const key = process.env.ZERNIO_API_KEY;
24
- if (!key) {
25
- throw new Error('ZERNIO_API_KEY environment variable is required');
26
- }
27
- return key;
28
- }
29
-
30
- async function zernioGet(endpoint) {
31
- const apiKey = requireApiKey();
32
- const url = `${ZERNIO_BASE}${endpoint}`;
33
-
34
- const res = await fetch(url, {
35
- method: 'GET',
36
- headers: {
37
- Authorization: `Bearer ${apiKey}`,
38
- 'Content-Type': 'application/json',
39
- },
40
- });
41
-
42
- if (!res.ok) {
43
- const errorText = await res.text().catch(() => '');
44
- throw new Error(`Zernio API ${res.status} for GET ${endpoint}: ${errorText}`);
45
- }
46
-
47
- return res.json();
48
- }
49
-
50
- /**
51
- * Fetches daily engagement metrics for a specific account.
52
- * @param {string} accountId
53
- * @returns {Promise<object>}
54
- */
55
- async function fetchDailyMetrics(accountId) {
56
- if (!accountId) throw new Error('fetchDailyMetrics: accountId is required');
57
- console.log(`[zernio:poller] Fetching daily metrics for account ${accountId}`);
58
- return zernioGet(`/analytics/daily-metrics?accountId=${encodeURIComponent(accountId)}`);
59
- }
60
-
61
- /**
62
- * Fetches per-post analytics for a specific post.
63
- * @param {string} postId
64
- * @returns {Promise<object>}
65
- */
66
- async function fetchPostAnalytics(postId) {
67
- if (!postId) throw new Error('fetchPostAnalytics: postId is required');
68
- console.log(`[zernio:poller] Fetching post analytics for post ${postId}`);
69
- return zernioGet(`/analytics?postId=${encodeURIComponent(postId)}`);
70
- }
71
-
72
- /**
73
- * Main entry point. Polls Zernio for daily metrics and per-post analytics,
74
- * normalizes the data, and upserts into the local SQLite database.
75
- * @param {import('better-sqlite3').Database} db
76
- */
77
- async function pollZernio(db) {
78
- requireApiKey();
79
-
80
- console.log('[zernio:poller] Starting Zernio analytics poll');
81
-
82
- let accounts;
83
- try {
84
- accounts = await getConnectedAccounts();
85
- } catch (err) {
86
- throw new Error(`[zernio:poller] Failed to fetch connected accounts: ${err.message}`);
87
- }
88
-
89
- if (!accounts || accounts.length === 0) {
90
- console.warn('[zernio:poller] No connected accounts found — skipping poll');
91
- return;
92
- }
93
-
94
- console.log(`[zernio:poller] Polling ${accounts.length} account(s)`);
95
-
96
- for (const account of accounts) {
97
- const accountId = account.accountId || account._id || account.id;
98
- const platform = account.platform;
99
-
100
- if (!accountId) {
101
- console.warn('[zernio:poller] Account missing accountId — skipping');
102
- continue;
103
- }
104
-
105
- try {
106
- const dailyResponse = await fetchDailyMetrics(accountId);
107
- const metrics = Array.isArray(dailyResponse)
108
- ? dailyResponse
109
- : (dailyResponse.data ?? dailyResponse.metrics ?? []);
110
-
111
- console.log(`[zernio:poller] Got ${metrics.length} daily metric(s) for account ${accountId}`);
112
-
113
- for (const rawMetric of metrics) {
114
- const enriched = {
115
- accountId,
116
- platform: rawMetric.platform || platform,
117
- ...rawMetric,
118
- };
119
-
120
- try {
121
- const normalized = normalizeZernioMetric(enriched);
122
- upsertMetric(db, normalized);
123
- console.log(`[zernio:poller] Upserted daily metric for post ${normalized.post_id} (${normalized.platform})`);
124
- } catch (normErr) {
125
- console.warn(`[zernio:poller] Normalization error for account ${accountId}: ${normErr.message}`);
126
- }
127
- }
128
-
129
- const postIds = metrics
130
- .map((m) => m.postId || m.id || m.platformPostId)
131
- .filter(Boolean);
132
-
133
- for (const postId of postIds) {
134
- try {
135
- const postResponse = await fetchPostAnalytics(postId);
136
- const postData = postResponse.data ?? postResponse;
137
-
138
- if (!postData || typeof postData !== 'object') continue;
139
-
140
- const enrichedPost = {
141
- accountId,
142
- platform: postData.platform || platform,
143
- ...postData,
144
- };
145
-
146
- try {
147
- const normalized = normalizeZernioMetric(enrichedPost);
148
- upsertMetric(db, normalized);
149
- console.log(`[zernio:poller] Upserted post analytics for post ${normalized.post_id}`);
150
- } catch (normErr) {
151
- console.warn(`[zernio:poller] Post analytics normalization error for post ${postId}: ${normErr.message}`);
152
- }
153
- } catch (postErr) {
154
- console.warn(`[zernio:poller] Post analytics fetch error for post ${postId}: ${postErr.message}`);
155
- }
156
- }
157
- } catch (err) {
158
- console.error(`[zernio:poller] Failed to poll account ${accountId}: ${err.message}`);
159
- }
160
- }
161
-
162
- console.log('[zernio:poller] Poll complete.');
163
- }
164
-
165
- module.exports = {
166
- pollZernio,
167
- fetchDailyMetrics,
168
- fetchPostAnalytics,
169
- };
170
-
171
- if (require.main === module) {
172
- (async () => {
173
- const db = initDb();
174
- try {
175
- await pollZernio(db);
176
- } catch (err) {
177
- console.error('[zernio:poller] Fatal error:', err.message);
178
- process.exit(1);
179
- } finally {
180
- db.close();
181
- }
182
- })();
183
- }
@@ -1,104 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * publish-instagram-thumbgate.js
6
- * Complete workflow: generate Instagram card image and post to Instagram via Zernio.
7
- *
8
- * Usage:
9
- * node publish-instagram-thumbgate.js [--image-only] [--post-only]
10
- *
11
- * Options:
12
- * --image-only Generate image only, don't post
13
- * --post-only Post an existing image without regenerating it
14
- */
15
-
16
- const path = require('path');
17
- const fs = require('node:fs');
18
- const { generateInstagramCard } = require('./generate-instagram-card');
19
- const { postThumbGateToInstagram, THUMBGATE_CAPTION } = require('./instagram-thumbgate-post');
20
-
21
- const REPO_ROOT = path.resolve(__dirname, '../..');
22
- const IMAGE_PATH = path.join(REPO_ROOT, '.thumbgate', 'instagram-card.png');
23
-
24
- async function publishInstagramThumbGate(options = {}) {
25
- const {
26
- caption = THUMBGATE_CAPTION,
27
- imageOnly = false,
28
- postOnly = false,
29
- imagePath = IMAGE_PATH,
30
- schedule = '',
31
- timezone = 'America/New_York',
32
- utm,
33
- } = options;
34
-
35
- try {
36
- // Step 1: Generate image (unless --post-only)
37
- if (!postOnly) {
38
- console.log('[workflow] Step 1: Generating Instagram card...');
39
- const generatedPath = await generateInstagramCard(imagePath);
40
- console.log(`[workflow] ✅ Image ready: ${generatedPath}`);
41
-
42
- if (imageOnly) {
43
- console.log('[workflow] Image-only mode. Stopping here.');
44
- return { imagePath: generatedPath };
45
- }
46
- } else if (!fs.existsSync(imagePath)) {
47
- throw new Error(`Image file is required for --post-only mode: ${imagePath}`);
48
- }
49
-
50
- // Step 2: Post to Instagram (unless --image-only)
51
- if (!imageOnly) {
52
- console.log('[workflow] Step 2: Publishing to Instagram via Zernio...');
53
- const postResult = await postThumbGateToInstagram({
54
- caption,
55
- imagePath,
56
- schedule,
57
- timezone,
58
- utm,
59
- });
60
- if (schedule) {
61
- console.log(`[workflow] ✅ Post scheduled: ${postResult.id || postResult.data?.id}`);
62
- } else {
63
- console.log(`[workflow] ✅ Post published: ${postResult.id || postResult.data?.id}`);
64
- }
65
-
66
- return {
67
- success: true,
68
- imagePath: postOnly ? undefined : imagePath,
69
- postId: postResult.id || postResult.data?.id,
70
- scheduled: Boolean(schedule),
71
- scheduledFor: schedule || undefined,
72
- };
73
- }
74
- } catch (err) {
75
- console.error(`[workflow] ❌ Failed: ${err.message}`);
76
- throw err;
77
- }
78
- }
79
-
80
- // CLI execution
81
- if (require.main === module) {
82
- const args = process.argv.slice(2);
83
- const imageOnly = args.includes('--image-only');
84
- const postOnly = args.includes('--post-only');
85
-
86
- if (imageOnly && postOnly) {
87
- console.error('❌ Cannot specify both --image-only and --post-only');
88
- process.exit(1);
89
- }
90
-
91
- (async () => {
92
- try {
93
- const result = await publishInstagramThumbGate({ imageOnly, postOnly });
94
- console.log('\n✅ Workflow complete!');
95
- console.log(JSON.stringify(result, null, 2));
96
- process.exit(0);
97
- } catch (err) {
98
- console.error(`\n❌ Workflow failed: ${err.message}`);
99
- process.exit(1);
100
- }
101
- })();
102
- }
103
-
104
- module.exports = { publishInstagramThumbGate, IMAGE_PATH };