thumbgate 0.9.9 → 0.9.11

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 (160) hide show
  1. package/.claude-plugin/README.md +4 -4
  2. package/.claude-plugin/marketplace.json +4 -2
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +115 -312
  6. package/adapters/README.md +2 -2
  7. package/adapters/amp/skills/{rlhf-feedback → thumbgate-feedback}/SKILL.md +1 -1
  8. package/adapters/chatgpt/openapi.yaml +2 -2
  9. package/adapters/claude/.mcp.json +3 -3
  10. package/adapters/codex/config.toml +4 -4
  11. package/adapters/gemini/function-declarations.json +1 -1
  12. package/adapters/mcp/server-stdio.js +66 -6
  13. package/adapters/opencode/opencode.json +4 -2
  14. package/bin/cli.js +188 -39
  15. package/config/e2e-critical-flows.json +4 -0
  16. package/config/gates/default.json +74 -2
  17. package/config/github-about.json +1 -1
  18. package/config/mcp-allowlists.json +33 -6
  19. package/config/skill-packs/react-testing.json +1 -1
  20. package/config/tessl-tiles.json +3 -3
  21. package/openapi/openapi.yaml +2 -2
  22. package/package.json +23 -9
  23. package/plugins/amp-skill/INSTALL.md +3 -2
  24. package/plugins/amp-skill/SKILL.md +1 -0
  25. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude-codex-bridge/.mcp.json +5 -3
  27. package/plugins/claude-codex-bridge/README.md +1 -1
  28. package/plugins/claude-codex-bridge/skills/setup/SKILL.md +1 -1
  29. package/plugins/claude-skill/INSTALL.md +4 -3
  30. package/plugins/claude-skill/SKILL.md +1 -1
  31. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  32. package/plugins/codex-profile/.mcp.json +5 -3
  33. package/plugins/codex-profile/INSTALL.md +2 -2
  34. package/plugins/codex-profile/README.md +1 -1
  35. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  36. package/plugins/cursor-marketplace/README.md +5 -5
  37. package/plugins/cursor-marketplace/mcp.json +4 -2
  38. package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +1 -1
  39. package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
  40. package/plugins/gemini-extension/INSTALL.md +4 -4
  41. package/plugins/opencode-profile/INSTALL.md +5 -5
  42. package/public/dashboard.html +15 -8
  43. package/public/index.html +134 -375
  44. package/public/js/buyer-intent.js +252 -0
  45. package/public/pro.html +1085 -0
  46. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  47. package/scripts/adk-consolidator.js +17 -5
  48. package/scripts/agent-readiness.js +3 -1
  49. package/scripts/agent-security-hardening.js +4 -4
  50. package/scripts/auto-promote-gates.js +8 -0
  51. package/scripts/auto-wire-hooks.js +105 -21
  52. package/scripts/billing.js +111 -7
  53. package/scripts/build-metadata.js +14 -0
  54. package/scripts/check-congruence.js +1 -1
  55. package/scripts/context-engine.js +2 -1
  56. package/scripts/daemon-manager.js +2 -2
  57. package/scripts/dashboard.js +2 -2
  58. package/scripts/data-governance.js +1 -1
  59. package/scripts/deploy-gcp.sh +1 -1
  60. package/scripts/deploy-policy.js +22 -4
  61. package/scripts/dispatch-brief.js +1 -1
  62. package/scripts/ensure-repo-bootstrap.js +1 -1
  63. package/scripts/feedback-attribution.js +22 -10
  64. package/scripts/feedback-fallback.js +3 -2
  65. package/scripts/feedback-inbox-read.js +1 -1
  66. package/scripts/feedback-loop.js +41 -3
  67. package/scripts/feedback-paths.js +8 -8
  68. package/scripts/feedback-schema.js +1 -1
  69. package/scripts/feedback-to-memory.js +2 -2
  70. package/scripts/filesystem-search.js +2 -2
  71. package/scripts/gates-engine.js +765 -34
  72. package/scripts/generate-paperbanana-diagrams.sh +3 -3
  73. package/scripts/github-about.js +1 -1
  74. package/scripts/gtm-revenue-loop.js +20 -1
  75. package/scripts/hook-runtime.js +89 -0
  76. package/scripts/hook-stop-self-score.sh +3 -3
  77. package/scripts/hook-thumbgate-cache-updater.js +98 -37
  78. package/scripts/hosted-config.js +12 -10
  79. package/scripts/hybrid-feedback-context.js +54 -13
  80. package/scripts/install-mcp.js +14 -1
  81. package/scripts/intent-router.js +1 -1
  82. package/scripts/internal-agent-bootstrap.js +1 -1
  83. package/scripts/lesson-inference.js +6 -1
  84. package/scripts/license.js +54 -16
  85. package/scripts/mcp-config.js +69 -7
  86. package/scripts/memory-migration.js +1 -1
  87. package/scripts/money-watcher.js +166 -16
  88. package/scripts/operational-integrity.js +480 -0
  89. package/scripts/optimize-context.js +1 -1
  90. package/scripts/perplexity-marketing.js +1 -1
  91. package/scripts/post-everywhere.js +7 -12
  92. package/scripts/post-to-x.js +1 -1
  93. package/scripts/pr-manager.js +14 -11
  94. package/scripts/problem-detail.js +10 -10
  95. package/scripts/profile-router.js +2 -0
  96. package/scripts/prompt-dlp.js +1 -0
  97. package/scripts/prove-adapters.js +6 -6
  98. package/scripts/prove-automation.js +1 -1
  99. package/scripts/prove-autoresearch.js +1 -1
  100. package/scripts/prove-claim-verification.js +3 -3
  101. package/scripts/prove-data-pipeline.js +5 -5
  102. package/scripts/prove-data-quality.js +1 -1
  103. package/scripts/prove-evolution.js +7 -7
  104. package/scripts/prove-harnesses.js +2 -2
  105. package/scripts/prove-lancedb.js +2 -2
  106. package/scripts/prove-local-intelligence.js +1 -1
  107. package/scripts/prove-loop-closure.js +1 -1
  108. package/scripts/prove-predictive-insights.js +2 -2
  109. package/scripts/prove-runtime.js +6 -6
  110. package/scripts/prove-seo-gsd.js +1 -1
  111. package/scripts/prove-settings.js +4 -4
  112. package/scripts/prove-subway-upgrades.js +1 -1
  113. package/scripts/prove-tessl.js +2 -2
  114. package/scripts/prove-xmemory.js +2 -2
  115. package/scripts/publish-decision.js +10 -0
  116. package/scripts/published-cli.js +34 -0
  117. package/scripts/rate-limiter.js +2 -2
  118. package/scripts/reddit-monitor-cron.sh +2 -2
  119. package/scripts/reminder-engine.js +1 -1
  120. package/scripts/schedule-manager.js +3 -3
  121. package/scripts/self-healing-check.js +1 -1
  122. package/scripts/shieldcortex-memory-firewall-runner.mjs +1 -1
  123. package/scripts/skill-quality-tracker.js +1 -1
  124. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  125. package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
  126. package/scripts/social-analytics/engagement-audit.js +202 -0
  127. package/scripts/social-analytics/generate-instagram-card.js +1 -1
  128. package/scripts/social-analytics/instagram-thumbgate-post.js +5 -1
  129. package/scripts/social-analytics/install-growth-automation.js +114 -0
  130. package/scripts/social-analytics/publish-instagram-thumbgate.js +8 -2
  131. package/scripts/social-analytics/publish-thumbgate-launch.js +1 -1
  132. package/scripts/social-analytics/publishers/reddit.js +7 -12
  133. package/scripts/social-analytics/publishers/zernio.js +19 -0
  134. package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
  135. package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
  136. package/scripts/social-analytics/sync-launch-assets.js +185 -0
  137. package/scripts/social-pipeline.js +2 -2
  138. package/scripts/social-post-hourly.js +185 -0
  139. package/scripts/social-quality-gate.js +119 -3
  140. package/scripts/social-reply-monitor.js +150 -34
  141. package/scripts/statusline-cache-path.js +27 -0
  142. package/scripts/statusline-meta.js +22 -0
  143. package/scripts/statusline.sh +24 -32
  144. package/scripts/sync-version.js +24 -12
  145. package/scripts/telemetry-analytics.js +4 -4
  146. package/scripts/tessl-export.js +1 -1
  147. package/scripts/test-coverage.js +20 -13
  148. package/scripts/thumbgate-search.js +2 -2
  149. package/scripts/tool-registry.js +98 -1
  150. package/scripts/train_from_feedback.py +1 -1
  151. package/scripts/user-profile.js +4 -4
  152. package/scripts/validate-feedback.js +1 -1
  153. package/scripts/vector-store.js +1 -1
  154. package/scripts/verification-loop.js +1 -1
  155. package/scripts/verify-run.js +1 -1
  156. package/scripts/weekly-auto-post.js +1 -1
  157. package/skills/{rlhf-feedback → thumbgate-feedback}/SKILL.md +1 -1
  158. package/src/api/server.js +291 -41
  159. package/scripts/__pycache__/train_from_feedback.cpython-314.pyc +0 -0
  160. package/scripts/social-analytics/db/social-analytics.db +0 -0
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { publishInstagramThumbGate } = require('./publish-instagram-thumbgate');
7
+ const {
8
+ DEFAULT_TIMEZONE,
9
+ buildCampaignEntries,
10
+ defaultCampaignSchedule,
11
+ } = require('./publish-thumbgate-launch');
12
+ const {
13
+ getConnectedAccounts,
14
+ groupAccountsByPlatform,
15
+ schedulePost,
16
+ } = require('./publishers/zernio');
17
+
18
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
19
+ const DEFAULT_STATE_PATH = path.join(REPO_ROOT, '.thumbgate', 'social-campaign-schedule-state.json');
20
+
21
+ function parseArgs(argv = []) {
22
+ const options = {
23
+ dryRun: false,
24
+ platforms: [],
25
+ scheduleTimes: [],
26
+ statePath: DEFAULT_STATE_PATH,
27
+ timezone: DEFAULT_TIMEZONE,
28
+ };
29
+
30
+ for (let index = 0; index < argv.length; index += 1) {
31
+ const token = argv[index];
32
+ if (token === '--dry-run') {
33
+ options.dryRun = true;
34
+ continue;
35
+ }
36
+
37
+ if (token.startsWith('--platforms=')) {
38
+ options.platforms = token.slice('--platforms='.length).split(',').map((value) => value.trim()).filter(Boolean);
39
+ continue;
40
+ }
41
+
42
+ if (token === '--platforms' && argv[index + 1]) {
43
+ options.platforms = String(argv[index + 1]).split(',').map((value) => value.trim()).filter(Boolean);
44
+ index += 1;
45
+ continue;
46
+ }
47
+
48
+ if (token.startsWith('--times=')) {
49
+ options.scheduleTimes = token.slice('--times='.length).split(',').map((value) => value.trim()).filter(Boolean);
50
+ continue;
51
+ }
52
+
53
+ if (token === '--times' && argv[index + 1]) {
54
+ options.scheduleTimes = String(argv[index + 1]).split(',').map((value) => value.trim()).filter(Boolean);
55
+ index += 1;
56
+ continue;
57
+ }
58
+
59
+ if (token.startsWith('--timezone=')) {
60
+ options.timezone = token.slice('--timezone='.length).trim() || DEFAULT_TIMEZONE;
61
+ continue;
62
+ }
63
+
64
+ if (token.startsWith('--state-path=')) {
65
+ options.statePath = token.slice('--state-path='.length).trim() || DEFAULT_STATE_PATH;
66
+ continue;
67
+ }
68
+
69
+ if (token === '--timezone' && argv[index + 1]) {
70
+ options.timezone = String(argv[index + 1]).trim() || DEFAULT_TIMEZONE;
71
+ index += 1;
72
+ continue;
73
+ }
74
+
75
+ if (token === '--state-path' && argv[index + 1]) {
76
+ options.statePath = String(argv[index + 1]).trim() || DEFAULT_STATE_PATH;
77
+ index += 1;
78
+ }
79
+ }
80
+
81
+ return options;
82
+ }
83
+
84
+ function readScheduleState(statePath = DEFAULT_STATE_PATH) {
85
+ if (!fs.existsSync(statePath)) {
86
+ return { scheduled: {} };
87
+ }
88
+
89
+ try {
90
+ return JSON.parse(fs.readFileSync(statePath, 'utf8'));
91
+ } catch {
92
+ return { scheduled: {} };
93
+ }
94
+ }
95
+
96
+ function writeScheduleState(statePath = DEFAULT_STATE_PATH, state = { scheduled: {} }) {
97
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
98
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`);
99
+ }
100
+
101
+ function buildScheduleKey({ slug, platform, scheduledFor }) {
102
+ return `${scheduledFor}::${slug}::${platform}`;
103
+ }
104
+
105
+ async function scheduleCampaign(options = {}, api = {}) {
106
+ const scheduleApi = {
107
+ getConnectedAccounts: api.getConnectedAccounts || getConnectedAccounts,
108
+ groupAccountsByPlatform: api.groupAccountsByPlatform || groupAccountsByPlatform,
109
+ publishInstagramThumbGate: api.publishInstagramThumbGate || publishInstagramThumbGate,
110
+ schedulePost: api.schedulePost || schedulePost,
111
+ };
112
+
113
+ const campaignEntries = buildCampaignEntries();
114
+ const scheduleTimes = options.scheduleTimes && options.scheduleTimes.length > 0
115
+ ? options.scheduleTimes
116
+ : defaultCampaignSchedule();
117
+ const platforms = options.platforms && options.platforms.length > 0
118
+ ? options.platforms
119
+ : ['twitter', 'linkedin', 'instagram'];
120
+ const statePath = options.statePath || DEFAULT_STATE_PATH;
121
+ const state = readScheduleState(statePath);
122
+ const scheduledState = state.scheduled && typeof state.scheduled === 'object' ? state.scheduled : {};
123
+ const accounts = await scheduleApi.getConnectedAccounts();
124
+ const groupedAccounts = scheduleApi.groupAccountsByPlatform(accounts);
125
+ const results = {
126
+ dryRun: options.dryRun === true,
127
+ statePath,
128
+ timezone: options.timezone || DEFAULT_TIMEZONE,
129
+ scheduleTimes,
130
+ platforms,
131
+ scheduled: [],
132
+ skipped: [],
133
+ errors: [],
134
+ };
135
+
136
+ for (let index = 0; index < campaignEntries.length; index += 1) {
137
+ const entry = campaignEntries[index];
138
+ const scheduledFor = scheduleTimes[index];
139
+ if (!scheduledFor) {
140
+ results.skipped.push({ slug: entry.slug, reason: 'missing_schedule_time' });
141
+ continue;
142
+ }
143
+
144
+ for (const platform of platforms) {
145
+ const normalizedPlatform = String(platform || '').trim().toLowerCase();
146
+ const platformAccounts = groupedAccounts.get(normalizedPlatform) || [];
147
+ if (platformAccounts.length === 0) {
148
+ results.skipped.push({ slug: entry.slug, platform: normalizedPlatform, reason: 'not_connected' });
149
+ continue;
150
+ }
151
+
152
+ const content = entry.posts[normalizedPlatform];
153
+ if (!content) {
154
+ results.skipped.push({ slug: entry.slug, platform: normalizedPlatform, reason: 'no_content' });
155
+ continue;
156
+ }
157
+
158
+ const scheduleKey = buildScheduleKey({
159
+ slug: entry.slug,
160
+ platform: normalizedPlatform,
161
+ scheduledFor,
162
+ });
163
+ if (!results.dryRun && scheduledState[scheduleKey]) {
164
+ results.skipped.push({
165
+ slug: entry.slug,
166
+ platform: normalizedPlatform,
167
+ reason: 'already_scheduled',
168
+ scheduledFor,
169
+ existing: scheduledState[scheduleKey],
170
+ });
171
+ continue;
172
+ }
173
+
174
+ if (results.dryRun) {
175
+ results.scheduled.push({
176
+ slug: entry.slug,
177
+ platform: normalizedPlatform,
178
+ scheduledFor,
179
+ dryRun: true,
180
+ content,
181
+ });
182
+ continue;
183
+ }
184
+
185
+ const utm = {
186
+ source: normalizedPlatform === 'twitter' ? 'x' : normalizedPlatform,
187
+ medium: 'organic_social',
188
+ campaign: 'first_customer_push',
189
+ };
190
+
191
+ try {
192
+ if (normalizedPlatform === 'instagram') {
193
+ const scheduledResult = await scheduleApi.publishInstagramThumbGate({
194
+ caption: content,
195
+ schedule: scheduledFor,
196
+ timezone: options.timezone || DEFAULT_TIMEZONE,
197
+ utm,
198
+ });
199
+ scheduledState[scheduleKey] = {
200
+ id: scheduledResult.postId || scheduledResult.id || null,
201
+ scheduledFor,
202
+ slug: entry.slug,
203
+ platform: normalizedPlatform,
204
+ recordedAt: new Date().toISOString(),
205
+ };
206
+ results.scheduled.push({
207
+ slug: entry.slug,
208
+ platform: normalizedPlatform,
209
+ scheduledFor,
210
+ result: scheduledResult,
211
+ });
212
+ continue;
213
+ }
214
+
215
+ const scheduledResult = await scheduleApi.schedulePost(
216
+ content,
217
+ platformAccounts,
218
+ scheduledFor,
219
+ options.timezone || DEFAULT_TIMEZONE,
220
+ { utm }
221
+ );
222
+ scheduledState[scheduleKey] = {
223
+ id: scheduledResult.id || scheduledResult.post?._id || null,
224
+ scheduledFor,
225
+ slug: entry.slug,
226
+ platform: normalizedPlatform,
227
+ recordedAt: new Date().toISOString(),
228
+ };
229
+ results.scheduled.push({
230
+ slug: entry.slug,
231
+ platform: normalizedPlatform,
232
+ scheduledFor,
233
+ result: scheduledResult,
234
+ });
235
+ } catch (error) {
236
+ results.errors.push({
237
+ slug: entry.slug,
238
+ platform: normalizedPlatform,
239
+ error: error && error.message ? error.message : String(error),
240
+ });
241
+ }
242
+ }
243
+ }
244
+
245
+ if (!results.dryRun) {
246
+ writeScheduleState(statePath, { scheduled: scheduledState });
247
+ }
248
+
249
+ return results;
250
+ }
251
+
252
+ async function main() {
253
+ const options = parseArgs(process.argv.slice(2));
254
+ const results = await scheduleCampaign(options);
255
+ process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
256
+ if (results.errors.length > 0) {
257
+ process.exitCode = 1;
258
+ }
259
+ }
260
+
261
+ if (require.main === module) {
262
+ main().catch((error) => {
263
+ console.error(error && error.message ? error.message : error);
264
+ process.exit(1);
265
+ });
266
+ }
267
+
268
+ module.exports = {
269
+ buildScheduleKey,
270
+ DEFAULT_STATE_PATH,
271
+ parseArgs,
272
+ readScheduleState,
273
+ scheduleCampaign,
274
+ writeScheduleState,
275
+ };
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const {
7
+ listPosts,
8
+ } = require('./publishers/zernio');
9
+
10
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
11
+ const DEFAULT_STATE_PATH = path.join(REPO_ROOT, '.thumbgate', 'social-launch-assets.json');
12
+
13
+ const LAUNCH_MARKERS = {
14
+ twitter: 'launch_post_twitter',
15
+ linkedin: 'launch_post_linkedin',
16
+ instagram: 'launch_post_instagram',
17
+ reddit: 'launch_post_reddit',
18
+ };
19
+
20
+ const CAMPAIGN_MARKERS = {
21
+ proof_pack: 'campaign_proof_pack',
22
+ free_local: 'campaign_free_local',
23
+ checkout_path: 'campaign_checkout_path',
24
+ };
25
+
26
+ function parseArgs(argv = []) {
27
+ const options = {
28
+ limit: 50,
29
+ statePath: DEFAULT_STATE_PATH,
30
+ };
31
+
32
+ for (let index = 0; index < argv.length; index += 1) {
33
+ const token = String(argv[index] || '').trim();
34
+ if (token.startsWith('--limit=')) {
35
+ options.limit = Number.parseInt(token.slice('--limit='.length), 10) || options.limit;
36
+ continue;
37
+ }
38
+ if (token === '--limit' && argv[index + 1]) {
39
+ options.limit = Number.parseInt(String(argv[index + 1]), 10) || options.limit;
40
+ index += 1;
41
+ continue;
42
+ }
43
+ if (token.startsWith('--state-path=')) {
44
+ options.statePath = token.slice('--state-path='.length).trim() || DEFAULT_STATE_PATH;
45
+ continue;
46
+ }
47
+ if (token === '--state-path' && argv[index + 1]) {
48
+ options.statePath = String(argv[index + 1]).trim() || DEFAULT_STATE_PATH;
49
+ index += 1;
50
+ }
51
+ }
52
+
53
+ return options;
54
+ }
55
+
56
+ function normalizePlatform(post = {}) {
57
+ return String(post?.platforms?.[0]?.platform || '').trim().toLowerCase();
58
+ }
59
+
60
+ function extractMarker(post = {}) {
61
+ const content = String(post.content || '');
62
+
63
+ for (const marker of Object.values(LAUNCH_MARKERS)) {
64
+ if (content.includes(`utm_content=${marker}`)) {
65
+ return marker;
66
+ }
67
+ }
68
+
69
+ for (const marker of Object.values(CAMPAIGN_MARKERS)) {
70
+ if (content.includes(`utm_content=${marker}`)) {
71
+ return marker;
72
+ }
73
+ }
74
+
75
+ return '';
76
+ }
77
+
78
+ function toTimestamp(post = {}) {
79
+ return new Date(post.createdAt || post.updatedAt || post.scheduledFor || 0).getTime();
80
+ }
81
+
82
+ function selectNewestPost(posts = []) {
83
+ return [...posts].sort((left, right) => toTimestamp(right) - toTimestamp(left))[0] || null;
84
+ }
85
+
86
+ function summarizePost(post = {}, marker = '') {
87
+ return {
88
+ id: post._id || post.id || null,
89
+ platform: normalizePlatform(post),
90
+ status: post.status || null,
91
+ marker,
92
+ createdAt: post.createdAt || null,
93
+ updatedAt: post.updatedAt || null,
94
+ scheduledFor: post.scheduledFor || null,
95
+ content: post.content || '',
96
+ };
97
+ }
98
+
99
+ function buildLaunchAssetState(posts = []) {
100
+ const state = {
101
+ updatedAt: new Date().toISOString(),
102
+ launchPosts: {},
103
+ campaignPosts: {},
104
+ };
105
+
106
+ for (const [platform, marker] of Object.entries(LAUNCH_MARKERS)) {
107
+ const matching = posts.filter((post) => extractMarker(post) === marker);
108
+ const selected = selectNewestPost(matching);
109
+ if (selected) {
110
+ state.launchPosts[platform] = summarizePost(selected, marker);
111
+ }
112
+ }
113
+
114
+ for (const [slug, marker] of Object.entries(CAMPAIGN_MARKERS)) {
115
+ const matching = posts.filter((post) => extractMarker(post) === marker);
116
+ if (matching.length === 0) {
117
+ continue;
118
+ }
119
+
120
+ const byPlatform = {};
121
+ for (const post of matching) {
122
+ const platform = normalizePlatform(post);
123
+ if (!platform) continue;
124
+ const existing = byPlatform[platform];
125
+ if (!existing || toTimestamp(post) > toTimestamp(existing)) {
126
+ byPlatform[platform] = post;
127
+ }
128
+ }
129
+
130
+ state.campaignPosts[slug] = {};
131
+ for (const [platform, post] of Object.entries(byPlatform)) {
132
+ state.campaignPosts[slug][platform] = summarizePost(post, marker);
133
+ }
134
+ }
135
+
136
+ return state;
137
+ }
138
+
139
+ function writeLaunchAssetState(statePath = DEFAULT_STATE_PATH, state = {}) {
140
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
141
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
142
+ return statePath;
143
+ }
144
+
145
+ async function syncLaunchAssets(options = {}, api = {}) {
146
+ const zernio = {
147
+ listPosts: api.listPosts || listPosts,
148
+ };
149
+
150
+ const posts = await zernio.listPosts({ limit: options.limit || 50 });
151
+ const state = buildLaunchAssetState(Array.isArray(posts) ? posts : []);
152
+ const statePath = options.statePath || DEFAULT_STATE_PATH;
153
+ writeLaunchAssetState(statePath, state);
154
+ return {
155
+ statePath,
156
+ launchCount: Object.keys(state.launchPosts).length,
157
+ campaignCount: Object.keys(state.campaignPosts).length,
158
+ state,
159
+ };
160
+ }
161
+
162
+ if (require.main === module) {
163
+ syncLaunchAssets(parseArgs(process.argv.slice(2)))
164
+ .then((result) => {
165
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
166
+ })
167
+ .catch((error) => {
168
+ console.error(error && error.message ? error.message : error);
169
+ process.exit(1);
170
+ });
171
+ }
172
+
173
+ module.exports = {
174
+ CAMPAIGN_MARKERS,
175
+ DEFAULT_STATE_PATH,
176
+ LAUNCH_MARKERS,
177
+ buildLaunchAssetState,
178
+ extractMarker,
179
+ normalizePlatform,
180
+ parseArgs,
181
+ selectNewestPost,
182
+ summarizePost,
183
+ syncLaunchAssets,
184
+ writeLaunchAssetState,
185
+ };
@@ -25,8 +25,8 @@ const DEFAULT_CAPTION_PATH = path.join(
25
25
  'pre-action-gates-caption.txt'
26
26
  );
27
27
  const DEFAULT_OUTPUT_ROOT = path.join(REPO_ROOT, '.artifacts', 'social');
28
- const DEFAULT_QUEUE_PATH = path.join(REPO_ROOT, '.rlhf', 'social-post-queue.json');
29
- const DEFAULT_HISTORY_PATH = path.join(REPO_ROOT, '.rlhf', 'social-post-history.jsonl');
28
+ const DEFAULT_QUEUE_PATH = path.join(REPO_ROOT, '.thumbgate', 'social-post-queue.json');
29
+ const DEFAULT_HISTORY_PATH = path.join(REPO_ROOT, '.thumbgate', 'social-post-history.jsonl');
30
30
  const DEFAULT_LAUNCHD_LABEL = 'io.github.IgorGanapolsky.thumbgate.social';
31
31
  const DEFAULT_SCHEDULE_INTERVAL_MINUTES = 15;
32
32
  const DEFAULT_CHROME_PROFILE_ROOT = path.join(
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * social-post-hourly.js → now "social-post-daily.js" in practice
6
+ *
7
+ * Generates ONE quality social post per day and publishes via Zernio
8
+ * to LinkedIn, X/Twitter, and TikTok (text-friendly platforms).
9
+ *
10
+ * Strategy based on research of top SaaS companies (Linear, Vercel, Supabase,
11
+ * PostHog, Cursor, Raycast, Cal.com):
12
+ * - 1 post/day, not 24. Quality over volume.
13
+ * - Rotate 7 content angles across the week (not 4 recycled hourly).
14
+ * - Content ratio: 30% educational, 25% product demo, 25% community, 20% hot takes.
15
+ * - NO Reddit auto-posting (ban risk). Reddit engagement via reply-monitor only.
16
+ * - NO Dev.to auto-posting (counterproductive at high volume).
17
+ *
18
+ * Runs daily via CI (.github/workflows/social-engagement-hourly.yml at 2pm UTC).
19
+ *
20
+ * Usage:
21
+ * node scripts/social-post-hourly.js # publish for real
22
+ * node scripts/social-post-hourly.js --dry-run # preview only
23
+ */
24
+
25
+ require('dotenv').config();
26
+
27
+ const { generateWeeklyStatsPost } = require('./daily-digest');
28
+ const { publishPost, getConnectedAccounts } = require('./social-analytics/publishers/zernio');
29
+
30
+ // Platforms that support text-only posts.
31
+ // Reddit EXCLUDED — engagement only via reply-monitor, not auto-posting.
32
+ // Instagram EXCLUDED — requires media.
33
+ // TikTok EXCLUDED — requires video.
34
+ const TEXT_PLATFORMS = new Set(['linkedin', 'twitter']);
35
+
36
+ // 7 angles, one per day of the week (Monday=0 through Sunday=6)
37
+ // Ratio: 2 educational, 2 product, 2 hot-take/community, 1 tip
38
+ const DAILY_ANGLES = [
39
+ 'horror-story', // Monday: "This AI PR would have broken production"
40
+ 'educational', // Tuesday: Teach a concept (context engineering, gate patterns)
41
+ 'product-demo', // Wednesday: Specific feature highlight with concrete example
42
+ 'hot-take', // Thursday: Contrarian opinion about AI coding agents
43
+ 'community', // Friday: Highlight a user, contributor, or community discussion
44
+ 'tip', // Saturday: Quick actionable tip
45
+ 'stats', // Sunday: Weekly build-in-public numbers
46
+ ];
47
+
48
+ function getTodayAngle() {
49
+ const day = new Date().getUTCDay(); // 0=Sun, 1=Mon, ..., 6=Sat
50
+ // Remap: Mon=0, Tue=1, ..., Sun=6
51
+ const idx = day === 0 ? 6 : day - 1;
52
+ return DAILY_ANGLES[idx];
53
+ }
54
+
55
+ function generatePost(angle) {
56
+ const { post, stats } = generateWeeklyStatsPost({ periodDays: 1 });
57
+ const REPO = 'https://github.com/IgorGanapolsky/ThumbGate';
58
+
59
+ switch (angle) {
60
+ case 'horror-story': {
61
+ const gate = stats.topGate || 'force-push prevention';
62
+ return [
63
+ `A Claude Code agent tried to force-push to main today.`,
64
+ '',
65
+ `The "${gate}" gate caught it before execution. No rollback needed, no incident, no Slack panic.`,
66
+ '',
67
+ `Pre-action gates > post-mortem reviews.`,
68
+ '',
69
+ `ThumbGate is open source: ${REPO}`,
70
+ ].join('\n');
71
+ }
72
+
73
+ case 'educational':
74
+ return [
75
+ 'Context engineering vs prompt engineering for AI agents:',
76
+ '',
77
+ 'Prompt engineering: "Please don\'t force-push to main"',
78
+ 'Context engineering: Agent physically cannot force-push because a gate blocks it',
79
+ '',
80
+ 'One is a suggestion. The other is enforcement.',
81
+ '',
82
+ 'The agents that work reliably in production use both — but enforcement is what prevents the 2am incidents.',
83
+ ].join('\n');
84
+
85
+ case 'product-demo':
86
+ return [
87
+ 'How ThumbGate works in 30 seconds:',
88
+ '',
89
+ '1. Agent tries to run a tool call',
90
+ '2. PreToolUse hook intercepts it',
91
+ '3. Call is checked against prevention rules',
92
+ '4. If it matches a known-bad pattern → blocked',
93
+ '5. Agent tries a different approach',
94
+ '',
95
+ 'Rules are generated from your thumbs-down feedback. The system learns from your corrections.',
96
+ '',
97
+ `Try it: npx thumbgate init`,
98
+ ].join('\n');
99
+
100
+ case 'hot-take':
101
+ return [
102
+ 'Unpopular opinion: CLAUDE.md files are not enough to make AI agents reliable.',
103
+ '',
104
+ 'Instructions in markdown are suggestions. The agent can ignore them after context compaction, hallucinate past them, or just decide they don\'t apply.',
105
+ '',
106
+ 'You need enforcement — gates that physically block bad actions before execution.',
107
+ '',
108
+ 'Memory helps agents remember. Gates make them comply.',
109
+ ].join('\n');
110
+
111
+ case 'community':
112
+ return [
113
+ `This week's most common agent mistake caught by ThumbGate users:`,
114
+ '',
115
+ `Agents trying to commit .env files to public repos.`,
116
+ '',
117
+ `It's such a common pattern that we made it a default gate. Works across Claude Code, Cursor, and Codex.`,
118
+ '',
119
+ `What's the most dangerous thing your AI agent has tried to do? Genuinely curious.`,
120
+ ].join('\n');
121
+
122
+ case 'tip':
123
+ return [
124
+ 'Quick tip for Claude Code users:',
125
+ '',
126
+ 'Add a PreToolUse hook that checks for `git push --force` before every Bash call.',
127
+ '',
128
+ 'One line of prevention saves hours of rollback.',
129
+ '',
130
+ `ThumbGate automates this — generates hooks from your feedback: ${REPO}`,
131
+ ].join('\n');
132
+
133
+ case 'stats':
134
+ return post; // Use the generated weekly stats
135
+
136
+ default:
137
+ return post;
138
+ }
139
+ }
140
+
141
+ async function main() {
142
+ const dryRun = process.argv.includes('--dry-run');
143
+ const angle = getTodayAngle();
144
+ const content = generatePost(angle);
145
+
146
+ console.log(`[daily-post] Day: ${new Date().toUTCString()}`);
147
+ console.log(`[daily-post] Angle: ${angle}`);
148
+ console.log(`[daily-post] Content:\n${content}\n`);
149
+
150
+ if (dryRun) {
151
+ console.log('[daily-post] Dry run — not posting.');
152
+ return;
153
+ }
154
+
155
+ // Fetch connected accounts, filter to text-friendly platforms (no Reddit, no Instagram)
156
+ const accounts = await getConnectedAccounts();
157
+ const textAccounts = accounts
158
+ .filter(a => TEXT_PLATFORMS.has(a.platform))
159
+ .map(a => ({ platform: a.platform, accountId: a._id || a.accountId }));
160
+
161
+ if (textAccounts.length === 0) {
162
+ console.error('[daily-post] No text-friendly accounts connected.');
163
+ process.exit(1);
164
+ }
165
+
166
+ console.log(`[daily-post] Publishing to ${textAccounts.length} platform(s): ${textAccounts.map(a => a.platform).join(', ')}`);
167
+
168
+ const result = await publishPost(content, textAccounts);
169
+ console.log('[daily-post] Result:', JSON.stringify(result, null, 2));
170
+
171
+ if (result.platformResults) {
172
+ for (const pr of result.platformResults) {
173
+ if (pr.status === 'published') {
174
+ console.log(`[daily-post] ${pr.platform}: published`);
175
+ } else {
176
+ console.error(`[daily-post] ${pr.platform}: ${pr.status} — ${pr.error || 'unknown'}`);
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ main().catch(err => {
183
+ console.error('[daily-post] Fatal:', err.message);
184
+ process.exit(1);
185
+ });