thumbgate 0.9.10 → 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.
- package/.claude-plugin/README.md +2 -2
- package/.claude-plugin/marketplace.json +4 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +115 -312
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +4 -4
- package/adapters/mcp/server-stdio.js +61 -1
- package/adapters/opencode/opencode.json +4 -2
- package/bin/cli.js +156 -8
- package/bin/memory.sh +3 -3
- package/config/e2e-critical-flows.json +4 -0
- package/config/gates/default.json +74 -2
- package/config/github-about.json +1 -1
- package/config/mcp-allowlists.json +27 -0
- package/package.json +22 -5
- package/plugins/amp-skill/INSTALL.md +1 -0
- package/plugins/amp-skill/SKILL.md +1 -0
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +4 -2
- package/plugins/claude-skill/INSTALL.md +1 -0
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +4 -2
- package/plugins/codex-profile/INSTALL.md +1 -1
- package/plugins/codex-profile/README.md +1 -1
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-marketplace/README.md +3 -3
- package/plugins/cursor-marketplace/mcp.json +3 -1
- package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
- package/plugins/gemini-extension/INSTALL.md +3 -3
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/dashboard.html +15 -8
- package/public/index.html +125 -185
- package/public/js/buyer-intent.js +252 -0
- package/public/pro.html +1085 -0
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/adk-consolidator.js +14 -2
- package/scripts/agent-readiness.js +3 -1
- package/scripts/agent-security-hardening.js +4 -4
- package/scripts/auto-promote-gates.js +2 -0
- package/scripts/auto-wire-hooks.js +105 -17
- package/scripts/behavioral-extraction.js +2 -6
- package/scripts/billing.js +107 -3
- package/scripts/budget-guard.js +2 -2
- package/scripts/build-metadata.js +14 -0
- package/scripts/context-engine.js +1 -0
- package/scripts/deploy-policy.js +3 -17
- package/scripts/dpo-optimizer.js +3 -6
- package/scripts/ensure-repo-bootstrap.js +129 -0
- package/scripts/export-dpo-pairs.js +2 -3
- package/scripts/export-kto-pairs.js +3 -4
- package/scripts/export-training.js +8 -6
- package/scripts/feedback-attribution.js +23 -11
- package/scripts/feedback-loop.js +40 -2
- package/scripts/feedback-to-rules.js +2 -1
- package/scripts/filesystem-search.js +3 -2
- package/scripts/gates-engine.js +760 -29
- package/scripts/generate-pretool-hook.sh +0 -0
- package/scripts/gtm-revenue-loop.js +20 -1
- package/scripts/hook-auto-capture.sh +8 -3
- package/scripts/hook-runtime.js +89 -0
- package/scripts/hook-stop-self-score.sh +3 -3
- package/scripts/hook-thumbgate-cache-updater.js +99 -38
- package/scripts/hosted-config.js +4 -16
- package/scripts/hybrid-feedback-context.js +54 -14
- package/scripts/install-mcp.js +13 -0
- package/scripts/intent-router.js +2 -2
- package/scripts/license.js +52 -14
- package/scripts/local-model-profile.js +3 -2
- package/scripts/mcp-config.js +68 -6
- package/scripts/meta-policy.js +4 -8
- package/scripts/money-watcher.js +166 -16
- package/scripts/obsidian-export.js +1 -0
- package/scripts/operational-integrity.js +480 -0
- package/scripts/post-everywhere.js +7 -12
- package/scripts/pr-manager.js +14 -11
- package/scripts/profile-router.js +2 -0
- package/scripts/prompt-dlp.js +1 -0
- package/scripts/publish-decision.js +10 -0
- package/scripts/published-cli.js +34 -0
- package/scripts/risk-scorer.js +3 -2
- package/scripts/rlhf_session_start.sh +32 -0
- package/scripts/skill-quality-tracker.js +3 -5
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
- package/scripts/social-analytics/engagement-audit.js +202 -0
- package/scripts/social-analytics/instagram-thumbgate-post.js +45 -7
- package/scripts/social-analytics/install-growth-automation.js +114 -0
- package/scripts/social-analytics/load-env.js +46 -0
- package/scripts/social-analytics/poll-all.js +3 -18
- package/scripts/social-analytics/pollers/zernio.js +3 -0
- package/scripts/social-analytics/publish-instagram-thumbgate.js +22 -3
- package/scripts/social-analytics/publish-thumbgate-launch.js +316 -0
- package/scripts/social-analytics/publishers/reddit.js +7 -12
- package/scripts/social-analytics/publishers/zernio.js +210 -22
- package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
- package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
- package/scripts/social-analytics/sync-launch-assets.js +185 -0
- package/scripts/social-post-hourly.js +185 -0
- package/scripts/social-quality-gate.js +119 -3
- package/scripts/social-reply-monitor.js +148 -32
- package/scripts/statusline-cache-path.js +27 -0
- package/scripts/statusline-meta.js +22 -0
- package/scripts/statusline.sh +24 -32
- package/scripts/sync-version.js +11 -3
- package/scripts/test-coverage.js +20 -13
- package/scripts/tool-registry.js +97 -0
- package/scripts/train_from_feedback.py +32 -9
- package/scripts/validate-feedback.js +3 -2
- package/scripts/vector-store.js +2 -3
- package/scripts/verify-obsidian-setup.sh +3 -3
- package/src/api/server.js +281 -33
|
@@ -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
|
+
});
|
|
@@ -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
|
-
|
|
17
|
-
function
|
|
18
|
-
|
|
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
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
|
|
@@ -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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
381
|
+
console.warn('[reply-monitor] Could not load X modules, skipping X');
|
|
267
382
|
return [];
|
|
268
383
|
}
|
|
269
384
|
|
|
270
|
-
|
|
271
|
-
let mentions;
|
|
385
|
+
let searchResult;
|
|
272
386
|
try {
|
|
273
|
-
|
|
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
|
-
|
|
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 = {
|
|
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 };
|