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
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+
3
4
  const BOT_SLOP_PATTERNS = [
4
5
  { id: 'emoji_spam', pattern: /(?:🚀|💡|🔥|⚡|🎯|💪|🙌|👀){3,}/g, reason: 'Excessive emoji spam' },
5
6
  { id: 'generic_opener', pattern: /^(?:Just|Excited to|Thrilled to|Happy to|Proud to) (?:\w+ )*?(?:launch|ship|release|publish|built|creat|announc)/i, reason: 'Generic shipped opener' },
@@ -11,8 +12,123 @@ const BOT_SLOP_PATTERNS = [
11
12
  { id: 'self_congratulation', pattern: /(?:We're proud to|I'm honored to|Humbled to|Grateful to announce)/i, reason: 'Self-congratulatory opener' },
12
13
  { id: 'empty_hype', pattern: /(?:game.?changer|revolutionary|disruptive|next.?gen|cutting.?edge|world.?class|best.?in.?class)/i, reason: 'Empty hype words' },
13
14
  ];
15
+
16
+ const REPLY_TOPIC_PATTERNS = {
17
+ skills: /\bskill|template|process|workflow|review|sprint|implement|phase/i,
18
+ context: /\bcontext doc|context docs|conflicting|inconsisten|claude\.md|cursorrules|instruction/i,
19
+ memory: /\bmemory|remember|amnesia|across sessions|next session|compaction|persist/i,
20
+ setup: /\binstall|setup|config|init|repo|github|link|tool|open source|built/i,
21
+ gates: /\bgate|hook|block|prevent|pretooluse|mcp/i,
22
+ };
23
+
24
+ const UNSOLICITED_PROMO_PATTERNS = [
25
+ { id: 'unsolicited_link', pattern: /https?:\/\//i, reason: 'Unsolicited link in reply' },
26
+ { id: 'unsolicited_install', pattern: /npx thumbgate init/i, reason: 'Unsolicited install CTA in reply' },
27
+ { id: 'unsolicited_stack_dump', pattern: /\b(?:sqlite\+fts5|thompson sampling|pretooluse|mcp server)\b/i, reason: 'Unsolicited architecture dump in reply' },
28
+ ];
29
+
14
30
  const MIN_POST_LENGTH = 30;
15
31
  const MAX_POST_LENGTH = 2000;
16
- function scanForSlop(postText) { const text = String(postText || ''); const findings = []; if (text.length < MIN_POST_LENGTH) findings.push({ id: 'too_short', reason: 'Too short' }); if (text.length > MAX_POST_LENGTH) findings.push({ id: 'too_long', reason: 'Too long' }); for (const p of BOT_SLOP_PATTERNS) { p.pattern.lastIndex = 0; if (p.pattern.test(text)) findings.push({ id: p.id, reason: p.reason }); } const words = text.split(/\s+/).filter(w => w.length > 3); const capsWords = words.filter(w => w === w.toUpperCase() && /[A-Z]/.test(w)); if (words.length > 5 && capsWords.length / words.length > 0.3) findings.push({ id: 'caps_shouting', reason: 'Too many ALL CAPS' }); return { allowed: findings.length === 0, findings, findingCount: findings.length, postLength: text.length }; }
17
- function gatePost(postText) { const scan = scanForSlop(postText); if (!scan.allowed) { console.error('[social-quality-gate] BLOCKED:'); for (const f of scan.findings) console.error(' -', f.id, f.reason); } return scan; }
18
- module.exports = { scanForSlop, gatePost, BOT_SLOP_PATTERNS, MIN_POST_LENGTH, MAX_POST_LENGTH };
32
+
33
+ function scanForSlop(postText) {
34
+ const text = String(postText || '');
35
+ const findings = [];
36
+
37
+ if (text.length < MIN_POST_LENGTH) {
38
+ findings.push({ id: 'too_short', reason: 'Too short' });
39
+ }
40
+
41
+ if (text.length > MAX_POST_LENGTH) {
42
+ findings.push({ id: 'too_long', reason: 'Too long' });
43
+ }
44
+
45
+ for (const rule of BOT_SLOP_PATTERNS) {
46
+ rule.pattern.lastIndex = 0;
47
+ if (rule.pattern.test(text)) {
48
+ findings.push({ id: rule.id, reason: rule.reason });
49
+ }
50
+ }
51
+
52
+ const words = text.split(/\s+/).filter((word) => word.length > 3);
53
+ const capsWords = words.filter((word) => word === word.toUpperCase() && /[A-Z]/.test(word));
54
+ if (words.length > 5 && capsWords.length / words.length > 0.3) {
55
+ findings.push({ id: 'caps_shouting', reason: 'Too many ALL CAPS' });
56
+ }
57
+
58
+ return {
59
+ allowed: findings.length === 0,
60
+ findings,
61
+ findingCount: findings.length,
62
+ postLength: text.length,
63
+ };
64
+ }
65
+
66
+ function detectReplyTopics(text) {
67
+ const content = String(text || '');
68
+ return Object.entries(REPLY_TOPIC_PATTERNS)
69
+ .filter(([, pattern]) => pattern.test(content))
70
+ .map(([topic]) => topic);
71
+ }
72
+
73
+ function commentExplicitlyRequestsProduct(commentText) {
74
+ return /\b(?:what tool|what is it|which tool|repo|github|link|where can i find|can you share|how do i install|setup details|what did you build)\b/i.test(
75
+ String(commentText || '')
76
+ );
77
+ }
78
+
79
+ function gateContextualReply(commentText, replyText, options = {}) {
80
+ const scan = scanForSlop(replyText);
81
+ const findings = [...scan.findings];
82
+ const comment = String(commentText || '');
83
+ const reply = String(replyText || '');
84
+ const platform = String(options.platform || '').toLowerCase();
85
+ const commentTopics = detectReplyTopics(comment);
86
+ const replyTopics = detectReplyTopics(reply);
87
+
88
+ if (commentTopics.length > 0 && !commentTopics.some((topic) => replyTopics.includes(topic))) {
89
+ findings.push({
90
+ id: 'not_contextual',
91
+ reason: 'Reply does not address the commenter’s actual point',
92
+ });
93
+ }
94
+
95
+ if (platform === 'reddit' && !commentExplicitlyRequestsProduct(comment)) {
96
+ for (const rule of UNSOLICITED_PROMO_PATTERNS) {
97
+ rule.pattern.lastIndex = 0;
98
+ if (rule.pattern.test(reply)) {
99
+ findings.push({ id: rule.id, reason: rule.reason });
100
+ }
101
+ }
102
+ }
103
+
104
+ return {
105
+ allowed: findings.length === 0,
106
+ findings,
107
+ findingCount: findings.length,
108
+ replyLength: reply.length,
109
+ commentTopics,
110
+ replyTopics,
111
+ };
112
+ }
113
+
114
+ function gatePost(postText) {
115
+ const scan = scanForSlop(postText);
116
+ if (!scan.allowed) {
117
+ console.error('[social-quality-gate] BLOCKED:');
118
+ for (const finding of scan.findings) {
119
+ console.error(' -', finding.id, finding.reason);
120
+ }
121
+ }
122
+ return scan;
123
+ }
124
+
125
+ module.exports = {
126
+ BOT_SLOP_PATTERNS,
127
+ MAX_POST_LENGTH,
128
+ MIN_POST_LENGTH,
129
+ commentExplicitlyRequestsProduct,
130
+ detectReplyTopics,
131
+ gateContextualReply,
132
+ gatePost,
133
+ scanForSlop,
134
+ };
@@ -16,12 +16,16 @@
16
16
  * State file: .thumbgate/reply-monitor-state.json — tracks which replies we've already responded to.
17
17
  */
18
18
 
19
- require('dotenv').config();
20
19
  const fs = require('fs');
21
20
  const path = require('path');
21
+ const { loadLocalEnv } = require('./social-analytics/load-env');
22
+ const { gateContextualReply, commentExplicitlyRequestsProduct } = require('./social-quality-gate');
22
23
 
23
- const STATE_FILE = path.resolve(__dirname, '..', '.rlhf', 'reply-monitor-state.json');
24
+ const STATE_FILE = path.resolve(__dirname, '..', '.thumbgate', 'reply-monitor-state.json');
24
25
  const REDDIT_API_BASE = 'https://oauth.reddit.com';
26
+ const DEFAULT_X_HANDLE = 'IgorGanapolsky';
27
+
28
+ loadLocalEnv();
25
29
 
26
30
  // ---------------------------------------------------------------------------
27
31
  // State management
@@ -50,7 +54,7 @@ function saveState(state) {
50
54
  // Draft file for human review (Reddit replies are NEVER auto-posted)
51
55
  // ---------------------------------------------------------------------------
52
56
 
53
- const DRAFT_FILE = path.resolve(__dirname, '..', '.rlhf', 'reply-drafts.jsonl');
57
+ const DRAFT_FILE = path.resolve(__dirname, '..', '.thumbgate', 'reply-drafts.jsonl');
54
58
 
55
59
  function saveDraft(draft) {
56
60
  const dir = path.dirname(DRAFT_FILE);
@@ -58,6 +62,85 @@ function saveDraft(draft) {
58
62
  fs.appendFileSync(DRAFT_FILE, JSON.stringify(draft) + '\n');
59
63
  }
60
64
 
65
+ function normalizeArray(value) {
66
+ if (value && Array.isArray(value.data)) {
67
+ return value.data;
68
+ }
69
+ return Array.isArray(value) ? value : [];
70
+ }
71
+
72
+ function isRevenueRelevantXTweet(tweet = {}) {
73
+ const text = String(tweet.text || '').toLowerCase();
74
+ if (!text) return false;
75
+ return /thumbgate|thumbgate-production|checkout\/pro|workflow-sprint|pre-action gates|vibe coding/.test(text);
76
+ }
77
+
78
+ function buildOwnedConversationQuery(tweetId, username = DEFAULT_X_HANDLE) {
79
+ const normalizedId = String(tweetId || '').trim();
80
+ const normalizedUsername = String(username || DEFAULT_X_HANDLE).trim();
81
+ if (!normalizedId) {
82
+ return '';
83
+ }
84
+ return `conversation_id:${normalizedId} -from:${normalizedUsername}`;
85
+ }
86
+
87
+ async function collectXSearchCandidates(options = {}) {
88
+ const searchTweets = options.searchTweets;
89
+ if (typeof searchTweets !== 'function') {
90
+ return { tweets: [], searchMode: 'unavailable', queryLog: [] };
91
+ }
92
+
93
+ const username = options.username || process.env.X_USERNAME || DEFAULT_X_HANDLE;
94
+ const ownUserId = options.ownUserId || process.env.X_USER_ID || '1733256637199073280';
95
+ const queryLog = [];
96
+ const aggregate = [];
97
+ const seenTweetIds = new Set();
98
+ let ownedCandidates = [];
99
+
100
+ if (typeof options.fetchOwnedTweets === 'function') {
101
+ try {
102
+ const recentTweets = await options.fetchOwnedTweets();
103
+ ownedCandidates = normalizeArray(recentTweets)
104
+ .filter((tweet) => String(tweet.author_id || ownUserId) === ownUserId)
105
+ .filter(isRevenueRelevantXTweet)
106
+ .slice(0, 6);
107
+ } catch (err) {
108
+ console.warn(`[reply-monitor] Could not fetch own X tweets: ${err.message}`);
109
+ }
110
+ }
111
+
112
+ for (const tweet of ownedCandidates) {
113
+ const query = buildOwnedConversationQuery(tweet.id, username);
114
+ if (!query) continue;
115
+ queryLog.push(query);
116
+ const replies = normalizeArray(await searchTweets(query, { maxResults: 10 }));
117
+ for (const reply of replies) {
118
+ if (!reply || seenTweetIds.has(reply.id)) continue;
119
+ seenTweetIds.add(reply.id);
120
+ aggregate.push(reply);
121
+ }
122
+ }
123
+
124
+ if (aggregate.length > 0) {
125
+ return {
126
+ tweets: aggregate,
127
+ ownTweets: ownedCandidates.map((tweet) => ({ id: tweet.id, text: tweet.text || '' })),
128
+ queryLog,
129
+ searchMode: 'owned_conversations',
130
+ };
131
+ }
132
+
133
+ const fallbackQuery = 'thumbgate OR ThumbGate OR "pre-action gates"';
134
+ queryLog.push(fallbackQuery);
135
+ const fallbackTweets = normalizeArray(await searchTweets(fallbackQuery, { maxResults: 10 }));
136
+ return {
137
+ tweets: fallbackTweets,
138
+ ownTweets: ownedCandidates.map((tweet) => ({ id: tweet.id, text: tweet.text || '' })),
139
+ queryLog,
140
+ searchMode: 'keyword_fallback',
141
+ };
142
+ }
143
+
61
144
  // ---------------------------------------------------------------------------
62
145
  // Bot/hostile detection — skip comments that are calling us out
63
146
  // ---------------------------------------------------------------------------
@@ -97,7 +180,8 @@ async function generateReply(comment, context) {
97
180
 
98
181
  // NEVER reply with generic fluff — build reply from what they ACTUALLY said
99
182
  const isQuestion = context.isQuestion || /\?/.test(comment);
100
- const REPO = 'https://github.com/IgorGanapolsky/ThumbGate';
183
+ const isReddit = context.platform === 'reddit';
184
+ const wantsProductDetails = commentExplicitlyRequestsProduct(comment);
101
185
 
102
186
  // Extract the specific topic they're asking about
103
187
  const mentionsSetup = /install|setup|config|init|npx|how.+start/i.test(lc);
@@ -108,30 +192,59 @@ async function generateReply(comment, context) {
108
192
  const mentionsScaling = /scale|team|multi.?repo|collaborate|share/i.test(lc);
109
193
  const mentionsSkeptical = /why not|already exist|what.+different|vs |compared to/i.test(lc);
110
194
  const mentionsThanks = /thanks|thank you|cool|nice|interesting|awesome/i.test(lc);
195
+ const mentionsSkillsProcess = /skill|template|process|workflow|review|sprint|implement|phase/i.test(lc);
196
+ const mentionsConflictingDocs = /context doc|context docs|conflicting|inconsisten|claude\.md|cursorrules|instruction/i.test(lc);
111
197
 
112
198
  // Build response that addresses THEIR specific point
113
- if (mentionsSetup && isQuestion) {
199
+ if (mentionsSkillsProcess || mentionsConflictingDocs) {
200
+ const reply = [
201
+ 'That matches what I have seen too.',
202
+ 'Smaller review/implement phases hold up much better than one giant instruction blob,',
203
+ 'and conflicting context docs are where things usually start drifting.'
204
+ ].join(' ');
205
+ const gate = gateContextualReply(comment, reply, context);
206
+ return gate.allowed ? reply : null;
207
+ }
208
+ if (mentionsSetup && isQuestion && !isReddit) {
114
209
  return `\`npx thumbgate init\` auto-detects your agent and wires the hooks. Takes about 30 seconds. What agent are you using?`;
115
210
  }
116
211
  if (mentionsSkeptical) {
117
- return `Fair question. The difference from rules files or memory tools: this physically blocks the action before execution, not after. The agent can't ignore a gate the way it can ignore a system prompt. Whether that tradeoff is worth it depends on how often your agent repeats mistakes.`;
212
+ const reply = 'Fair question. The difference from rules files or memory tools is enforcement: the bad action gets stopped before execution instead of being remembered and then ignored. Whether that tradeoff is worth it depends on how often your agent repeats the same mistake.';
213
+ const gate = gateContextualReply(comment, reply, context);
214
+ return gate.allowed ? reply : null;
118
215
  }
119
216
  if (mentionsHow && mentionsGates) {
120
- return `PreToolUse hooks intercept the tool call before it runs. Each call is checked against prevention rules promoted from past failures. If it matches, the action is blocked — the agent has to try a different approach. The rules adapt over time via Thompson Sampling so false positives decrease.`;
217
+ const reply = isReddit
218
+ ? 'The short version is: the tool call gets checked before it runs. If it matches a previously rejected pattern, it is blocked and the agent has to try a different path.'
219
+ : 'PreToolUse hooks intercept the tool call before it runs. Each call is checked against prevention rules promoted from past failures. If it matches, the action is blocked and the agent has to try a different approach. The rules adapt over time so false positives decrease.';
220
+ const gate = gateContextualReply(comment, reply, context);
221
+ return gate.allowed ? reply : null;
121
222
  }
122
223
  if (mentionsScaling) {
123
- return `For teams, the Pro tier syncs prevention rules across machines so everyone benefits from lessons learned on any repo. But the free local version covers solo dev workflows completely.`;
224
+ if (isReddit && !wantsProductDetails) {
225
+ return null;
226
+ }
227
+ const reply = 'For teams, the useful part is shared lessons instead of each developer relearning the same failure pattern alone. Solo workflows usually benefit first from the local version.';
228
+ const gate = gateContextualReply(comment, reply, context);
229
+ return gate.allowed ? reply : null;
124
230
  }
125
231
  if (mentionsMemory && isQuestion) {
126
- return `The key difference from memory tools: memory helps agents remember, but they can still ignore what they remember. Gates enforce if there's a rule against force-pushing, the agent physically can't do it. It's enforcement, not suggestion.`;
232
+ const reply = 'The useful distinction is memory versus enforcement. Memory helps the agent remember, but it can still ignore that memory. Enforcement is what stops the already-rejected move from happening again.';
233
+ const gate = gateContextualReply(comment, reply, context);
234
+ return gate.allowed ? reply : null;
127
235
  }
128
- if (mentionsCursor && isQuestion) {
129
- return `Works with Cursor via MCP. The hooks are agent-agnostic — same prevention rules apply whether you're using Cursor, Claude Code, or Codex. What specific failure patterns are you hitting?`;
236
+ if (mentionsCursor && isQuestion && !isReddit) {
237
+ return 'Works with Cursor via MCP. The same prevention rules can apply across Cursor, Claude Code, and Codex. What specific failure patterns are you hitting?';
130
238
  }
131
239
  if (mentionsThanks && !isQuestion) {
132
240
  // Don't reply to simple "thanks" — it looks desperate
133
241
  return null;
134
242
  }
243
+ if (isReddit && wantsProductDetails) {
244
+ const reply = 'Happy to share the repo or setup details if that would help. The main thing that worked for me was keeping accepted and rejected patterns outside the session so the next run starts with the same constraints.';
245
+ const gate = gateContextualReply(comment, reply, context);
246
+ return gate.allowed ? reply : null;
247
+ }
135
248
  if (isQuestion) {
136
249
  // They asked something specific we didn't match — better to draft for human review
137
250
  return null;
@@ -260,29 +373,41 @@ async function checkXReplies(state, dryRun) {
260
373
 
261
374
  // Use the post-to-x module for OAuth signing
262
375
  let xModule;
376
+ let xPoller;
263
377
  try {
264
378
  xModule = require('./post-to-x.js');
379
+ xPoller = require('./social-analytics/pollers/x.js');
265
380
  } catch {
266
- console.warn('[reply-monitor] Could not load post-to-x.js, skipping X');
381
+ console.warn('[reply-monitor] Could not load X modules, skipping X');
267
382
  return [];
268
383
  }
269
384
 
270
- // Search for recent mentions
271
- let mentions;
385
+ let searchResult;
272
386
  try {
273
- mentions = await xModule.searchTweets('thumbgate OR ThumbGate OR "pre-action gates"');
387
+ searchResult = await collectXSearchCandidates({
388
+ searchTweets: xModule.searchTweets,
389
+ fetchOwnedTweets: process.env.X_BEARER_TOKEN && process.env.X_USER_ID
390
+ ? async () => {
391
+ const response = await xPoller.fetchUserTweets(process.env.X_BEARER_TOKEN, process.env.X_USER_ID);
392
+ return response && response.data;
393
+ }
394
+ : null,
395
+ ownUserId: process.env.X_USER_ID || '1733256637199073280',
396
+ username: process.env.X_USERNAME || DEFAULT_X_HANDLE,
397
+ });
274
398
  } catch (err) {
275
399
  console.warn(`[reply-monitor] X search failed: ${err.message}`);
276
400
  return [];
277
401
  }
278
402
 
279
- // searchTweets returns the array directly, not {data: [...]}
280
- const mentionsList = Array.isArray(mentions) ? mentions : mentions?.data;
403
+ const mentionsList = normalizeArray(searchResult && searchResult.tweets);
281
404
  if (!mentionsList || mentionsList.length === 0) {
282
405
  console.log('[reply-monitor] No X mentions found');
283
406
  return [];
284
407
  }
285
408
 
409
+ console.log(`[reply-monitor] X candidate search mode: ${searchResult.searchMode}`);
410
+
286
411
  const results = [];
287
412
  const repliesSentThisRun = new Set(); // Track reply text to prevent duplicates
288
413
 
@@ -400,7 +525,14 @@ async function monitor({ platforms, dryRun } = {}) {
400
525
  return allResults;
401
526
  }
402
527
 
403
- module.exports = { monitor, generateReply };
528
+ module.exports = {
529
+ buildOwnedConversationQuery,
530
+ checkXReplies,
531
+ collectXSearchCandidates,
532
+ generateReply,
533
+ isRevenueRelevantXTweet,
534
+ monitor,
535
+ };
404
536
 
405
537
  // ---------------------------------------------------------------------------
406
538
  // CLI
@@ -418,22 +550,6 @@ if (require.main === module) {
418
550
  const platformArg = getArg('--platform');
419
551
  const platforms = platformArg ? [platformArg] : null;
420
552
 
421
- // Load .env
422
- const envPath = path.resolve(__dirname, '..', '.env');
423
- if (fs.existsSync(envPath)) {
424
- const envContent = fs.readFileSync(envPath, 'utf8');
425
- for (const line of envContent.split('\n')) {
426
- const trimmed = line.trim();
427
- if (!trimmed || trimmed.startsWith('#')) continue;
428
- const eqIdx = trimmed.indexOf('=');
429
- if (eqIdx > 0) {
430
- const key = trimmed.slice(0, eqIdx);
431
- const value = trimmed.slice(eqIdx + 1);
432
- if (!process.env[key]) process.env[key] = value;
433
- }
434
- }
435
- }
436
-
437
553
  monitor({ platforms, dryRun })
438
554
  .then((results) => {
439
555
  console.log('\n[reply-monitor] Summary:', JSON.stringify(results, null, 2));
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const { resolveFeedbackDir } = require('./feedback-paths');
6
+
7
+ function unique(values = []) {
8
+ return [...new Set(values.filter(Boolean).map((value) => path.resolve(value)))];
9
+ }
10
+
11
+ function getStatuslineCacheCandidates(options = {}) {
12
+ const cwd = options.cwd || process.cwd();
13
+ const home = options.home || process.env.HOME || process.env.USERPROFILE || '';
14
+ const feedbackDir = resolveFeedbackDir({ cwd, env: options.env || process.env });
15
+
16
+ return unique([
17
+ path.join(feedbackDir, 'statusline_cache.json'),
18
+ path.join(cwd, '.thumbgate', 'statusline_cache.json'),
19
+ home ? path.join(home, '.thumbgate', 'statusline_cache.json') : null,
20
+ ]);
21
+ }
22
+
23
+ if (require.main === module) {
24
+ process.stdout.write(JSON.stringify({ candidates: getStatuslineCacheCandidates() }));
25
+ }
26
+
27
+ module.exports = { getStatuslineCacheCandidates };
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const { isProLicensed } = require('./license');
6
+
7
+ function getStatuslineMeta(options = {}) {
8
+ const pkg = require(path.join(__dirname, '..', 'package.json'));
9
+ const env = options.env || process.env;
10
+ const homeDir = options.homeDir || env.HOME || env.USERPROFILE || '.';
11
+
12
+ return {
13
+ version: String(pkg.version || '').trim() || 'unknown',
14
+ tier: isProLicensed({ homeDir }) ? 'Pro' : 'Free',
15
+ };
16
+ }
17
+
18
+ if (require.main === module) {
19
+ process.stdout.write(JSON.stringify(getStatuslineMeta()));
20
+ }
21
+
22
+ module.exports = { getStatuslineMeta };
@@ -1,7 +1,6 @@
1
1
  #!/bin/bash
2
2
  # ThumbGate Status Line for Claude Code
3
- # Shows ThumbGate feedback stats + most recent lesson at a glance.
4
- # Thumbs icons trigger CLI feedback capture inline (no browser).
3
+ # Shows ThumbGate feedback stats + package version/tier at a glance.
5
4
  # Installed by: npx thumbgate init --agent claude-code
6
5
 
7
6
  # Resolve script directory safely (CodeQL: no uncontrolled paths)
@@ -17,14 +16,19 @@ CTX_PCT="${CTX_PCT:-0}"
17
16
 
18
17
  # ── ThumbGate stats from cache ────────────────────────────────────────
19
18
  THUMBGATE_CACHE=""
20
- for base in "${THUMBGATE_FEEDBACK_DIR:-.}" "." "${HOME}"; do
21
- for rel in "statusline_cache.json" ".thumbgate/statusline_cache.json" ".rlhf/statusline_cache.json"; do
22
- if [ -f "${base}/${rel}" ]; then
23
- THUMBGATE_CACHE="${base}/${rel}"
24
- break 2
19
+ _CACHE_CANDIDATES_JSON=$(node "${SCRIPT_DIR}/statusline-cache-path.js" 2>/dev/null)
20
+ if [ -n "$_CACHE_CANDIDATES_JSON" ]; then
21
+ while IFS= read -r candidate; do
22
+ [ -z "$candidate" ] && continue
23
+ if [ -f "$candidate" ]; then
24
+ THUMBGATE_CACHE="$candidate"
25
+ break
25
26
  fi
26
- done
27
- done
27
+ done < <(echo "$_CACHE_CANDIDATES_JSON" | jq -r '.candidates[]?' 2>/dev/null)
28
+ fi
29
+ if [ -z "$THUMBGATE_CACHE" ]; then
30
+ THUMBGATE_CACHE="$(echo "$_CACHE_CANDIDATES_JSON" | jq -r '.candidates[0] // empty' 2>/dev/null)"
31
+ fi
28
32
  if [ -z "$THUMBGATE_CACHE" ]; then
29
33
  THUMBGATE_CACHE="${THUMBGATE_FEEDBACK_DIR:-.}/statusline_cache.json"
30
34
  fi
@@ -44,7 +48,7 @@ fi
44
48
  _NOW=$(date +%s)
45
49
  if [ $(( _NOW - ${CACHE_TS:-0} )) -gt 120 ]; then
46
50
  (
47
- _R=$(curl -s --max-time 3 "http://localhost:9876/v1/feedback/stats" -H "Authorization: Bearer tg_creator_dev_enterprise" 2>/dev/null)
51
+ _R=$(curl -s --max-time 3 "http://localhost:3456/v1/feedback/stats" -H "Authorization: Bearer ${THUMBGATE_API_KEY:-tg_creator_dev_enterprise}" 2>/dev/null)
48
52
  [ -z "$_R" ] && exit 0
49
53
  echo "$_R" | python3 -c "
50
54
  import json,sys,time,os
@@ -59,13 +63,13 @@ except:pass
59
63
  disown 2>/dev/null
60
64
  fi
61
65
 
62
- # ── Most recent lesson from lesson-inference ──────────────────────
63
- LESSON_TEXT=""; LESSON_ID=""
64
- _LESSON_JSON=$(node "${SCRIPT_DIR}/statusline-lesson.js" 2>/dev/null)
65
- if [ -n "$_LESSON_JSON" ]; then
66
- eval "$(echo "$_LESSON_JSON" | jq -r '
67
- @sh "LESSON_TEXT=\(.text // "")",
68
- @sh "LESSON_ID=\(.lessonId // "")"
66
+ # ── ThumbGate package metadata ────────────────────────────────────────
67
+ TG_VERSION="unknown"; TG_TIER="Free"
68
+ _META_JSON=$(node "${SCRIPT_DIR}/statusline-meta.js" 2>/dev/null)
69
+ if [ -n "$_META_JSON" ]; then
70
+ eval "$(echo "$_META_JSON" | jq -r '
71
+ @sh "TG_VERSION=\(.version // "unknown")",
72
+ @sh "TG_TIER=\(.tier // "Free")"
69
73
  ' 2>/dev/null)"
70
74
  fi
71
75
 
@@ -88,29 +92,17 @@ case "${TREND}" in
88
92
  improving) ARROW="↗" ;; degrading) ARROW="↘" ;; stable) ARROW="→" ;; *) ARROW="?" ;;
89
93
  esac
90
94
 
91
- # ── OSC 8 clickable links ────────────────────────────────────────
92
- # Links use CLI commands instead of browser URLs.
93
- # Clicking 👍 runs: node bin/cli.js feedback --signal=up
94
- # Clicking 👎 runs: node bin/cli.js feedback --signal=down
95
- osc_link() { printf '\033]8;;%s\a%s\033]8;;\a' "$1" "$2"; }
96
- CLI="node ${SCRIPT_DIR}/../bin/cli.js"
97
-
98
95
  # ── Output (single line) ─────────────────────────────────────────
96
+ LINE="ThumbGate v${TG_VERSION} · ${TG_TIER}"
99
97
  if [ "$UP" = "0" ] && [ "$DOWN" = "0" ]; then
100
- echo -e "${D}ThumbGate: no feedback yet — type 'thumbs up' or 'thumbs down'${RST}"
98
+ echo -e "${D}${LINE} · no feedback yet${RST}"
101
99
  else
102
- # Feedback counts
103
- LINE="ThumbGate: ${G}${BD}${UP}${RST}👍 ${R}${BD}${DOWN}${RST}👎 · ${M}${BD}${LESSONS}${RST} lessons ${ARROW}"
100
+ LINE="${LINE} · ${G}${BD}${UP}${RST}👍 ${R}${BD}${DOWN}${RST}👎 ${ARROW}"
104
101
 
105
102
  # Control Tower alerts (if any)
106
103
  [ "${SLO_V:-0}" -gt 0 ] && LINE="${LINE} ${R}${SLO_V} SLO${RST}"
107
104
  [ "${AT_RISK:-0}" -gt 0 ] && LINE="${LINE} ${R}${AT_RISK}⚠${RST}"
108
105
  [ "${ANOMALIES:-0}" -gt 0 ] && LINE="${LINE} ${R}${ANOMALIES}☠${RST}"
109
106
 
110
- # Most recent lesson
111
- if [ -n "$LESSON_TEXT" ]; then
112
- LINE="${LINE} · ${C}${LESSON_TEXT}${RST}"
113
- fi
114
-
115
107
  echo -e "$LINE"
116
108
  fi
@@ -19,6 +19,14 @@ const path = require('path');
19
19
 
20
20
  const PROJECT_ROOT = path.join(__dirname, '..');
21
21
 
22
+ function explicitPinnedServeArgs(version) {
23
+ return ['--yes', '--package', `thumbgate@${version}`, 'thumbgate', 'serve'];
24
+ }
25
+
26
+ function explicitLatestServeArgs() {
27
+ return ['--yes', '--package', 'thumbgate@latest', 'thumbgate', 'serve'];
28
+ }
29
+
22
30
  function readJson(relPath) {
23
31
  return JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, relPath), 'utf-8'));
24
32
  }
@@ -210,11 +218,11 @@ function syncVersion(opts) {
210
218
  const codexPluginConfigPath = 'plugins/codex-profile/.mcp.json';
211
219
  if (fs.existsSync(path.join(PROJECT_ROOT, codexPluginConfigPath))) {
212
220
  const codexPluginConfig = readJson(codexPluginConfigPath);
213
- const server = codexPluginConfig.mcpServers && codexPluginConfig.mcpServers.rlhf;
214
- const expectedArgs = ['-y', `thumbgate@${version}`, 'serve'];
221
+ const server = codexPluginConfig.mcpServers && codexPluginConfig.mcpServers.thumbgate;
222
+ const expectedArgs = explicitPinnedServeArgs(version);
215
223
  const currentArgs = server && Array.isArray(server.args) ? server.args : [];
216
224
  if (server && server.command === 'npx' && JSON.stringify(currentArgs) !== JSON.stringify(expectedArgs)) {
217
- drifted.push({ file: codexPluginConfigPath, field: 'mcpServers.rlhf.args', current: JSON.stringify(currentArgs) });
225
+ drifted.push({ file: codexPluginConfigPath, field: 'mcpServers.thumbgate.args', current: JSON.stringify(currentArgs) });
218
226
  if (!checkOnly) {
219
227
  server.args = expectedArgs.slice();
220
228
  writeJson(codexPluginConfigPath, codexPluginConfig);
@@ -242,11 +250,11 @@ function syncVersion(opts) {
242
250
  const claudeCodexBridgeConfigPath = 'plugins/claude-codex-bridge/.mcp.json';
243
251
  if (fs.existsSync(path.join(PROJECT_ROOT, claudeCodexBridgeConfigPath))) {
244
252
  const bridgeConfig = readJson(claudeCodexBridgeConfigPath);
245
- const server = bridgeConfig.mcpServers && bridgeConfig.mcpServers.rlhf;
246
- const expectedArgs = ['-y', `thumbgate@${version}`, 'serve'];
253
+ const server = bridgeConfig.mcpServers && bridgeConfig.mcpServers.thumbgate;
254
+ const expectedArgs = explicitPinnedServeArgs(version);
247
255
  const currentArgs = server && Array.isArray(server.args) ? server.args : [];
248
256
  if (server && server.command === 'npx' && JSON.stringify(currentArgs) !== JSON.stringify(expectedArgs)) {
249
- drifted.push({ file: claudeCodexBridgeConfigPath, field: 'mcpServers.rlhf.args', current: JSON.stringify(currentArgs) });
257
+ drifted.push({ file: claudeCodexBridgeConfigPath, field: 'mcpServers.thumbgate.args', current: JSON.stringify(currentArgs) });
250
258
  if (!checkOnly) {
251
259
  server.args = expectedArgs.slice();
252
260
  writeJson(claudeCodexBridgeConfigPath, bridgeConfig);
@@ -259,11 +267,11 @@ function syncVersion(opts) {
259
267
  const cursorPluginConfigPath = 'plugins/cursor-marketplace/mcp.json';
260
268
  if (fs.existsSync(path.join(PROJECT_ROOT, cursorPluginConfigPath))) {
261
269
  const cursorPluginConfig = readJson(cursorPluginConfigPath);
262
- const server = cursorPluginConfig.mcpServers && cursorPluginConfig.mcpServers.rlhf;
263
- const expectedArgs = ['-y', 'thumbgate@latest', 'serve'];
270
+ const server = cursorPluginConfig.mcpServers && cursorPluginConfig.mcpServers.thumbgate;
271
+ const expectedArgs = explicitLatestServeArgs();
264
272
  const currentArgs = server && Array.isArray(server.args) ? server.args : [];
265
273
  if (server && server.command === 'npx' && JSON.stringify(currentArgs) !== JSON.stringify(expectedArgs)) {
266
- drifted.push({ file: cursorPluginConfigPath, field: 'mcpServers.rlhf.args', current: JSON.stringify(currentArgs) });
274
+ drifted.push({ file: cursorPluginConfigPath, field: 'mcpServers.thumbgate.args', current: JSON.stringify(currentArgs) });
267
275
  if (!checkOnly) {
268
276
  server.args = expectedArgs.slice();
269
277
  writeJson(cursorPluginConfigPath, cursorPluginConfig);
@@ -343,12 +351,16 @@ function syncVersion(opts) {
343
351
  // 13. public/index.html — static landing proof pill + footer version
344
352
  const publicIndexPath = 'public/index.html';
345
353
  if (fs.existsSync(path.join(PROJECT_ROOT, publicIndexPath))) {
346
- const publicContent = fs.readFileSync(path.join(PROJECT_ROOT, publicIndexPath), 'utf-8');
347
- const heroVersionMatch = publicContent.match(/New in v(\d+\.\d+\.\d+):/);
354
+ const publicIndexFile = path.join(PROJECT_ROOT, publicIndexPath);
355
+ const publicContent = fs.readFileSync(publicIndexFile, 'utf-8');
356
+ const heroVersionMatch = publicContent.match(/New in v(\d+\.\d+\.\d+):?/);
348
357
  if (heroVersionMatch && heroVersionMatch[1] !== version) {
349
358
  drifted.push({ file: publicIndexPath, field: 'hero-release-note', current: heroVersionMatch[1] });
350
359
  if (!checkOnly) {
351
- replaceInFile(publicIndexPath, `New in v${heroVersionMatch[1]}:`, `New in v${version}:`);
360
+ fs.writeFileSync(
361
+ publicIndexFile,
362
+ publicContent.replace(/New in v\d+\.\d+\.\d+:?/, `New in v${version}`)
363
+ );
352
364
  }
353
365
  }
354
366