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,202 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
8
+ const DEFAULT_REPLY_STATE_PATH = path.join(REPO_ROOT, '.thumbgate', 'reply-monitor-state.json');
9
+ const DEFAULT_DRAFTS_PATH = path.join(REPO_ROOT, '.thumbgate', 'reply-drafts.jsonl');
10
+ const DEFAULT_LAUNCH_ASSETS_PATH = path.join(REPO_ROOT, '.thumbgate', 'social-launch-assets.json');
11
+ const DEFAULT_TIMEZONE = 'America/New_York';
12
+
13
+ const PLATFORM_CAPABILITIES = {
14
+ x: 'active_reply_monitor',
15
+ reddit: 'draft_only_reply_monitor',
16
+ linkedin: 'comment_intake_blocked_by_api_approval',
17
+ instagram: 'no_comment_intake_implemented',
18
+ tiktok: 'no_comment_intake_implemented',
19
+ youtube: 'no_comment_intake_implemented',
20
+ devto: 'no_comment_intake_implemented',
21
+ };
22
+
23
+ function parseArgs(argv = []) {
24
+ const options = {
25
+ date: '',
26
+ timezone: DEFAULT_TIMEZONE,
27
+ replyStatePath: DEFAULT_REPLY_STATE_PATH,
28
+ draftsPath: DEFAULT_DRAFTS_PATH,
29
+ launchAssetsPath: DEFAULT_LAUNCH_ASSETS_PATH,
30
+ };
31
+
32
+ for (let index = 0; index < argv.length; index += 1) {
33
+ const token = String(argv[index] || '').trim();
34
+ if (token.startsWith('--date=')) {
35
+ options.date = token.slice('--date='.length).trim();
36
+ continue;
37
+ }
38
+ if (token === '--date' && argv[index + 1]) {
39
+ options.date = String(argv[index + 1]).trim();
40
+ index += 1;
41
+ continue;
42
+ }
43
+ if (token.startsWith('--timezone=')) {
44
+ options.timezone = token.slice('--timezone='.length).trim() || DEFAULT_TIMEZONE;
45
+ }
46
+ }
47
+
48
+ return options;
49
+ }
50
+
51
+ function readJson(filePath, fallback) {
52
+ try {
53
+ if (!fs.existsSync(filePath)) {
54
+ return fallback;
55
+ }
56
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
57
+ } catch {
58
+ return fallback;
59
+ }
60
+ }
61
+
62
+ function readJsonl(filePath) {
63
+ if (!fs.existsSync(filePath)) {
64
+ return [];
65
+ }
66
+ return fs.readFileSync(filePath, 'utf8')
67
+ .split('\n')
68
+ .map((line) => line.trim())
69
+ .filter(Boolean)
70
+ .map((line) => {
71
+ try {
72
+ return JSON.parse(line);
73
+ } catch {
74
+ return null;
75
+ }
76
+ })
77
+ .filter(Boolean);
78
+ }
79
+
80
+ function formatDateInTimezone(date, timezone = DEFAULT_TIMEZONE) {
81
+ const formatter = new Intl.DateTimeFormat('en-CA', {
82
+ timeZone: timezone,
83
+ year: 'numeric',
84
+ month: '2-digit',
85
+ day: '2-digit',
86
+ });
87
+ return formatter.format(new Date(date));
88
+ }
89
+
90
+ function buildPlatformSummary() {
91
+ return {
92
+ checked: 0,
93
+ replied: 0,
94
+ drafted: 0,
95
+ skipped: 0,
96
+ skippedOwnTweet: 0,
97
+ skippedNoReplyGenerated: 0,
98
+ capability: '',
99
+ ownedLaunchAssets: 0,
100
+ };
101
+ }
102
+
103
+ function buildEngagementAudit(options = {}) {
104
+ const timezone = options.timezone || DEFAULT_TIMEZONE;
105
+ const targetDate = options.date || formatDateInTimezone(new Date(), timezone);
106
+ const replyState = readJson(options.replyStatePath || DEFAULT_REPLY_STATE_PATH, { repliedTo: {}, lastCheck: {} });
107
+ const drafts = readJsonl(options.draftsPath || DEFAULT_DRAFTS_PATH);
108
+ const launchAssets = readJson(options.launchAssetsPath || DEFAULT_LAUNCH_ASSETS_PATH, { launchPosts: {}, campaignPosts: {} });
109
+
110
+ const platforms = {
111
+ x: buildPlatformSummary(),
112
+ reddit: buildPlatformSummary(),
113
+ linkedin: buildPlatformSummary(),
114
+ instagram: buildPlatformSummary(),
115
+ tiktok: buildPlatformSummary(),
116
+ youtube: buildPlatformSummary(),
117
+ devto: buildPlatformSummary(),
118
+ };
119
+
120
+ for (const [platform, capability] of Object.entries(PLATFORM_CAPABILITIES)) {
121
+ platforms[platform].capability = capability;
122
+ }
123
+
124
+ for (const entry of Object.values(replyState.repliedTo || {})) {
125
+ const platform = String(entry.platform || '').trim().toLowerCase();
126
+ if (!platforms[platform]) continue;
127
+ if (formatDateInTimezone(entry.at, timezone) !== targetDate) continue;
128
+ platforms[platform].checked += 1;
129
+ if (entry.drafted) {
130
+ platforms[platform].drafted += 1;
131
+ continue;
132
+ }
133
+ if (entry.skipped) {
134
+ platforms[platform].skipped += 1;
135
+ if (entry.skipped === 'own_tweet') {
136
+ platforms[platform].skippedOwnTweet += 1;
137
+ }
138
+ if (entry.skipped === 'no_reply_generated') {
139
+ platforms[platform].skippedNoReplyGenerated += 1;
140
+ }
141
+ continue;
142
+ }
143
+ platforms[platform].replied += 1;
144
+ }
145
+
146
+ for (const draft of drafts) {
147
+ const platform = String(draft.platform || '').trim().toLowerCase();
148
+ if (!platforms[platform]) continue;
149
+ if (formatDateInTimezone(draft.draftedAt, timezone) !== targetDate) continue;
150
+ platforms[platform].drafted += 1;
151
+ }
152
+
153
+ for (const platform of Object.keys(launchAssets.launchPosts || {})) {
154
+ if (platforms[platform]) {
155
+ platforms[platform].ownedLaunchAssets += 1;
156
+ }
157
+ }
158
+ for (const byPlatform of Object.values(launchAssets.campaignPosts || {})) {
159
+ for (const platform of Object.keys(byPlatform || {})) {
160
+ if (platforms[platform]) {
161
+ platforms[platform].ownedLaunchAssets += 1;
162
+ }
163
+ }
164
+ }
165
+
166
+ const totals = Object.values(platforms).reduce((acc, platform) => {
167
+ acc.checked += platform.checked;
168
+ acc.replied += platform.replied;
169
+ acc.drafted += platform.drafted;
170
+ acc.skipped += platform.skipped;
171
+ return acc;
172
+ }, { checked: 0, replied: 0, drafted: 0, skipped: 0 });
173
+
174
+ return {
175
+ date: targetDate,
176
+ timezone,
177
+ totals,
178
+ platforms,
179
+ evidence: {
180
+ replyStatePath: options.replyStatePath || DEFAULT_REPLY_STATE_PATH,
181
+ draftsPath: options.draftsPath || DEFAULT_DRAFTS_PATH,
182
+ launchAssetsPath: options.launchAssetsPath || DEFAULT_LAUNCH_ASSETS_PATH,
183
+ },
184
+ };
185
+ }
186
+
187
+ if (require.main === module) {
188
+ const audit = buildEngagementAudit(parseArgs(process.argv.slice(2)));
189
+ process.stdout.write(`${JSON.stringify(audit, null, 2)}\n`);
190
+ }
191
+
192
+ module.exports = {
193
+ DEFAULT_DRAFTS_PATH,
194
+ DEFAULT_LAUNCH_ASSETS_PATH,
195
+ DEFAULT_REPLY_STATE_PATH,
196
+ DEFAULT_TIMEZONE,
197
+ PLATFORM_CAPABILITIES,
198
+ buildEngagementAudit,
199
+ formatDateInTimezone,
200
+ parseArgs,
201
+ readJsonl,
202
+ };
@@ -17,7 +17,7 @@ let sharp;
17
17
  try { sharp = require('sharp'); } catch { /* optional dependency */ }
18
18
 
19
19
  const REPO_ROOT = path.resolve(__dirname, '../..');
20
- const DEFAULT_OUTPUT = path.join(REPO_ROOT, '.rlhf', 'instagram-card.png');
20
+ const DEFAULT_OUTPUT = path.join(REPO_ROOT, '.thumbgate', 'instagram-card.png');
21
21
 
22
22
  async function generateInstagramCard(outputPath = DEFAULT_OUTPUT) {
23
23
  const width = 1080;
@@ -79,7 +79,11 @@ async function postThumbGateToInstagram(options = {}) {
79
79
  throw new Error(`Instagram post blocked: ${reasons}`);
80
80
  }
81
81
 
82
- console.log('✅ Post published successfully!');
82
+ if (schedule) {
83
+ console.log('✅ Instagram post scheduled successfully!');
84
+ } else {
85
+ console.log('✅ Post published successfully!');
86
+ }
83
87
  console.log(`Post ID: ${result.id || result.data?.id || 'unknown'}`);
84
88
  return result;
85
89
  } catch (err) {
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('node:path');
5
+ const scheduleManager = require('../schedule-manager');
6
+
7
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
8
+ const GROWTH_REPORT_DIR = path.join(REPO_ROOT, '.thumbgate', 'reports', 'gtm-revenue-loop');
9
+
10
+ function buildNodeEvalCommand(scriptPath, args = []) {
11
+ const absolutePath = path.resolve(scriptPath);
12
+ const serializedArgs = JSON.stringify(args);
13
+ return [
14
+ 'const { spawnSync } = require(\'node:child_process\');',
15
+ `process.chdir(${JSON.stringify(REPO_ROOT)});`,
16
+ `const result = spawnSync(process.execPath, [${JSON.stringify(absolutePath)}, ...${serializedArgs}], {`,
17
+ ' cwd: process.cwd(),',
18
+ ' env: process.env,',
19
+ ' stdio: \'inherit\',',
20
+ '});',
21
+ 'if (result.error) throw result.error;',
22
+ 'process.exit(typeof result.status === \'number\' ? result.status : 0);',
23
+ ].join(' ');
24
+ }
25
+
26
+ function buildGrowthSchedules() {
27
+ return [
28
+ {
29
+ id: 'thumbgate-growth-schedule-campaign',
30
+ name: 'ThumbGate Growth Campaign Scheduler',
31
+ description: 'Schedules the next day of tracked Zernio launch posts.',
32
+ schedule: 'daily 21:15',
33
+ command: buildNodeEvalCommand(path.join(__dirname, 'schedule-thumbgate-campaign.js')),
34
+ workingDirectory: REPO_ROOT,
35
+ },
36
+ {
37
+ id: 'thumbgate-growth-poll-zernio',
38
+ name: 'ThumbGate Growth Poll Zernio',
39
+ description: 'Polls Zernio analytics into the local engagement store every hour.',
40
+ schedule: 'hourly',
41
+ command: buildNodeEvalCommand(path.join(__dirname, 'pollers', 'zernio.js')),
42
+ workingDirectory: REPO_ROOT,
43
+ },
44
+ {
45
+ id: 'thumbgate-growth-sync-launch-assets',
46
+ name: 'ThumbGate Growth Sync Launch Assets',
47
+ description: 'Syncs published and scheduled launch assets from Zernio into a durable local registry.',
48
+ schedule: 'hourly',
49
+ command: buildNodeEvalCommand(path.join(__dirname, 'sync-launch-assets.js')),
50
+ workingDirectory: REPO_ROOT,
51
+ },
52
+ {
53
+ id: 'thumbgate-growth-reply-monitor',
54
+ name: 'ThumbGate Growth Reply Monitor',
55
+ description: 'Checks social replies and posts supported follow-ups or drafts them for review.',
56
+ schedule: 'hourly',
57
+ command: buildNodeEvalCommand(path.join(REPO_ROOT, 'scripts', 'social-reply-monitor.js')),
58
+ workingDirectory: REPO_ROOT,
59
+ },
60
+ {
61
+ id: 'thumbgate-growth-money-watch',
62
+ name: 'ThumbGate Growth Money Watch',
63
+ description: 'Persists hourly commercial-change checks so the first paid event is captured immediately.',
64
+ schedule: 'hourly',
65
+ command: buildNodeEvalCommand(path.join(REPO_ROOT, 'scripts', 'money-watcher.js'), [
66
+ '--once',
67
+ ]),
68
+ workingDirectory: REPO_ROOT,
69
+ },
70
+ {
71
+ id: 'thumbgate-growth-revenue-loop',
72
+ name: 'ThumbGate Growth Revenue Loop',
73
+ description: 'Refreshes the local-first target queue and outreach artifact for the first paid customers.',
74
+ schedule: 'daily 08:20',
75
+ command: buildNodeEvalCommand(path.join(REPO_ROOT, 'scripts', 'autonomous-sales-agent.js'), [
76
+ `--report-dir=${GROWTH_REPORT_DIR}`,
77
+ '--max-targets=8',
78
+ ]),
79
+ workingDirectory: REPO_ROOT,
80
+ },
81
+ {
82
+ id: 'thumbgate-growth-social-digest',
83
+ name: 'ThumbGate Growth Social Digest',
84
+ description: 'Builds the daily social analytics digest after the day closes.',
85
+ schedule: 'daily 22:15',
86
+ command: buildNodeEvalCommand(path.join(__dirname, 'run-digest.js'), ['--days=7']),
87
+ workingDirectory: REPO_ROOT,
88
+ },
89
+ ];
90
+ }
91
+
92
+ function installGrowthAutomation(manager = scheduleManager) {
93
+ const schedules = buildGrowthSchedules();
94
+
95
+ const installed = schedules.map((schedule) => manager.createSchedule(schedule));
96
+ return {
97
+ installed,
98
+ schedules: manager.listSchedules().filter((schedule) => schedule.id.startsWith('thumbgate-growth-')),
99
+ };
100
+ }
101
+
102
+ if (require.main === module) {
103
+ const result = installGrowthAutomation();
104
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
105
+ if (result.installed.some((entry) => !entry.success)) {
106
+ process.exitCode = 1;
107
+ }
108
+ }
109
+
110
+ module.exports = {
111
+ buildNodeEvalCommand,
112
+ buildGrowthSchedules,
113
+ installGrowthAutomation,
114
+ };
@@ -19,7 +19,7 @@ const { generateInstagramCard } = require('./generate-instagram-card');
19
19
  const { postThumbGateToInstagram, THUMBGATE_CAPTION } = require('./instagram-thumbgate-post');
20
20
 
21
21
  const REPO_ROOT = path.resolve(__dirname, '../..');
22
- const IMAGE_PATH = path.join(REPO_ROOT, '.rlhf', 'instagram-card.png');
22
+ const IMAGE_PATH = path.join(REPO_ROOT, '.thumbgate', 'instagram-card.png');
23
23
 
24
24
  async function publishInstagramThumbGate(options = {}) {
25
25
  const {
@@ -57,12 +57,18 @@ async function publishInstagramThumbGate(options = {}) {
57
57
  timezone,
58
58
  utm,
59
59
  });
60
- console.log(`[workflow] ✅ Post published: ${postResult.id || postResult.data?.id}`);
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
+ }
61
65
 
62
66
  return {
63
67
  success: true,
64
68
  imagePath: postOnly ? undefined : imagePath,
65
69
  postId: postResult.id || postResult.data?.id,
70
+ scheduled: Boolean(schedule),
71
+ scheduledFor: schedule || undefined,
66
72
  };
67
73
  }
68
74
  } catch (err) {
@@ -142,7 +142,7 @@ function buildCampaignEntries() {
142
142
  linkedin: [
143
143
  'Workflow hardening beats generic AI hype.',
144
144
  'ThumbGate captures failure signals, promotes them into prevention rules, and blocks the same bad pattern before the next tool call executes.',
145
- 'This is about one workflow becoming safe enough to ship, not abstract agent memory.”',
145
+ 'This is about one workflow becoming safe enough to ship, not abstract "agent memory."',
146
146
  buildLandingUrl('linkedin', 'campaign_proof_pack'),
147
147
  ].join(' '),
148
148
  instagram: `${THUMBGATE_CAPTION}\n\nProof-backed workflow hardening.\n\n${buildLandingUrl('instagram', 'campaign_proof_pack')}`,
@@ -257,24 +257,19 @@ async function submitComment(token, userAgent, { parentId, text }) {
257
257
  // ---------------------------------------------------------------------------
258
258
 
259
259
  /**
260
- * Build the standard follow-up comment for a Reddit post.
261
- * This comment contains the trial CTA and source code link.
260
+ * Build a follow-up comment for a Reddit post.
261
+ * Kept minimal and non-promotional to avoid spam flags.
262
+ * Reddit communities aggressively downvote/ban promotional CTAs.
262
263
  *
263
- * @param {string} subreddit - The subreddit name (used for UTM tracking)
264
- * @param {string} [utmContent] - Optional UTM content tag (defaults to subreddit name)
264
+ * @param {string} subreddit - The subreddit name
265
+ * @param {string} [utmContent] - Optional UTM content tag
265
266
  * @returns {string} The follow-up comment text
266
267
  */
267
268
  function buildFollowUpComment(subreddit, utmContent) {
268
- const content = utmContent || `${subreddit}_post`;
269
- const trialUrl = `https://thumbgate-production.up.railway.app/?utm_source=reddit&utm_medium=organic_social&utm_campaign=reddit_followup_comment&utm_content=${encodeURIComponent(content)}&community=${encodeURIComponent(subreddit)}`;
270
269
  return [
271
- 'The problem: AI coding agents repeat the same mistakes every session. You correct a force-push, it does it again tomorrow. Prompt rules get ignored after context compaction.',
270
+ 'Happy to answer questions about the implementation.',
272
271
  '',
273
- 'ThumbGate fixes this with enforcement, not memory. You give a thumbs-down, it auto-generates a prevention rule, and a gate physically blocks the agent from repeating that action. Thumbs-up reinforces good behavior.',
274
- '',
275
- `Try free for 7 days (no credit card, 2-minute setup): ${trialUrl}`,
276
- '',
277
- 'Source code (MIT licensed): https://github.com/IgorGanapolsky/ThumbGate',
272
+ 'Source code (MIT): https://github.com/IgorGanapolsky/ThumbGate',
278
273
  '',
279
274
  'Disclosure: I built this.',
280
275
  ].join('\n');
@@ -129,6 +129,23 @@ async function zernioFetch(method, endpoint, body = null) {
129
129
  return res.json();
130
130
  }
131
131
 
132
+ async function listPosts(options = {}) {
133
+ const query = new URLSearchParams();
134
+ if (options.limit) query.set('limit', String(options.limit));
135
+ if (options.page) query.set('page', String(options.page));
136
+ if (options.status) query.set('status', String(options.status));
137
+
138
+ const suffix = query.toString() ? `?${query.toString()}` : '';
139
+ const json = await zernioFetch('GET', `/posts${suffix}`);
140
+ return Array.isArray(json.posts) ? json.posts : (json.data?.posts || json.data || []);
141
+ }
142
+
143
+ async function deletePost(postId) {
144
+ if (!postId) throw new Error('deletePost: postId is required');
145
+ const json = await zernioFetch('DELETE', `/posts/${encodeURIComponent(String(postId).trim())}`);
146
+ return json.data ?? json;
147
+ }
148
+
132
149
  async function requestMediaPresign(filename, contentType, size) {
133
150
  if (!filename) throw new Error('requestMediaPresign: filename is required');
134
151
  if (!contentType) throw new Error('requestMediaPresign: contentType is required');
@@ -326,6 +343,8 @@ async function publishToAllPlatforms(content, options = {}) {
326
343
  }
327
344
 
328
345
  module.exports = {
346
+ deletePost,
347
+ listPosts,
329
348
  publishPost,
330
349
  schedulePost,
331
350
  publishToAllPlatforms,
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const {
5
+ buildCampaignEntries,
6
+ defaultCampaignSchedule,
7
+ } = require('./publish-thumbgate-launch');
8
+ const {
9
+ deletePost,
10
+ listPosts,
11
+ } = require('./publishers/zernio');
12
+ const {
13
+ buildScheduleKey,
14
+ DEFAULT_STATE_PATH,
15
+ writeScheduleState,
16
+ } = require('./schedule-thumbgate-campaign');
17
+
18
+ const CAMPAIGN_MARKERS = {
19
+ proof_pack: 'campaign_proof_pack',
20
+ free_local: 'campaign_free_local',
21
+ checkout_path: 'campaign_checkout_path',
22
+ };
23
+
24
+ function parseArgs(argv = []) {
25
+ const options = {
26
+ cancelDuplicates: false,
27
+ limit: 50,
28
+ scheduleTimes: [],
29
+ statePath: DEFAULT_STATE_PATH,
30
+ };
31
+
32
+ for (let index = 0; index < argv.length; index += 1) {
33
+ const token = argv[index];
34
+ if (token === '--cancel-duplicates') {
35
+ options.cancelDuplicates = true;
36
+ continue;
37
+ }
38
+ if (token.startsWith('--limit=')) {
39
+ options.limit = Number(token.slice('--limit='.length)) || options.limit;
40
+ continue;
41
+ }
42
+ if (token.startsWith('--times=')) {
43
+ options.scheduleTimes = token.slice('--times='.length).split(',').map((value) => value.trim()).filter(Boolean);
44
+ continue;
45
+ }
46
+ if (token.startsWith('--state-path=')) {
47
+ options.statePath = token.slice('--state-path='.length).trim() || DEFAULT_STATE_PATH;
48
+ }
49
+ }
50
+
51
+ return options;
52
+ }
53
+
54
+ function normalizePlatform(post) {
55
+ return String(post?.platforms?.[0]?.platform || '').trim().toLowerCase();
56
+ }
57
+
58
+ function selectCanonicalPost(posts = []) {
59
+ return [...posts].sort((left, right) => {
60
+ const leftCreated = new Date(left.createdAt || 0).getTime();
61
+ const rightCreated = new Date(right.createdAt || 0).getTime();
62
+ return leftCreated - rightCreated;
63
+ })[0] || null;
64
+ }
65
+
66
+ async function reconcileCampaignState(options = {}, api = {}) {
67
+ const zernio = {
68
+ deletePost: api.deletePost || deletePost,
69
+ listPosts: api.listPosts || listPosts,
70
+ };
71
+ const scheduleTimes = options.scheduleTimes && options.scheduleTimes.length > 0
72
+ ? options.scheduleTimes
73
+ : defaultCampaignSchedule();
74
+ const posts = await zernio.listPosts({ limit: options.limit || 50 });
75
+ const scheduled = {};
76
+ const duplicates = [];
77
+ const kept = [];
78
+ const entries = buildCampaignEntries();
79
+
80
+ for (let index = 0; index < entries.length; index += 1) {
81
+ const entry = entries[index];
82
+ const scheduleLocal = scheduleTimes[index];
83
+ if (!scheduleLocal) {
84
+ continue;
85
+ }
86
+
87
+ const scheduledForUtc = new Date(scheduleLocal).toISOString();
88
+
89
+ for (const platform of Object.keys(entry.posts)) {
90
+ const marker = CAMPAIGN_MARKERS[entry.slug];
91
+ const matchingPosts = posts.filter((post) => (
92
+ post.status === 'scheduled' &&
93
+ normalizePlatform(post) === platform &&
94
+ String(post.content || '').includes(`utm_content=${marker}`) &&
95
+ String(post.scheduledFor || '') === scheduledForUtc
96
+ ));
97
+
98
+ if (matchingPosts.length === 0) {
99
+ continue;
100
+ }
101
+
102
+ const canonical = selectCanonicalPost(matchingPosts);
103
+ if (!canonical) {
104
+ continue;
105
+ }
106
+
107
+ const scheduleKey = buildScheduleKey({
108
+ slug: entry.slug,
109
+ platform,
110
+ scheduledFor: scheduleLocal,
111
+ });
112
+ scheduled[scheduleKey] = {
113
+ id: canonical._id,
114
+ scheduledFor: scheduleLocal,
115
+ slug: entry.slug,
116
+ platform,
117
+ recordedAt: new Date().toISOString(),
118
+ };
119
+ kept.push({
120
+ key: scheduleKey,
121
+ id: canonical._id,
122
+ });
123
+
124
+ const extras = matchingPosts.filter((post) => post._id !== canonical._id);
125
+ for (const duplicate of extras) {
126
+ const duplicateRecord = {
127
+ key: scheduleKey,
128
+ id: duplicate._id,
129
+ platform,
130
+ slug: entry.slug,
131
+ };
132
+ if (options.cancelDuplicates) {
133
+ duplicateRecord.cancelled = await zernio.deletePost(duplicate._id);
134
+ }
135
+ duplicates.push(duplicateRecord);
136
+ }
137
+ }
138
+ }
139
+
140
+ writeScheduleState(options.statePath || DEFAULT_STATE_PATH, { scheduled });
141
+ return {
142
+ duplicates,
143
+ kept,
144
+ statePath: options.statePath || DEFAULT_STATE_PATH,
145
+ };
146
+ }
147
+
148
+ if (require.main === module) {
149
+ reconcileCampaignState(parseArgs(process.argv.slice(2)))
150
+ .then((result) => {
151
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
152
+ })
153
+ .catch((error) => {
154
+ console.error(error && error.message ? error.message : error);
155
+ process.exit(1);
156
+ });
157
+ }
158
+
159
+ module.exports = {
160
+ CAMPAIGN_MARKERS,
161
+ normalizePlatform,
162
+ parseArgs,
163
+ reconcileCampaignState,
164
+ selectCanonicalPost,
165
+ };